Run Caddy unless you have a specific reason not to. A complete site definition is two lines, HTTPS is on by default, and one binary replaces the entire certbot-cron-reload contraption. Traefik earns its extra complexity once you run fifteen-plus containers and want routing declared in Docker labels next to each service. Nginx is the right pick only if you already speak it fluently or need its module ecosystem — its raw-performance edge is real, and irrelevant at homelab traffic levels.

The reverse proxy decision in one table

Caddy 2Traefik v3Nginx / NPM
Config for one service2–3 lines~5 Docker labels~25 lines, or GUI clicks in NPM
Automatic TLSDefault, zero configBuilt in, one resolver blockExternal (certbot); NPM builds it in
Docker awarenessVia caddy-docker-proxy pluginNative, label-drivenNone; NPM adds a GUI and database
Idle RAM~40MB~70MB~5MB nginx alone, ~120MB NPM stack
Config reloadcaddy reload, zero downtimeAutomatic on container eventsnginx -s reload
Wildcard certsDNS plugin per providerBuilt-in DNS challenge, 100+ providerscertbot/acme.sh plugins

Caddy: TLS you stop thinking about

A complete Caddyfile for two services:

# /etc/caddy/Caddyfile
photos.example.com {
	reverse_proxy 127.0.0.1:2283
}
media.example.com {
	reverse_proxy 127.0.0.1:8096
}

That's everything — certificate issuance, renewal, HTTP-to-HTTPS redirect, OCSP stapling, modern cipher defaults. Automatic HTTPS isn't a feature you enable; it's the absence of a system you'd otherwise maintain. For LAN-only hostnames, tls internal issues from Caddy's own CA. The one genuine annoyance: wildcard certificates on a network that never opens port 80 need a DNS-provider plugin, which means a custom build via xcaddy or the download page's build picker. The DNS-01 pattern, including why it beats HTTP-01 for internal hosts, is covered in Let's Encrypt done right.

Traefik: routing lives with the container

Traefik watches Docker events and assembles its routing table from labels, so each service carries its own proxy config:

services:
  immich-server:
    image: ghcr.io/immich-app/immich-server:release
    labels:
      - traefik.enable=true
      - traefik.http.routers.immich.rule=Host(`photos.example.com`)
      - traefik.http.services.immich.loadbalancer.server.port=2283

Deploy the stack and the route exists; remove the stack and the route is gone. Nothing central drifts out of sync with what's actually running, and that self-cleaning property is the whole argument for Traefik. The cost is front-loaded: the static-versus-dynamic configuration split, middleware chains, and documentation that quietly assumes you have a Kubernetes-shaped problem make the first weekend rough. Budget a full day to get the dashboard, the ACME resolver, and one service working; every service after that is five minutes of copy-paste labels.

Nginx is for people who already know nginx

Plain nginx is still the most capable of the three and the lightest on memory, and every edge case you will ever hit has a decade-old answer online. But certificates remain your job — certbot on a systemd timer, deploy hooks to reload, and the occasional silently failed renewal you discover from a browser warning. A vhost Caddy expresses in 3 lines runs 20–30 in nginx conf, and none of it is generated for you. If you administer nginx at work, use it at home too; consistency beats novelty. I wouldn't learn it from scratch for a homelab in 2026.

Nginx Proxy Manager wraps nginx in a web GUI with Let's Encrypt built in and is the fastest zero-to-HTTPS path for someone who doesn't want to touch a config file. The ceiling arrives quickly: anything past host-and-forward means pasting raw nginx into the "Advanced" custom config box, at which point you're writing nginx anyway, minus version control. It's a fine on-ramp. Most people outgrow it inside a year, and the migration off it is manual because your config lives in a database, not files.

Performance is a tie you can ignore

All three saturate a gigabit link on N100-class hardware and add roughly a millisecond of proxy latency. Benchmarks showing nginx ahead at 100,000 requests per second are accurate and describe a load your homelab will never generate — a busy Jellyfin night is a few dozen requests per second plus one big sequential stream. TLS termination overhead is likewise noise on anything with AES-NI, which is every x86 CPU made since 2011. Choose on operations, not throughput.

Migration is cheaper than it feels

Everything flows through the proxy, so swapping it feels like heart surgery. The mechanics are mild:

  1. Translate vhosts first. Nginx-to-Caddy typically shrinks 10:1; keep the old config open and port one hostname at a time.
  2. Run the new proxy on 8080/8443 alongside the old one and test every hostname with curl --resolve before anything moves.
  3. Swap the port bindings (or the DNAT rules) in one change, off-peak. Total downtime is seconds.
  4. Reissue certificates instead of copying them. Let's Encrypt allows 50 new certificates per registered domain per week — a homelab never gets close.

The only genuinely fiddly translation is Traefik middleware chains to Caddy directives (auth headers, rate limits, path strips). Map those one by one and test each behind a temporary hostname.

What I'd do

New homelab: Caddy with a plain Caddyfile, one block per service, the file in git. If your compose stacks number in the dozens, you rebuild services weekly, or you want the proxy to configure itself, take Traefik and pay the day of setup — it amortises. Keep nginx wherever it already works; replacing a functioning proxy for aesthetics is negative-value work. And if a GUI is the difference between shipping HTTPS this weekend and never, Nginx Proxy Manager today beats the perfect setup you'll never finish — just know you'll probably graduate to Caddy later, and that's fine.