Caddy vs Nginx for Automatic HTTPS: When Each One Wins

Photo by Taylor Vick

Photo by Taylor Vick
Every VPS I provision eventually arrives at the same question: who terminates TLS? For most of my career the reflex answer was Nginx, with certbot bolted on for Let's Encrypt. Then I started using Caddy on side projects and internal tools, and the honest conclusion is that neither tool wins universally. They win in different situations, and knowing which situation you are in saves you from either babysitting cron-driven renewals or fighting an unfamiliar config language at 2 AM.
The short version: Caddy makes HTTPS a non-event — certificates are issued, renewed, and failed-over automatically the moment you declare a hostname. Nginx remains the better choice when you need its enormous ecosystem, battle-tested tuning knobs, or you already run a fleet standardized on it with Ansible. Below is the detailed comparison I wish I had before migrating my first server.
Caddy has served all sites over HTTPS by default since 2015. You write a Caddyfile that declares hostnames, and that declaration alone is the trigger: Caddy obtains certificates from an ACME provider, redirects HTTP to HTTPS, and keeps everything renewed without a single extra line. This is genuinely the whole config for two proxied apps:
# Caddyfile — this is the entire HTTPS configuration
app.example.com {
reverse_proxy localhost:3000
}
api.example.com {
reverse_proxy localhost:4000
}What you get implicitly with that file is the part that impresses me most:
Nginx does not do ACME by itself; the standard pairing is certbot, installed via snap per EFF's current instructions. Setup is short but it is a separate moving part with its own lifecycle:
# Nginx + certbot: install once, then per domain
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot
sudo certbot --nginx -d app.example.com
# verify the renewal timer actually works
sudo certbot renew --dry-run
systemctl list-timers | grep certbotCertbot installs a systemd timer (or cron job) that renews certificates before expiry and reloads Nginx. It works well — I have run it for years across client VPSs — but note the verbs in that sentence: installs, renews, reloads. Three things that live outside your web server and can fail independently of it. The dry-run check above belongs in your provisioning playbook, not in your memory.
Benchmarks mostly measure scenarios you will never hit on a 2-vCPU VPS. These are the dimensions that actually drove my decisions:
| Aspect | Caddy | Nginx + certbot |
|---|---|---|
| HTTPS setup effort | Zero beyond naming the host. Redirects, issuance, and renewal are defaults. | Install certbot, run it per domain, verify the timer. Scriptable but real work. |
| Renewal model | In-process, continuous, no external scheduler to audit. | systemd timer or cron outside the server; must be monitored separately. |
| CA failover | Automatic fallback from Let's Encrypt to ZeroSSL, then retry with backoff. | Single CA unless you script alternatives yourself. |
| Ecosystem and docs | Smaller community; fewer copy-paste answers for exotic setups. | Twenty years of modules, tutorials, and Stack Overflow coverage for nearly everything. |
| Low-level tuning | Sensible defaults; deep tuning happens in JSON config and is less documented. | Fine-grained control: ssl_session_cache, worker tuning, buffer sizes, rate limiting modules. |
Both stacks land on modern TLS defaults today: Nginx ships TLSv1.2 and TLSv1.3 as defaults since 1.27.3, and Caddy negotiates modern protocols out of the box. The real security difference is operational: an expired certificate is a security incident your users see, and Caddy's design makes that class of incident structurally harder to have.
Whichever you choose, never let TLS expiry be observable only by your users. I export certificate expiry into Prometheus and alert at 21 days remaining — that threshold has caught both a broken certbot timer and a Caddy instance whose storage volume had filled and could not persist a renewed certificate.
If you want to try Caddy on a live box without burning the bridge, this is the sequence I used:
Automatic HTTPS is no longer a differentiator you must pay for with complexity. Caddy made it a default, and for the small-to-medium VPS workloads most of us actually run, that default is worth real operational money: fewer moving parts, fewer expiry incidents, fewer playbooks.
But infrastructure decisions are about fleets and teams, not single servers. Nginx with a properly verified certbot timer is still a perfectly modern stack, and the right one when ecosystem depth, tuning control, or existing automation tips the scale. Pick per situation — and whichever you pick, put certificate expiry on a dashboard you look at.
Sources and further reading