$linuxjunkies
>

How to Install Caddy: The TLS-Automatic Web Server

Install Caddy web server, configure the Caddyfile, get automatic HTTPS from Let's Encrypt, and set up reverse proxying in under 15 minutes.

BeginnerUbuntuDebianFedoraArch8 min readUpdated June 7, 2026

Before you start

  • A server with a public IP address and root or sudo access
  • A domain name with an A record pointing to that IP (required for automatic HTTPS)
  • Ports 80 and 443 not in use by another service such as Apache or Nginx

Caddy is a production-ready web server written in Go. Its headline feature is automatic HTTPS: give it a domain name and it fetches, renews, and reloads TLS certificates from Let's Encrypt or ZeroSSL without any manual steps. Configuration is a single human-readable file. If you have wrestled with Certbot and Nginx reloads at 3 a.m., Caddy is a welcome change.

Install Caddy

The Caddy project publishes official packages for all major distros. Use the official repo rather than the version in your distro's default repos — it is usually significantly older.

Debian / Ubuntu

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
  | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
  | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy

Fedora / RHEL 9 / Rocky Linux 9

sudo dnf install -y 'dnf-command(copr)'
sudo dnf copr enable -y @caddy/caddy
sudo dnf install -y caddy

Arch Linux

sudo pacman -S caddy

Arch ships a reasonably current build from the community repo. For the absolute latest, use the caddy-git AUR package.

Enable and Start the Service

The package creates a caddy system user and installs a systemd unit. Enable it so it starts on boot, then start it now.

sudo systemctl enable --now caddy

Verify it is running:

sudo systemctl status caddy

You should see active (running). Caddy binds to ports 80 and 443 by default. If another service already owns those ports, Caddy will fail to start — stop that service first.

Open the Firewall

Caddy needs inbound HTTP and HTTPS traffic to reach the server. It also needs outbound HTTPS to reach the ACME certificate authority.

ufw (Debian / Ubuntu)

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp   # HTTP/3 over QUIC
sudo ufw reload

firewalld (Fedora / RHEL / Rocky)

sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --add-port=443/udp
sudo firewall-cmd --reload

Understand the Caddyfile

Caddy's primary configuration format is the Caddyfile, located at /etc/caddy/Caddyfile after a package install. Its syntax is minimal: a block opens with an address, and directives go inside curly braces.

sudo nano /etc/caddy/Caddyfile

A bare-minimum file looks like this:

example.com {
    root * /var/www/html
    file_server
}

That single block tells Caddy to serve static files from /var/www/html over HTTPS. Caddy will automatically obtain a certificate for example.com the first time a request arrives, as long as the domain resolves to this server's public IP.

Serve a Static Site

Create web root and a test page

sudo mkdir -p /var/www/html
echo '<h1>Hello from Caddy</h1>' | sudo tee /var/www/html/index.html

Set ownership

The caddy process runs as the caddy user. Give it read access to the web root.

sudo chown -R caddy:caddy /var/www/html

Write the Caddyfile

sudo tee /etc/caddy/Caddyfile <<'EOF'
example.com {
    root * /var/www/html
    file_server
    encode gzip
    log {
        output file /var/log/caddy/access.log
    }
}
EOF

Validate and reload

caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

caddy validate parses the file and prints any errors before they affect the running service. Make this a habit before every reload.

Set Up a Reverse Proxy

Caddy's most common production use is fronting an application that listens on a local port — a Node app, a Python API, a Gitea instance, and so on. The reverse_proxy directive handles this in one line and still manages TLS automatically.

sudo tee /etc/caddy/Caddyfile <<'EOF'
app.example.com {
    reverse_proxy localhost:3000
}
EOF

If your backend uses its own self-signed TLS, tell Caddy not to verify it:

app.example.com {
    reverse_proxy https://localhost:3000 {
        transport http {
            tls_insecure_skip_verify
        }
    }
}

For multiple sites in one file, simply stack blocks:

blog.example.com {
    root * /var/www/blog
    file_server
}

api.example.com {
    reverse_proxy localhost:8080
}

app.example.com {
    reverse_proxy localhost:3000
}

After each edit, validate and reload:

caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

Automatic HTTPS — How It Works

When Caddy sees a hostname that looks public (not localhost or an IP), it automatically:

  • Contacts Let's Encrypt (or ZeroSSL) via the ACME protocol.
  • Completes an HTTP-01 or TLS-ALPN-01 challenge to prove domain ownership.
  • Stores the certificate and private key under /var/lib/caddy/.local/share/caddy/.
  • Renews certificates automatically before expiry, with zero downtime.
  • Redirects plain HTTP to HTTPS with a permanent redirect.

For this to work your DNS A record must point to this server's public IP before you start Caddy with that hostname. Let's Encrypt also rate-limits failed attempts, so test with a staging environment if you are iterating quickly:

example.com {
    tls {
        ca https://acme-staging-v02.api.letsencrypt.org/directory
    }
    root * /var/www/html
    file_server
}

Remove the tls block once everything looks correct to get a real certificate.

Verify TLS and Logs

Check that the certificate was issued and looks correct:

curl -I https://example.com

You should see HTTP/2 200 and no certificate warnings. For more detail:

openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>&1 \
  | grep -E 'subject|issuer|Verify'

Follow the Caddy journal for real-time diagnostics:

sudo journalctl -u caddy -f

Troubleshooting

  • Port 80 or 443 already in use. Run sudo ss -tlnp | grep -E ':80|:443' to identify the conflicting process, then stop it before starting Caddy.
  • Certificate not issued, ACME errors in the journal. Confirm your DNS resolves to this server (dig +short example.com), that ports 80 and 443 are reachable from the internet, and that your firewall rules are applied.
  • Permission denied reading web root. Verify ownership with ls -la /var/www/html and ensure the caddy user has at least read and execute on all parent directories.
  • Config changes have no effect. Confirm you ran sudo systemctl reload caddy (not restart) and that caddy validate reported no errors.
  • Reverse proxy returns 502. The upstream service is not listening on the expected port. Check it with sudo ss -tlnp | grep 3000 (replace with your port).
tested on:Ubuntu 24.04Debian 12Fedora 40Arch rolling

Frequently asked questions

Does Caddy renew certificates automatically, or do I need a cron job?
Caddy handles renewal entirely by itself. It tracks expiry internally and renews certificates in the background well before they expire, with no cron job or external tool required.
Can I use Caddy on a server without a public domain name?
Yes. For localhost or private IP addresses, Caddy automatically issues a locally trusted certificate using its own CA. For production use with a real domain, the domain must resolve to your server's public IP.
How do I serve multiple domains or subdomains from one Caddy instance?
Add separate site blocks in the Caddyfile, one per hostname. Caddy obtains and manages a certificate for each domain independently. Reload after saving.
Does Caddy support HTTP/3?
Yes. Caddy enables HTTP/3 over QUIC by default on port 443 UDP when HTTPS is active. No extra configuration is needed; just make sure UDP 443 is open in your firewall.
What is the difference between `systemctl reload caddy` and `systemctl restart caddy`?
Reload performs a graceful in-place configuration update with zero dropped connections. Restart stops and restarts the process, briefly interrupting active connections. Always prefer reload for config changes.

Related guides