If you've ever spent an afternoon wrestling with Certbot cron jobs, nginx reload scripts, and ACME challenge directories, you already understand the problem Caddy is solving. The pitch is simple: point it at a domain, and it handles certificate provisioning, renewal, OCSP stapling, and HTTP-to-HTTPS redirection with zero additional configuration. No Certbot. No systemd timers. No manual reloads after renewal.
Caddy (v2.11.3 as of May 2026, Apache 2.0 licensed) is written in Go and has accumulated over 72,000 stars on GitHub. That's not a niche tool. The question isn't whether Caddy works — it clearly does — but whether its design tradeoffs make sense for your specific deployment context.
How Automatic HTTPS Actually Works
Caddy's automatic HTTPS is powered by a built-in ACME client. When you start Caddy with a public domain name in your config, it reaches out to Let's Encrypt or ZeroSSL, completes an ACME challenge, stores the certificate, and starts serving HTTPS. Renewal happens in the background before expiry without a restart.
Three ACME challenge methods are available:
- HTTP-01: Caddy serves a challenge file on port 80. Straightforward, but requires port 80 to be reachable from the internet.
- TLS-ALPN-01: The challenge runs over port 443 using a special TLS handshake. No port 80 dependency, but port 443 must be open.
- DNS-01: Caddy writes a TXT record to your DNS zone. This is the only method that works for wildcard certificates, and it requires DNS provider credentials (Caddy has plugins for most major providers). Neither port needs to be open, which makes it viable behind a firewall.
Both HTTP-01 and TLS-ALPN-01 are enabled by default. DNS-01 requires explicit configuration and a provider plugin.
For internal services — localhost, private IPs, or .local names — Caddy spins up its own local CA and issues self-signed certificates, then tries to install that CA into your system's trust store automatically. This works well on Linux and macOS. Whether it will work in your CI containers or Docker images depends on your setup, and you may need to handle trust store installation manually.
When testing ACME integrations, configure Caddy to use Let's Encrypt's staging endpoint. The production endpoint has rate limits (roughly 10 failed authorization attempts per account per 10 seconds, plus weekly issuance limits per domain). Hitting those limits during a botched deployment will lock you out of cert issuance for hours.
Caddy also supports on-demand TLS: certificates are obtained during the first TLS handshake for a domain, rather than at startup. This is useful if you're proxying thousands of customer subdomains and don't know them all at configuration time — think multi-tenant SaaS platforms. The catch is that on-demand TLS must be paired with an "ask" endpoint, a URL Caddy calls to verify that a given domain is authorized before it requests a certificate. Without this restriction, a misconfigured server could be tricked into requesting certificates for arbitrary domains, burning through ACME rate limits or triggering abuse detection. The documentation is explicit about this requirement.
The Caddyfile: What Simple Configuration Looks Like in Practice
The Caddyfile format is Caddy's user-facing configuration language. Here's what a production-ish setup for a Node.js API and a static frontend looks like:
# Static frontend
app.example.com {
root /var/www/frontend
encode gzip
try_files {path} /index.html
file_server
}
# API reverse proxy
api.example.com {
reverse_proxy localhost:3000
}
That's it. No server {} blocks, no listen directives, no SSL certificate paths. Caddy reads the domain names, recognizes they're public hostnames, and handles the rest. HTTP-to-HTTPS redirects are automatic — you don't write them.
Compare that to a typical Nginx config that does the same thing: two server blocks for HTTP and HTTPS per domain, a Certbot configuration, a cron entry for renewal, and a post-renewal hook to reload nginx. The operational surface is genuinely smaller with Caddy.
For path-based routing to multiple backends:
example.com {
reverse_proxy /api/* localhost:5000
root /srv/public
file_server
}
Caddy evaluates directives in a defined order, so the reverse_proxy matcher intercepts /api/* requests before file_server sees them.
The Caddyfile isn't the only configuration interface. Caddy also exposes a JSON API on port 2019 (by default), which accepts configuration changes without a restart. This matters if you're building tooling around Caddy or need programmatic config updates — CI pipelines, orchestrators, or custom control planes can push updates via HTTP rather than templating config files. The JSON config is more verbose than the Caddyfile but is fully documented and can be reloaded live.
Where Caddy Falls Short
Caddy's defaults are reasonable, but the ecosystem of third-party modules is narrower than Nginx's. If your architecture depends on specific Nginx modules — video streaming modules, LDAP authentication, or certain WAF integrations — you'll need to verify that a Caddy equivalent exists. Caddy's module system allows compiling custom binaries with additional plugins, but that adds a build step and complicates upgrade paths. The official xcaddy tool manages this, but it's another thing to maintain.
On raw throughput, Nginx still holds an advantage for large-file streaming. One independent benchmark (Tyblog, not sponsored by either project) showed Caddy slightly ahead for small-file workloads while Nginx retained an edge for large static assets. For typical API proxying or serving web apps, the difference is unlikely to matter — but if you're running a high-traffic CDN origin or large media server, test your specific workload rather than assuming parity.
Caddy's CVE history is shorter than Nginx's — the Go runtime eliminates an entire class of memory-safety bugs by construction — but "fewer historical CVEs" isn't the same as "more secure." Evaluate based on your threat model and your team's familiarity with Go-based operational tooling.
Certificate storage is another consideration. Caddy writes certificates to the local filesystem (defaulting to the user's home directory) or to a configured storage backend. If you run Caddy in a container or ephemeral VM, you need persistent storage mounted and writable, or certificates will be re-requested on every restart — which will eventually hit ACME rate limits. Multiple Caddy instances pointed at the same storage backend will coordinate automatically, which simplifies horizontal scaling, but the storage backend itself becomes a dependency to manage.
Rate limits from ACME providers are enforced per account, not per server. If you run multiple Caddy instances without shared storage, each instance requests its own certificate for each domain. Exceed the issuance limits and you lose the ability to provision new certs for up to a week. Always configure a shared storage backend before going multi-instance.
When to Choose Caddy Over Nginx
The honest answer is: Caddy is a better default for new deployments where TLS management complexity is a friction point and you don't have deep Nginx expertise already. If your team knows Nginx, has existing configurations, and has working Certbot automation, migrating to Caddy has a real cost in exchange for a benefit that may be marginal in your case.
Caddy earns its place for:
- Solo developers and small teams who want HTTPS without maintaining certificate renewal infrastructure.
- Internal tooling where you want TLS on private services without manually managing a CA.
- Multi-tenant platforms where on-demand TLS handles dynamic customer domains.
- Docker-heavy setups where Caddy's single binary and JSON API fit cleanly into a container orchestration model.
Nginx remains the better choice when you need specific ecosystem modules, are running extremely high-volume static file serving, or have a team with deep existing Nginx operational knowledge. Both servers are production-grade. The choice is about operational fit, not technical merit.
Originally published at pickuma.com. Subscribe to the RSS or follow @pickuma.bsky.social for new reviews.
Top comments (0)