Caddy vs Nginx for a Home Lab Reverse Proxy
Disclosure: this post contains affiliate links (PartnerStack). If you buy through them I may earn a commission, at no extra cost to you.
Why this matters
Every self-hosted setup ends up with one box in front of everything: the reverse proxy. It terminates TLS, maps hostnames to containers, and is the single component whose failure takes down every service at once. Picking it is a five-minute decision you then live with for years.
I ran Caddy and Nginx side by side for three months, serving the same six applications behind the same domain, and kept notes on which one made me open a terminal on a Sunday.
What each one actually is
Nginx is a C web server and proxy from 2004. It is in every distro, runs on a large share of the public internet, and its configuration language is effectively a dialect of its own. Nothing about a home lab stresses it; the reason to pick it is that every answer you will ever need already exists somewhere.
Caddy is a Go server from 2015 built around one idea: TLS should not be a task. It obtains and renews certificates through ACME by default, upgrades WebSockets automatically, and its config file is short enough to read in one screen.
The comparison, without the marketing
| Caddy | Nginx | |
|---|---|---|
| Config for 5 services | ~15 lines, one Caddyfile | ~60 lines across sites-available |
| Certificate renewal | automatic, invisible | certbot + timer you must monitor |
| WebSockets | automatic upgrade | manual headers per location |
| HTTP/3 | on by default | needs a build with QUIC support |
| Rate limiting | not built in (plugin) | limit_req is first-class |
| Memory at idle | ~30 MB | ~10 MB |
| Streaming (RTMP/HLS) | no | yes, mature module |
| Docs and answers online | good, younger | enormous |
| Failure mode | config parse error on reload, old config keeps running | silent 502s when an upstream moves |
Two rows matter more than the rest: certificate renewal and WebSocket headers. Both are things you configure once and then forget, which is exactly the category of thing that fails quietly six months later.
The same job, both ways
Here is one hostname proxied to a container, in each dialect.
Caddyfile
media.example.com {
reverse_proxy 127.0.0.1:8096
}
notes.example.com {
reverse_proxy 127.0.0.1:3001
}
That is the whole file. Both sites get certificates, HTTP-to-HTTPS redirects, and WebSocket upgrades. Add a service by adding four lines.
Nginx
server {
listen 80;
server_name media.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name media.example.com;
ssl_certificate /etc/letsencrypt/live/media.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/media.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8096;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
}
Then a certbot run, then a renewal timer, then a reload hook. None of it is hard. All of it is maintenance you own.
How I tested
- Host: Debian 13 on the same mini PC, both proxies in Docker with host networking
- Apps behind them: Jellyfin, Vaultwarden, Gitea, Miniflux, Uptime Kuma, a static site
- Traffic: real use, not a benchmark — two people, a few thousand requests a day
- Duration: three months, switching the active proxy weekly
I did not measure requests per second. At this scale both are limited by the applications, not the proxy, and any benchmark I published would describe my hardware rather than your decision.
Where Caddy genuinely wins
Certificate handling. Not the initial issuance — both are fine there — but the renewal that quietly stops working because a DNS record changed or port 80 got closed by an ISP. Caddy fails loudly in its log and retries; a certbot timer fails into a log nobody reads until the browser shows a warning.
Configuration you can hold in your head. Five services, fifteen lines, no include files. When you come back in eight months, you can still edit it.
Sane defaults. Compression, HTTP/2 and HTTP/3, WebSocket upgrade, and X-Forwarded-* headers are already correct. Nginx requires knowing which headers to forward, and the classic bug — an app generating http:// links behind a TLS proxy — comes from forgetting X-Forwarded-Proto.
Where Nginx genuinely wins
Rate limiting and request filtering. limit_req_zone is built in and well documented. Caddy needs a third-party module, which means a custom build. If you expose anything to the internet and want brute-force protection at the proxy layer, this is a real advantage.
Media streaming. If you serve HLS or run an RTMP ingest, Nginx has the mature module. Caddy does not.
Complex rewrites and legacy apps. The rewrite, try_files and map directives handle pathological routing that would need Caddy plugins.
The answer already exists. Whatever error you hit, someone has posted it. That has real value at 1am, and it is the honest reason many people stay.
The mistakes I made, so you don’t have to
Caddy could not bind to privileged ports. Running it as a non-root user needs setcap cap_net_bind_service=+ep /usr/bin/caddy, or run it as root, or listen on 8080 behind something else. I hit this and spent twenty minutes convinced DNS was broken.
Nginx silently served the wrong certificate after I renamed a server block. nginx -t passed. The browser warning was the only signal. Caddy refuses to start on a config error instead of half-working, which is a better default.
Both broke on the same thing: an upstream container that moved to a different port. Caddy returned 502 with a clear log line; Nginx returned 502 and I had to check error.log. Equivalent outcome, different amount of swearing.
Best for / Skip if
Caddy is best for anyone who wants one file, automatic certificates, and no renewal cron to babysit. If you run fewer than twenty hostnames and do not need rate limiting at the proxy, it is the lower-maintenance choice.
Skip Caddy if you need streaming modules, per-IP rate limits without rebuilding, or you are more comfortable debugging Nginx because you have done it for a decade. Familiarity is a legitimate reason.
Nginx is best for people with existing Nginx muscle memory, streaming workloads, or a need for request-level filtering at the edge.
Skip Nginx if you are new to both and value not thinking about certificates.
Whatever you pick, this decision touches the config and the containers it fronts, nothing else. The Caddy install walkthrough shows the full file for six services; the Docker Compose layout is the structure those services live in. Neither pick changes the rest of your stack, which is why this is worth an afternoon of thinking rather than a week of migration.
Alternatives
- Traefik — routes by Docker labels, which is elegant if everything is in containers and annoying if it is not. Automatic ACME like Caddy.
- HAProxy — the serious load balancer. Overkill for six services, excellent if you need health-checked backends.
- A Cloudflare Tunnel — removes the reverse proxy from your box entirely, at the cost of depending on a third party for ingress.
Verdict
I run Caddy. Not because it is faster — it is not meaningfully faster — but because the config is fifteen lines I can still read, and because I have never once thought about certificate renewal since installing it. On a home lab, the component you think about least is the one that is working.
If you need rate limiting or stream media, install Nginx and accept the maintenance. Both are correct answers; only one of them fits how much you enjoy editing config files.
References
- Caddy documentation — config structure and automatic HTTPS
- nginx documentation — core modules and proxy configuration
Tools used in this post
FAQ
Does Caddy really get HTTPS working with no extra setup?
Yes, as long as ports 80 and 443 reach the internet and DNS points at the host. It obtains and renews certificates through ACME on its own. What it cannot do is get a certificate for a name that does not resolve, so DNS has to be right first.
Is Nginx slower than Caddy for a home lab?
No, and anyone claiming a meaningful difference at home-lab traffic levels is measuring their own benchmark, not yours. Both handle a few hundred requests per second on a mini PC without breaking a sweat. Choose on configuration and maintenance, not throughput.
Can I run both at the same time?
You can, but only one of them can own ports 80 and 443. The usual pattern is Nginx on the host and Caddy inside Docker for a single service, which is more moving parts than most home labs need.
Which one handles WebSockets without extra config?
Caddy does. Nginx needs proxy_http_version 1.1 plus explicit Upgrade and Connection headers on every location block that carries a WebSocket. Forget one and the app silently falls back to polling.