Web servers

How to get automatic HTTPS with Caddy

By · Last verified

Caddy obtains and renews a certificate for every site address in the Caddyfile on its own and redirects HTTP to HTTPS, with no TLS configuration. Name the domain in the Caddyfile, keep ports 80 and 443 open, and reload Caddy.

The steps apply to Caddy 2.x on Debian, Ubuntu and RHEL-compatible Linux, with example.com and www.example.com as the site names. Check the Caddy docs for the current release.

Prerequisites

Caddy does the ACME work itself, so it must own ports 80 and 443.

  • A Linux server with systemd and sudo access.
  • DNS A/AAAA records for example.com and www.example.com pointing at the server, for example 203.0.113.10.
  • Inbound TCP 80 and 443 open to all sources, and no other web server (Nginx, Apache) listening on them.
dig +short example.com A
sudo ss -ltnp | grep -E ':(80|443) '

Step 1: Install Caddy from the official repository

The Caddy project publishes an apt repository for Debian and 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 chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
sudo chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

On Fedora and RHEL-compatible systems the docs use the @caddy/caddy COPR repository. The package installs a caddy systemd service that reads /etc/caddy/Caddyfile.

Step 2: Write the Caddyfile

Naming a domain as the site address is all Caddy needs to enable HTTPS:

# /etc/caddy/Caddyfile
{
    email admin@example.com
}

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

For an application behind Caddy, replace the two body lines with reverse_proxy 127.0.0.1:3000. The global email is optional; it is attached to the ACME account.

Step 3: Validate and reload

Caddy requests the certificates as soon as it loads the new Caddyfile:

caddy fmt --overwrite /etc/caddy/Caddyfile
caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
sudo journalctl -u caddy --no-pager -n 50 | grep -iE 'certificate|obtain|error'

The log shows certificate obtained successfully for each name within a few seconds.

Step 4 (optional): Wildcards with the DNS-01 challenge

Wildcards need a DNS provider module, which the standard package does not include. Build Caddy with the module for your DNS host, for example Cloudflare:

xcaddy build --with github.com/caddy-dns/cloudflare

Then reference an API token from the environment of the Caddy service:

*.example.com, example.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
    reverse_proxy 127.0.0.1:3000
}

To request a specific Let's Encrypt profile, add issuer acme { profile tlsserver } inside the tls block; the docs mark this option experimental. See Let's Encrypt certificate profiles.

Verify

Both names should present a valid certificate, and plain HTTP should redirect to HTTPS. Check the result from outside with the SSL checker, or from any terminal:

curl ssl.sh/example.com

Confirm the redirect and the served certificate:

curl -sI http://example.com | head -n 3
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -dates

Automatic renewal

Renewal is built in and needs no cron job or timer. Caddy checks its certificates periodically, follows the CA's ARI renewal window when offered, and otherwise renews well before expiry, then hot-swaps the certificate without dropping connections. It will keep working when Let's Encrypt moves to 64-day certificates on February 10, 2027 and 45-day certificates on February 16, 2028.

What can break it: blocking port 80/443 after setup, moving DNS away, or deleting the data directory. Watch the journal for renewal errors and add the domain to ssl.sh monitoring (in development):

sudo journalctl -u caddy --since "7 days ago" | grep -i renew

Troubleshooting

  • listen tcp :443: bind: address already in use: another server holds the port; stop it or move it behind Caddy.
  • Timeout during connect or connection refused in the logs: port 80/443 is blocked or DNS is wrong; see certbot timeout during connect, which covers the same causes.
  • too many certificates already issued: repeated restarts with a broken config hit Let's Encrypt limits; test with acme_ca https://acme-staging-v02.api.letsencrypt.org/directory and read Let's Encrypt rate limits.
  • CAA record prevents issuance: your CAA records do not allow the CA Caddy used; see CAA record prevents issuance.
  • Browser shows an internal certificate: you used tls internal or a site address such as localhost; see self-signed certificate errors.

FAQ

Which CA does Caddy use by default?

Caddy tries Let's Encrypt first and falls back to ZeroSSL if issuance fails. To pin Let's Encrypt only, set acme_ca https://acme-v02.api.letsencrypt.org/directory in the global options block, and make sure your CAA records allow the CA you use.

Where does Caddy store its certificates?

In its data directory. For the systemd package, which runs as the caddy user, that is /var/lib/caddy/.local/share/caddy/certificates/. Back up the whole data directory rather than copying individual files, and do not edit it while Caddy is running.

Should I configure OCSP stapling in Caddy?

No. Caddy staples automatically when a certificate contains an OCSP URL. Let's Encrypt certificates no longer include one since 2025, so there is nothing to configure.

Is on-demand TLS a good idea?

Only with the on_demand_tls global option and an ask endpoint that approves known hostnames. Without it, anyone pointing a domain at your server can make Caddy request certificates and exhaust your rate limits.

Sources

  1. Caddy: Automatic HTTPS
  2. Caddy: Install
  3. Caddy: tls directive
  4. Let's Encrypt: Challenge Types