pickuma.
Infrastructure

Caddy Web Server Review: Automatic HTTPS Without the Ceremony

A detailed look at Caddy's automatic TLS, Caddyfile syntax, and reverse proxy setup — and where it falls short compared to Nginx.

7 min read

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.

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.

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.

FAQ

Does Caddy work if my server is behind a load balancer that terminates TLS? +
Yes, but automatic certificate provisioning only works when Caddy is the TLS termination point. If your load balancer handles TLS, Caddy receives plain HTTP and won't attempt to obtain certificates. You can still use Caddy as a reverse proxy in that topology — just don't expect automatic HTTPS to activate.
Can I use Caddy with DNS providers like Cloudflare or Route 53? +
Yes. Caddy has community-maintained plugins for most major DNS providers including Cloudflare, Route 53, Google Cloud DNS, and others. These plugins enable the DNS-01 ACME challenge, which is required for wildcard certificates and works without open inbound ports. You build a custom binary with xcaddy to include the provider plugin.
What happens to my certificates if I stop the Caddy process? +
Certificates are stored on disk and persist across restarts. Caddy will load them on startup and resume managing renewal. The risk is if your storage location isn't persistent — in containers, make sure the certificate directory is mounted from a volume, or Caddy will re-request certificates on each restart and can hit ACME rate limits.

Related tools

Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.

Related reading

See all Infrastructure articles →

Get the best tools, weekly

One email every Friday. No spam, unsubscribe anytime.