Caddy: The Web Server That Ended My 3 AM Certificate Alerts
Every operator has a certificate story. Mine ended with a 3 AM page because a certbot cron job had been failing silently for two months — the renewal hook script returned 0, the log rotated away, and Let's Encrypt's expiry notice went to an inbox nobody read. The site went down at the worst possible hour, for the most preventable reason in the industry.
That was the week I stopped hand-rolling HTTPS and moved a chunk of my fleet to Caddy. Not because nginx is bad — it isn't — but because I was tired of owning a problem that a web server can own for me.
What Caddy actually is
Caddy is a web server written in Go with one party trick that changes everything: automatic HTTPS. Point a domain's DNS at your server, write a three-line config, and Caddy obtains a certificate from Let's Encrypt (falling back to ZeroSSL), installs it, serves HTTPS, and renews it forever — no cron job, no hook script, no 3 AM pages.
The configuration format is called the Caddyfile, and it reads like someone finally got fed up with 400-line nginx configs:
example.com {
root * /srv/www
file_server
}
That's a complete, HTTPS-enabled static site. The whole philosophy is that the common case should be boring, and the uncommon case should still be possible.
Installing it the supported way
Skip the random blog-post install scripts. The official packages handle the systemd service, the caddy user, and the file permissions for you.
Debian / Ubuntu / Raspbian (from the official docs):
sudo apt install --yes 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
Fedora / RHEL / CentOS:
sudo dnf install dnf5-plugins # dnf-plugins-core on RHEL/CentOS
sudo dnf copr enable @caddy/caddy
sudo dnf install caddy
Arch: sudo pacman -Syu caddy. There's also an official Docker image. The Debian package starts the caddy systemd service automatically; on the RPM side you enable it yourself with systemctl enable --now caddy.
Your first site in sixty seconds
Edit /etc/caddy/Caddyfile, then validate and reload — Caddy reloads with zero downtime:
sudoedit /etc/caddy/Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo systemctl reload caddy
That validate step is the habit that separates calm operators from 3 AM operators. It catches typos before they hit production. Run it every time.
The workload that matters: reverse proxy
Most of us don't serve static files — we front apps. Here's a complete reverse proxy with access logging:
app.example.com {
reverse_proxy 127.0.0.1:8080
log {
output file /var/log/caddy/app.log
format json
}
}
HTTPS, cert management, proxy headers (X-Forwarded-For and friends are added automatically), structured logs. Compare that to the nginx equivalent and you'll understand why Caddy converts people.
A couple of extras worth knowing:
- Load balancing is built in:
reverse_proxy 10.0.0.2:8080 10.0.0.3:8080withlb_policyand active health checks available. - Path handling is explicit:
handle_path /api/*strips the prefix before proxying, which removes an entire class of "works on localhost, 404s in production" bugs. - Static + proxy on one domain works naturally — matchers route requests to the right handler.
The five traps that still bite people
Automatic HTTPS feels like magic, which means the failure modes feel like betrayal. They're all documented, and they're all operator error:
1. DNS must resolve before the first request. Caddy obtains the certificate when the first client arrives. If DNS isn't pointing at the server yet — or is cached to the old one — the ACME challenge fails and HTTPS never comes up. Point DNS, wait for propagation, then expect green locks.
2. Ports 80 and 443 must be publicly reachable. HTTP-01 challenges arrive on port 80; TLS-ALPN-01 on 443. Behind NAT, a firewall, or a security group that only opens 443? The challenge can't complete. Open both.
3. Rate limits are real. Let's Encrypt allows 50 certificates per registered domain per week. If you script Caddy deployments and keep wiping state, you'll hit the wall and stare at errors for a week. Keep /var/lib/caddy (the cert storage) persistent — bind-mount it in Docker, back it up on bare metal.
4. On-demand TLS without an ask endpoint is self-harm. The on_demand option mints certificates for arbitrary hostnames on the fly — great for SaaS custom domains, catastrophic if anyone on the internet can trigger issuance. Always gate it with an ask endpoint that whitelists your customers' domains, or strangers will burn your rate limit for fun.
5. Wildcards and internal names need extra steps. Wildcard certs require the DNS-01 challenge, which means building Caddy with your DNS provider's plugin (xcaddy build --with ...). For homelab names that never touch public DNS, tls internal issues certs from Caddy's own local CA — but then you must trust that CA on your clients (caddy trust), or browsers will scream at you.
Bonus trap: the admin API listens on localhost:2019 by default. That's fine. If you ever expose it to the network for automation, you've handed remote config control to anyone who can reach it. Leave it on localhost, or turn it off with admin off in the global options block.
When Caddy isn't the answer
Honesty time. If you live in nginx's module ecosystem (Lua scripting, exotic third-party modules), or you need the absolute last drop of edge performance with fine-tuned worker behavior, nginx still wins. Caddy trades a few percent of peak throughput for an enormous reduction in operational surface area. For the vast middle of the internet — personal projects, small business sites, homelab services, API front doors — that's the right trade.
The operator's summary
Caddy didn't make me a better engineer. It removed an entire category of outage I was bad at preventing: certificate management. The config fits on an index card, reloads are zero-downtime, and the traps above are all one-time lessons. If your HTTPS story still involves a cron job and a prayer, this is your sign to simplify it.
Sources: Caddy Documentation — Install · Caddy Documentation — Caddyfile Concepts · Caddy Documentation — Automatic HTTPS · caddyserver/caddy on GitHub
Reader feedback
Was this article helpful?
0 people found this article helpful.
