Overview
I put off trying Caddy for about two years, mostly out of habit. Nginx configs are muscle memory at this point, and Certbot does its job. Then I set up a side project on a fresh VPS and had HTTPS working in under a minute, with no certbot, no cron job, and no renewal email to ignore. That was the end of the debate for me.
Here's what's actually going on, and where Caddy is the wrong choice.
The short version
Caddy obtains and renews TLS certificates on its own. You write a config file with your domain in it, start the service, and it handles the ACME challenge, the certificate, the renewal, and the HTTP-to-HTTPS redirect. There is no separate renewal timer because there is nothing to renew manually.
The tradeoff is that Caddy is doing things on your behalf, and when something goes wrong it can be less obvious why. More on that below.
Installing it
The official packages are the least painful route — the repo setup is documented at caddyserver.com/docs/install. On Debian or Ubuntu that's the standard add-repo-then-apt-install dance. On macOS, brew install caddy works fine.
Verify before doing anything else:
caddy version
If you'd rather not add a third-party repo, there's a download builder on the Caddy site that produces a single static binary with only the modules you pick. Useful for minimal container images.
A real config file
The default config path is /etc/caddy/Caddyfile. Here's a config that serves a static site and proxies an API on the same domain:
example.com {
encode gzip zstd
handle /api/* {
reverse_proxy 127.0.0.1:3000
}
handle {
root * /var/www/example
file_server
try_files {path} /index.html
}
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
-Server
}
log {
output file /var/log/caddy/example.log
format json
}
}
Two things worth noticing. First, there's no listen 443 and no certificate paths — the site block keyed on the domain name is enough. Second, the handle blocks are mutually exclusive and evaluated in order, which is closer to how you'd naturally think about routing than Nginx's location-matching rules.
The one syntax trap: the Caddyfile is whitespace-sensitive in the sense that block braces must open on the same line as the directive. This is invalid:
example.com
{
file_server
}
Caddy will parse that as a site address with no block and then complain about the stray brace. I've watched two people lose twenty minutes to this.
Reverse proxy headers come for free
With Nginx you write the X-Forwarded-For, X-Forwarded-Proto, and Host headers by hand and remember to add the WebSocket upgrade pair. Caddy's reverse_proxy sets all of them, including the upgrade headers, without configuration. If your app has been reading X-Forwarded-Proto incorrectly behind Nginx, this alone is a reason to switch.
Passwords and other small jobs
Basic auth, which is genuinely useful for staging environments:
caddy hash-password --plaintext 'your-password'
Then in the Caddyfile:
staging.example.com {
basicauth {
admin $2a$14$replace.with.the.hash
}
reverse_proxy 127.0.0.1:3000
}
Local HTTPS for development, without a browser warning:
caddy run --config ./dev.Caddyfile
With a site block like localhost { reverse_proxy 127.0.0.1:8000 }, Caddy installs its local CA into your trust store and you get a green padlock on localhost. It's a nicer workflow than mkcert if you already have Caddy around.
When it breaks
The failure modes are predictable once you've seen them.
| Symptom | Almost always means |
|---|---|
| Certificate never issues, logs mention challenge | DNS hasn't propagated, or port 80 is blocked |
too many failed authorizations | You hit Let's Encrypt's failure limit while testing — switch to the staging CA with acme_ca |
| Works locally, fails on the server | Cloudflare proxy or another cdn sitting in front |
| Config parses but nothing responds | Another process holds port 80 or 443 |
Testing against the staging CA is worth knowing before you need it. Add this to the top of the Caddyfile while you're iterating:
{
acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}
Certificates from staging aren't trusted by browsers, which is the point — you can break things without burning your production rate limit. Remove the line when you're done.
Where Nginx still wins
Caddy isn't the right answer everywhere. If you need a module that only exists as a third-party Nginx extension, you're stuck. If your team already has a decade of Nginx configs, the migration cost is real. And if you're serving static files at very high volume, Nginx's sendfile tuning gives you more knobs than Caddy exposes.
For a small site, a side project, or anything where you'd otherwise be babysitting certificates, Caddy removes a whole category of maintenance work. That's the whole pitch, and it's a good one.
