Skip to main content
BlogDevOps

Caddy vs Nginx for Automatic HTTPS: When Each One Wins

Caddy gives you automatic HTTPS in two lines; Nginx plus certbot gives you control and raw ecosystem depth. Here is how I decide between them per VPS.

Matthews Wong

Software Engineer

April 20269 min read
Caddy vs Nginx for Automatic HTTPS: When Each One Wins

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.

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 certbot

Certbot 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:

AspectCaddyNginx + certbot
HTTPS setup effortZero beyond naming the host. Redirects, issuance, and renewal are defaults.Install certbot, run it per domain, verify the timer. Scriptable but real work.
Renewal modelIn-process, continuous, no external scheduler to audit.systemd timer or cron outside the server; must be monitored separately.
CA failoverAutomatic fallback from Let's Encrypt to ZeroSSL, then retry with backoff.Single CA unless you script alternatives yourself.
Ecosystem and docsSmaller community; fewer copy-paste answers for exotic setups.Twenty years of modules, tutorials, and Stack Overflow coverage for nearly everything.
Low-level tuningSensible 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:

  1. Write the Caddyfile equivalent of your server blocks and run Caddy on alternate ports (8080/8443) alongside Nginx to validate routing and headers.
  2. Lower your DNS TTL, then stop Nginx and start Caddy on 80/443 during a quiet window. First issuance takes seconds per domain.
  3. 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.
  4. 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.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

Why does Caddy handle HTTPS automatically while Nginx requires certbot?

Caddy has been designed to serve all sites over HTTPS by default since 2015 — certificate issuance and renewal are managed inside the server process itself, triggered simply by declaring a hostname in the Caddyfile. Nginx does not implement ACME natively, so the standard approach requires installing certbot separately, which adds a systemd timer or cron job that lives outside the web server and can fail independently of it.

What happens if Let's Encrypt has an outage when Caddy needs to renew a certificate?

Caddy automatically retries against ZeroSSL as a fallback before backing off, giving you built-in CA redundancy without any extra configuration. With certbot, a CA outage during your renewal window is a problem you have to notice and handle yourself, since certbot is configured against a single CA by default.

When should I still choose Nginx over Caddy?

Nginx remains the better choice for existing fleets already standardized on it with Ansible or Docker Swarm ingress, because the migration cost outweighs Caddy's convenience. It also wins when you need fine-grained low-level tuning — explicit control over ssl_session_cache, worker processes, and buffer sizes — or when your team already has deep Nginx fluency, since a config language everyone reads beats a better one only one person reads.

How should I monitor certificate expiry regardless of which tool I use?

The post recommends exporting certificate expiry metrics into Prometheus and setting an alert at 21 days remaining. This threshold has proven useful in practice: it caught both a broken certbot timer and a Caddy instance whose storage volume had filled up and could not persist a renewed certificate.

What is the safest way to migrate a live server from Nginx to Caddy?

Run Caddy on alternate ports (8080/8443) alongside Nginx first to validate routing and headers, then lower your DNS TTL before switching Caddy to ports 80/443 during a quiet window. Keep the old certificates and nginx.conf on disk as a rollback path, and only remove certbot, its timer, and the Nginx package after a clean week of operation.