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.
How Caddy Makes HTTPS Automatic
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:
- Issuance and renewal are managed inside the server process itself. There is no external timer, no cron job, no second binary that can silently break while the proxy keeps running.
- Fully redundant issuer failover: if Let's Encrypt has an outage or rate-limits you, Caddy automatically retries against ZeroSSL before backing off. With certbot, a CA outage during your renewal window is your problem to notice.
- Local and internal hostnames like localhost get self-signed certificates from Caddy's built-in CA, so your dev environment speaks HTTPS too, with the same config syntax.
The Nginx + Certbot Workflow, Done Properly
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.
Head-to-Head: the Differences That Matter in Production
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.
When I Reach for Caddy
- Small VPSs hosting a handful of apps behind a reverse proxy, where the two-line Caddyfile replaces fifty lines of Nginx server blocks plus certbot state.
- Internal tools and staging environments, where automatic local CA certificates mean HTTPS everywhere without the mkcert dance.
- Projects where someone less infrastructure-minded will maintain the server later. Handing a junior developer a Caddyfile is an order of magnitude kinder than handing them nginx.conf plus a renewal timer to keep alive.
- Anything with many short-lived subdomains, since on-demand certificate issuance handles hosts you did not know about at deploy time.
When Nginx Still Wins
- Existing fleets. All my production Docker Swarm ingress and the Ansible roles around it assume Nginx; the migration cost outweighs Caddy's convenience there.
- High-traffic workloads where you want explicit control over ssl_session_cache, keepalive behavior, worker processes, and buffer sizes that the Nginx docs spell out precisely.
- Setups needing specific modules or patterns with deep prior art: njs scripting, complex rate limiting, mirroring traffic, or the exact caching semantics your CDN-less app relies on.
- Teams where Nginx fluency already exists. A config language everyone reads beats a better config language only one person reads.
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.
Migrating One Server from Nginx to Caddy: a Safe Sequence
If you want to try Caddy on a live box without burning the bridge, this is the sequence I used:
- Write the Caddyfile equivalent of your server blocks and run Caddy on alternate ports (8080/8443) alongside Nginx to validate routing and headers.
- Lower your DNS TTL, then stop Nginx and start Caddy on 80/443 during a quiet window. First issuance takes seconds per domain.
- Watch the Caddy logs for ACME activity and hit every vhost. Keep your old certificates and nginx.conf untouched on disk as the rollback path.
- After a clean week, remove certbot, its timer, and the Nginx package — leftover renewal timers pointing at dead webroots love to fill logs with noise.
The Takeaway
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

