Serving a Static Site With Caddy and Automatic HTTPS
Caddy's headline feature is that HTTPS needs no configuration: write a domain name in the Caddyfile, start the server, and a trusted certificate appears and renews itself. For one site on one server that really is the whole story. Running several static sites in production on one Caddy server raises the questions the headline skips: what must be true of DNS and the network for issuance to work, how to test without burning through certificate authority rate limits, where certificates live and what happens when the server is rebuilt, and how to get wildcard certificates for preview subdomains.
This guide sets up a 2-vCPU VM that serves four static sites — documentation, marketing, a status page and per-branch previews — with Caddy 2.8, covering the certificate side end to end. It belongs to Self-Hosting Static Sites on S3, Nginx and Caddy; the basic Caddy setup is in Serving a Static Site with Caddy, and headers in Security Headers for Static Sites Served by Caddy.
Prerequisites
- A VM with a public IPv4 (and ideally IPv6) address, Caddy installed from its official package repository.
- DNS for each domain you control, with A/AAAA records you can point at the server.
- Firewall rules allowing inbound TCP 80 and 443, and UDP 443 for HTTP/3.
How Automatic HTTPS Works
Step 1: DNS and Network First
Most failed issuances are DNS or firewall problems. Before adding a domain to the Caddyfile, check from outside the server:
dig +short docs.example.com A @1.1.1.1 # must return the server's IP
dig +short docs.example.com AAAA @1.1.1.1 # if present, must also be this server
curl -sI http://docs.example.com/ --max-time 5 # port 80 must reach Caddy
An AAAA record pointing at an old server is a classic trap: the certificate authority may validate over IPv6, reach the old machine and fail. Either point AAAA at the new server or remove it.
Step 2: Configure the Sites, Starting With the Staging CA
{
email ops@example.com
acme_ca https://acme-staging-v02.api.letsencrypt.org/directory # remove after testing
}
docs.example.com {
root * /srv/docs/current
file_server { precompressed br gzip }
import security_headers
}
www.example.com, example.com {
root * /srv/www/current
file_server { precompressed br gzip }
import security_headers
}
status.example.com {
root * /srv/status/current
file_server
}
The staging CA issues untrusted certificates with much higher rate limits, so configuration mistakes during setup cost nothing. caddy validate checks syntax; journalctl -u caddy -f shows each issuance. Once all four names had staging certificates, the acme_ca line was removed and Caddy reloaded; production certificates were issued in about 20 seconds in total.
The email lets the certificate authority send expiry warnings if renewal ever fails, which is a useful second alarm.
Step 3: Redirect Apex and www
www.example.com, example.com in one block serves both names with the same content, which creates duplicate URLs. A separate block redirects one to the other:
example.com {
redir https://www.example.com{uri} permanent
}
www.example.com {
root * /srv/www/current
file_server { precompressed br gzip }
import security_headers
}
Caddy obtains certificates for both names, so the redirect itself is served over HTTPS without a warning.
Step 4: Wildcard Certificates for Previews
Branch previews are served at <branch>.preview.example.com, created and deleted by CI. One certificate per branch would hit rate limits on busy days. A wildcard certificate covers them all; it needs the DNS challenge, which needs Caddy built with the DNS provider's module:
xcaddy build --with github.com/caddy-dns/cloudflare
*.preview.example.com {
tls { dns cloudflare {env.CF_API_TOKEN} }
root * /srv/previews/{labels.3}
file_server
header X-Robots-Tag "noindex, nofollow"
}
{labels.3} is the branch label of the hostname, so feature-x.preview.example.com serves /srv/previews/feature-x. The API token is scoped to DNS edits on the one zone. Previews get noindex, as described in Fixing Preview Deploys Leaking Into Search Results.
Step 5: Persist and Back Up the Data Directory
Certificates, private keys and the ACME account live in Caddy's data directory (/var/lib/caddy/.local/share/caddy for the packaged service). The VM is rebuilt from an image a few times a year; the first rebuild lost the directory, and Caddy reissued all five certificates on start — fine once, but a rebuild loop during an incident could have hit the duplicate-certificate limit. The directory now lives on an attached volume that survives rebuilds, and a nightly backup copies it to object storage, encrypted.
Step 6: Monitor Renewal Anyway
Caddy renews about 30 days before expiry and retries on failure, but renewal can still fail for reasons outside it: a DNS change, a firewall rule, an expired DNS API token for the wildcard. An external check alerts when any of the five certificates has fewer than 21 days left, as in Monitoring TLS Certificate Expiry. In the first year it fired once — the DNS API token for the wildcard had been rotated without updating the server.
Running Two Servers
The status page must stay up when the main server does not, so it also runs on a second small VM in another region. Two Caddy servers obtaining certificates for the same names independently would each work, but doubles issuance and can trip rate limits during rebuilds. Caddy supports shared storage for certificates — any storage module both servers can reach, such as a shared filesystem or an object store through a plugin — and coordinates issuance through it with locks, so one server obtains a certificate and both use it. The team used a small S3-compatible bucket through a storage module, with DNS failover health checks switching status.example.com to the second VM if the first stops answering.
For the other sites, a single server behind a CDN was enough: the CDN serves cached pages through short origin outages, and a rebuilt server comes back with its certificates from the persistent volume in under a minute.
Measured Impact
| Measure | Value | Source |
|---|---|---|
| Time to add a new site with HTTPS | ~5 min (DNS + 4 config lines) | Runbook timing |
| Certificate-related config lines | 2 (email, wildcard tls) | Caddyfile |
| Renewal failures in 12 months | 1 (rotated DNS token), caught at 21 days | Monitoring |
| SSL Labs grade | A+ (with HSTS from the header snippet) | SSL Labs |
| TLS handshake time p50 (TLS 1.3, HTTP/3 available) | 21 ms in-region | curl -w |
No certificate has expired on the server since it went live.
Pitfalls & Rollback
- Stale AAAA records. Validation over IPv6 reaches the wrong server.
- Testing against the production CA. Use the staging CA until everything works.
- Ephemeral data directories. Persist and back up certificates and keys.
- Unmonitored DNS tokens for wildcards. Rotations break renewal silently.
- Rollback: each site is an independent block; removing it and reloading takes it offline without affecting the others.
Conclusion
Automatic HTTPS in Caddy is genuinely automatic once its preconditions hold: correct A and AAAA records, open ports 80 and 443, a persistent data directory and, for wildcards, a scoped DNS token. Testing against the staging CA, keeping certificates on a persistent volume and monitoring expiry independently turned four static sites and unlimited preview subdomains into five certificates that renewed themselves for a year with one caught failure.
FAQ
What does Caddy need to obtain certificates automatically?
A public DNS record pointing the domain at the server, ports 80 and 443 reachable from the internet for the HTTP and TLS-ALPN challenges, and a writable data directory to store certificates and account keys. With those in place, putting the domain name in the Caddyfile is enough.
How do I avoid hitting Let's Encrypt rate limits while testing?
Point Caddy at the Let's Encrypt staging environment with the acme_ca global option while setting up, which has much higher limits and issues untrusted test certificates. Switch back to production once the configuration works. Caddy also falls back to ZeroSSL automatically if Let's Encrypt fails.
Can Caddy issue wildcard certificates?
Yes, using the DNS challenge, which requires a Caddy build with the DNS provider module for your DNS host and an API token. Wildcards are useful for many subdomains such as per-branch previews; for a handful of fixed hostnames, individual certificates through the default challenges are simpler.
What happens to certificates if the server is rebuilt?
Caddy stores certificates in its data directory. If that directory is lost, Caddy requests new certificates on start, which works but counts against rate limits and fails if many domains are reissued at once. Back up the data directory or put it on persistent storage that survives rebuilds.
Related
- Parent: Self-Hosting Static Sites on S3, Nginx and Caddy — running your own hosting.
- Serving a Static Site with Caddy — the basic setup.
- Nginx vs Caddy for Serving Static Sites — why Caddy for this server.
- Security Headers for Static Sites Served by Caddy — the header snippet imported here.
- Zero-Downtime Deploys with Symlink Swaps — the
currentdirectories each site serves.