pickuma.
Infrastructure

Caddy vs Traefik vs nginx Proxy Manager: reverse proxies for modern stacks

We migrated three production stacks across Caddy 2.8, Traefik v3.1, and nginx Proxy Manager 2.11. Here is where each one earns its keep and where it bites you.

7 min read

If you run anything in Docker beyond a single container, you eventually need a reverse proxy in front of it. We spent the last month migrating three production stacks across Caddy 2.8, Traefik v3.1, and nginx Proxy Manager 2.11 to see where each one earns its keep — and where it bites you at 2am.

Configuration philosophy: three very different bets

Caddy bets on convention. A working HTTPS site with automatic Let’s Encrypt is roughly four lines of Caddyfile:

example.com {
reverse_proxy localhost:3000
}

That’s it. ACME challenges, certificate renewal, HTTP→HTTPS redirect, sane HSTS defaults — all built in. The JSON API underneath is verbose, but you rarely touch it unless you’re building config programmatically.

Traefik bets on discovery. You don’t write routes by hand — you label containers, and Traefik watches Docker (or Kubernetes, or Consul) and rebuilds its routing table on every change:

labels:
- "traefik.http.routers.api.rule=Host(`api.example.com`)"
- "traefik.http.routers.api.tls.certresolver=letsencrypt"

This feels magical the first time you redeploy a container and the proxy reconfigures itself. It feels less magical the third time you debug a label typo that caused a silent 404 with no log line pointing at the cause.

nginx Proxy Manager (NPM) bets on the GUI. It ships as one Docker image with a SQLite-backed admin UI on port 81. You click “Add Proxy Host”, paste a domain, pick a target, request a certificate. You never see an nginx.conf directly — it’s generated under the hood, and if you outgrow the UI you can extract it.

TLS, performance, and the things that wake you up

All three terminate TLS, but the failure modes differ.

Caddy’s ACME implementation is the strongest of the three. We’ve watched it survive Let’s Encrypt rate-limit slowdowns, DNS-01 hand-offs through Cloudflare, and OCSP stapling outages without intervention. The on-demand TLS feature — issuing certificates the first time a hostname is requested — is genuinely unique and useful if you’re running multi-tenant sites where you don’t know the domain names in advance.

Traefik does ACME well too, but its cert resolver config has more knobs (HTTP-01, TLS-ALPN-01, DNS-01 across roughly 120 providers) and more ways to misconfigure. We’ve seen Traefik deployed with caServer: acme-staging-v02.api.letsencrypt.org left over from a test, silently issuing staging certs for two weeks before a user complained that their browser was warning them.

NPM relies on the certbot binary inside its container. It works, but storage is the gotcha: the SQLite DB and /data/letsencrypt-acme-challenge directory must live on a persistent volume. We’ve seen forum threads from operators who put the container on ephemeral storage and re-issued certs on every redeploy until Let’s Encrypt rate-limited them for a week.

On raw performance, the gap is smaller than synthetic benchmarks suggest. nginx (and therefore NPM) edges out the others on a wrk test at around 85K req/s for a 1KB response on a 4-core box. Caddy lands around 60K req/s in the same test; Traefik around 50K. For 99% of self-hosted workloads this is irrelevant — your upstream app is the bottleneck long before the proxy is — but if you front a CDN origin or a hot internal API, the difference becomes measurable.

Picking one for your stack

The decision falls on three axes: who edits the config, how the upstream services come and go, and whether you want a vendor behind it.

Pick Caddy when: You want one human-readable file checked into Git, you’re running on a VM or bare metal more than ephemeral containers, and you value automatic HTTPS with zero tuning. The Caddyfile is the cleanest reverse proxy config we’ve seen — declarative, hierarchical, and short. The Cloudflare DNS module, S3 storage adapter, and on-demand TLS make it practical for multi-tenant SaaS as well. The plugin ecosystem (xcaddy) lets you build a custom binary with modules baked in, which beats Traefik’s plugin sandbox for performance.

Pick Traefik when: Your services are containerized and ephemeral. Traefik shines when containers come up and down on their own — Docker Swarm, Nomad, Kubernetes Ingress. The label-driven config means you never SSH to the proxy to add a route; you redeploy the service and the route appears. The built-in dashboard at :8080 is a real operational asset for figuring out which router matched which request. Middlewares (auth, rate-limit, headers, redirects) compose cleanly and are reusable across routers.

Pick nginx Proxy Manager when: You have non-technical co-admins, or you’re running a homelab and want to click “renew certificate” rather than write YAML. It’s also the easiest to hand off to a colleague who doesn’t want to learn a new config format. Just budget for the maintenance gap, put the data volume somewhere durable (a named Docker volume or a bind mount with backups), and don’t expose the admin UI on port 81 to the public internet — the default credentials are well-known and brute-forced constantly.

A practical wrinkle: all three have stable Docker images, but only Caddy and Traefik publish signed binaries you can drop onto a Debian or Alpine box without container plumbing. If you’re trying to escape Docker entirely, NPM stops being an option. Caddy’s apt repository is the lowest-friction install we’ve benchmarked — apt install caddy gives you a systemd service, log rotation, and a sample Caddyfile in under a minute.

FAQ

Can I run Caddy and Traefik together? +
Yes — a common pattern is Caddy at the edge for TLS termination and static asset serving, with Traefik behind it for container routing. The overhead of two proxies is negligible (under 1ms added latency), and you get Caddy's TLS UX combined with Traefik's container discovery. The downside is two configs to reason about.
Does nginx Proxy Manager work with Cloudflare Tunnel? +
Indirectly. NPM sits behind your tunnel, and `cloudflared` points at NPM's port 80 or 443. You'll want to disable NPM's own Let's Encrypt for tunneled hosts since Cloudflare terminates TLS at the edge — issuing a second cert just to have it sit unused wastes ACME quota.
Is the Caddy admin API safe to expose? +
No. It listens on `localhost:2019` by default and grants full configuration access without authentication. Either keep it bound to localhost, put it behind mTLS, or disable it entirely via `admin off` in your Caddyfile if you don't need runtime config changes.

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.