Homelab Reverse Proxy Showdown: Nginx Proxy Manager vs Traefik vs Caddy vs HAProxy
Every homelab eventually hits the same problem: you have a dozen services running on different ports, and remembering 192.168.1.50:8096 for Jellyfin and 192.168.1.50:3000 for Gitea gets old fast. A reverse proxy sits in front of all your services, routes traffic by domain name, and handles SSL certificates automatically. Instead of ports, you get clean URLs like jellyfin.home.lab and gitea.home.lab.
But which reverse proxy should you actually use? The homelab community has strong opinions, and for good reason — these tools have genuinely different philosophies. This guide compares the four most popular options based on real-world homelab use, not enterprise benchmarks that don't matter when you have 20 users (and 18 of them are you on different devices).

Quick Comparison
| Feature | Nginx Proxy Manager | Traefik | Caddy | HAProxy |
|---|---|---|---|---|
| Configuration | Web GUI | Labels/files | Caddyfile/API | Config file |
| SSL automation | Built-in (Let's Encrypt) | Built-in (Let's Encrypt + others) | Built-in (Let's Encrypt, ZeroSSL) | Manual or external |
| Docker integration | Manual per-host | Automatic via labels | Plugin or manual | Manual |
| Learning curve | Very low | Medium-high | Low | High |
| Dashboard | Full management GUI | Read-only dashboard | None (API only) | Stats page |
| Middleware/plugins | Limited | Extensive | Extensive | Extensive (ACLs) |
| Performance | Good | Good | Good | Excellent |
| Config format | GUI + SQLite | YAML/TOML + Docker labels | Caddyfile or JSON | Custom syntax |
| Wildcard certs | Yes (DNS challenge) | Yes (DNS challenge) | Yes (DNS challenge) | N/A |
| Community size | Large (homelab-focused) | Very large | Growing | Very large (enterprise) |
Nginx Proxy Manager
Nginx Proxy Manager (NPM) wraps Nginx in a web interface that makes reverse proxy configuration almost trivially easy. You click "Add Proxy Host," fill in the domain and upstream address, toggle SSL, and you're done. No config files, no YAML, no learning Nginx syntax.
Deployment
services:
nginx-proxy-manager:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81" # Admin panel
volumes:
- ./npm-data:/data
- ./npm-letsencrypt:/etc/letsencrypt
networks:
- proxy
networks:
proxy:
name: proxy
driver: bridge
Default login is [email protected] / changeme. Change it immediately.
Adding a Service
- Navigate to
http://your-server:81 - Click Proxy Hosts > Add Proxy Host
- Enter your domain (e.g.,
jellyfin.home.lab) - Set the forward hostname to the container name or IP
- Set the forward port (e.g.,
8096) - Under the SSL tab, request a new certificate
- Toggle "Force SSL" and "HTTP/2 Support"
That's it. The entire process takes about 30 seconds per service.
Custom Nginx Configuration
When you need something the GUI doesn't expose, NPM lets you inject custom Nginx directives in the "Advanced" tab of each proxy host:
# Increase upload size for Nextcloud
client_max_body_size 10G;
# WebSocket support (some services need this explicitly)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
Strengths and Weaknesses
Strengths:
- Lowest barrier to entry of any option
- Visual management means less chance of typos breaking your config
- Built-in access lists, redirections, and 404 hosts
- Stream proxying for TCP/UDP (useful for game servers)
Weaknesses:
- No automatic Docker service discovery — you add each host manually
- Limited middleware options compared to Traefik or Caddy
- The GUI can become tedious when managing 30+ services
- Config lives in SQLite, not in version-controllable files
Traefik
Traefik takes a fundamentally different approach: it watches your Docker daemon and automatically configures routes based on labels you add to your containers. Add a new service with the right labels, and Traefik picks it up immediately — no manual steps, no clicking through a GUI.
Deployment
services:
traefik:
image: traefik:v3.2
container_name: traefik
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "8080:8080" # Dashboard (disable in production)
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml
- ./acme.json:/acme.json
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.home.lab`)"
- "traefik.http.routers.dashboard.service=api@internal"
networks:
proxy:
name: proxy
driver: bridge
The static configuration file:
# traefik.yml
api:
dashboard: true
insecure: true # Only for local access — disable if exposed
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: proxy
certificatesResolvers:
letsencrypt:
acme:
email: [email protected]
storage: /acme.json
httpChallenge:
entryPoint: web
Adding a Service with Labels
Instead of configuring the proxy, you configure your services to announce themselves:
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
restart: unless-stopped
volumes:
- ./jellyfin-config:/config
- /mnt/media:/media:ro
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellyfin.rule=Host(`jellyfin.home.lab`)"
- "traefik.http.routers.jellyfin.entrypoints=websecure"
- "traefik.http.routers.jellyfin.tls.certresolver=letsencrypt"
- "traefik.http.services.jellyfin.loadbalancer.server.port=8096"
Deploy the container, and Traefik automatically creates the route. Remove the container, and the route disappears. This is infrastructure as code at its best.
Middleware Chains
Traefik's middleware system is powerful. You can chain authentication, rate limiting, headers, and more:
labels:
# Add authentication via Authelia/Authentik
- "traefik.http.routers.myapp.middlewares=authelia@docker"
# Rate limiting
- "traefik.http.middlewares.rate-limit.ratelimit.average=100"
- "traefik.http.middlewares.rate-limit.ratelimit.burst=50"
# Security headers
- "traefik.http.middlewares.secure-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.secure-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.secure-headers.headers.browserXssFilter=true"
Strengths and Weaknesses
Strengths:
- Automatic service discovery — add labels, done
- Configuration lives with your docker-compose files (version controllable)
- Rich middleware ecosystem (auth forwarding, IP whitelisting, circuit breakers)
- Excellent for dynamic environments where services come and go
Weaknesses:
- Docker socket access is a security concern (mitigate with socket proxy)
- Label syntax is verbose and error-prone — one typo and nothing works, with cryptic errors
- The learning curve is real — Traefik's concepts (routers, services, middlewares, providers) take time to internalize
- Debugging is harder than "look at the nginx config"
Caddy
Caddy's pitch is automatic HTTPS with minimal configuration. Its Caddyfile format is refreshingly simple — often just two or three lines per service. Caddy handles certificate issuance, renewal, OCSP stapling, and HTTP-to-HTTPS redirection out of the box without any configuration.
Deployment
services:
caddy:
image: caddy:2-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./caddy-data:/data
- ./caddy-config:/config
networks:
- proxy
networks:
proxy:
name: proxy
driver: bridge
The Caddyfile
This is where Caddy shines. Here's a complete reverse proxy configuration:
jellyfin.home.lab {
reverse_proxy jellyfin:8096
}
gitea.home.lab {
reverse_proxy gitea:3000
}
nextcloud.home.lab {
reverse_proxy nextcloud:80
# Increase upload limit
request_body {
max_size 10GB
}
}
# Wildcard for internal services with local TLS
*.internal.home.lab {
tls internal
@grafana host grafana.internal.home.lab
handle @grafana {
reverse_proxy grafana:3000
}
@prometheus host prometheus.internal.home.lab
handle @prometheus {
reverse_proxy prometheus:9090
}
}
Compare that to the equivalent Traefik label configuration or Nginx config blocks. Caddy's readability is a genuine advantage when you come back to your config six months later.
Docker Integration with caddy-docker-proxy
While Caddy doesn't have built-in Docker discovery, the caddy-docker-proxy plugin adds Traefik-style label-based configuration:
services:
caddy:
image: lucaslorentz/caddy-docker-proxy:ci-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./caddy-data:/data
networks:
- proxy
whoami:
image: traefik/whoami
networks:
- proxy
labels:
caddy: whoami.home.lab
caddy.reverse_proxy: "{{upstreams 80}}"
Strengths and Weaknesses
Strengths:
- Simplest configuration syntax of any option
- Automatic HTTPS works out of the box — no certresolver configuration needed
- Strong plugin ecosystem (Cloudflare DNS, Docker proxy, auth, etc.)
- Excellent documentation
- Written in Go with no external dependencies — single binary
Weaknesses:
- No built-in management GUI
- Docker integration requires a third-party plugin
- Smaller community than Nginx or Traefik (though growing fast)
- Some advanced load balancing features require more verbose configuration
HAProxy
HAProxy is the heavyweight champion of load balancing and proxying. It powers some of the highest-traffic sites on the internet. In a homelab context, it's overkill for most people — but if you're learning for a career in infrastructure or need advanced load balancing, it's worth understanding.
Deployment
services:
haproxy:
image: haproxy:2.9-alpine
container_name: haproxy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "8404:8404" # Stats page
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
- ./certs:/etc/ssl/certs:ro
networks:
- proxy
networks:
proxy:
name: proxy
driver: bridge
Configuration
global
maxconn 4096
log stdout format raw local0
defaults
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
log global
option httplog
frontend http
bind *:80
redirect scheme https code 301
frontend https
bind *:443 ssl crt /etc/ssl/certs/
# Route based on hostname
acl host_jellyfin hdr(host) -i jellyfin.home.lab
acl host_gitea hdr(host) -i gitea.home.lab
acl host_nextcloud hdr(host) -i nextcloud.home.lab
use_backend jellyfin if host_jellyfin
use_backend gitea if host_gitea
use_backend nextcloud if host_nextcloud
backend jellyfin
server jellyfin1 jellyfin:8096 check
backend gitea
server gitea1 gitea:3000 check
backend nextcloud
server nextcloud1 nextcloud:80 check
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 10s
Strengths and Weaknesses
Strengths:
- Best raw performance and lowest latency
- Extremely mature and battle-tested
- Advanced health checking, circuit breaking, and load balancing algorithms
- Excellent stats page for monitoring
- Great for learning enterprise infrastructure patterns
Weaknesses:
- No automatic SSL certificate management — you need certbot or another tool
- Configuration syntax is powerful but not intuitive
- No Docker service discovery
- More config to write and maintain for basic use cases
- Overkill for a homelab with 10-20 services
When to Choose What
Choose Nginx Proxy Manager if:
- You're new to homelabs and want the lowest friction setup
- You prefer clicking through a GUI over editing config files
- You have a relatively static set of services that don't change often
- You want something that "just works" without learning proxy concepts
- You're setting up a homelab for someone else who needs to manage it
Choose Traefik if:
- You run many Docker containers and want automatic discovery
- You practice infrastructure as code and want config in your compose files
- You need advanced middleware (auth forwarding, rate limiting, circuit breakers)
- You're comfortable with a steeper learning curve for a more powerful tool
- You have a dynamic environment where services are created and destroyed frequently
Choose Caddy if:
- You want the simplest possible config file syntax
- Automatic HTTPS with zero configuration is important to you
- You're comfortable with text-based configuration but want something more readable than Nginx
- You want a modern tool that does the right thing by default (HTTPS, HTTP/2, security headers)
- You value a single-binary deployment with no dependencies
Choose HAProxy if:
- You're learning infrastructure for a career in DevOps/SRE
- You need advanced load balancing (weighted routing, sticky sessions, health checks)
- Raw performance matters (high-throughput media streaming, many concurrent connections)
- You're already familiar with HAProxy from work
- You don't mind managing SSL certificates separately
SSL Certificate Strategies
All four options can work with Let's Encrypt, but the experience varies significantly:
| Proxy | HTTP Challenge | DNS Challenge | Wildcard Certs | Auto-Renewal |
|---|---|---|---|---|
| NPM | GUI checkbox | GUI + API creds | Yes (DNS only) | Automatic |
| Traefik | Config entrypoint | Config + env vars | Yes (DNS only) | Automatic |
| Caddy | Automatic | Plugin + env vars | Yes (DNS only) | Automatic |
| HAProxy | External (certbot) | External (certbot) | Yes (external) | Cron job |
For internal-only services (not exposed to the internet), consider these approaches:
- Local CA with step-ca: Run your own certificate authority and have your proxy request certs from it
- Caddy's internal TLS:
tls internalgenerates self-signed certs trusted by Caddy - Wildcard cert with DNS challenge: Get a real wildcard cert for
*.home.labusing a DNS provider API - mkcert: Generate locally-trusted development certificates and install the root CA on your devices
Network Architecture Tips
Regardless of which proxy you choose, these patterns apply:
Dedicated Proxy Network
Create a Docker network that your proxy and backend services share:
networks:
proxy:
name: proxy
driver: bridge
Add only the proxy network to your backend services — don't expose their ports to the host. The proxy reaches them through the Docker network, and nothing else can reach them directly:
services:
jellyfin:
image: jellyfin/jellyfin
# No "ports:" section — only accessible through the proxy
networks:
- proxy
- default # For internal service-to-service communication
DNS Resolution
For local domains, you need your devices to resolve *.home.lab to your proxy's IP. Options:
- Pi-hole/AdGuard Home: Add DNS rewrites for your domains
- Local DNS records: Add entries to your router's DNS or a local DNS server
- Split-horizon DNS: Use a real domain with internal and external DNS views
- Tailscale MagicDNS: If you use Tailscale, leverage its built-in DNS
Multiple Proxies
You can run more than one proxy if your needs differ. A common pattern:
- Caddy or NPM for web services (simple config, auto-SSL)
- HAProxy in front of game servers or TCP services (performance, L4 routing)
Performance Comparison
For a typical homelab with under 100 concurrent connections, performance differences between these proxies are imperceptible. All four handle homelab-scale traffic without breaking a sweat.
Where performance starts to matter:
| Scenario | Best Choice | Why |
|---|---|---|
| Media streaming (many clients) | HAProxy | Lowest per-connection overhead |
| Many microservices (50+) | Traefik | Dynamic discovery reduces config burden |
| Simple homelab (5-15 services) | NPM or Caddy | Least operational overhead |
| Learning/career development | Traefik or HAProxy | Most transferable to enterprise |
Migration Tips
If you're switching from one proxy to another:
- Document your current routes — export your NPM config or save your config files
- Run both proxies on different ports during migration — old on 80/443, new on 8080/8443
- Migrate one service at a time and test before moving on
- Update DNS last — once everything works on the new proxy, switch DNS to point to it
- Keep the old proxy running for a week as a rollback option
Final Thoughts
The "best" reverse proxy is the one you'll actually maintain. NPM's GUI is unbeatable for getting started quickly. Traefik's auto-discovery is magical once it clicks. Caddy's simplicity is hard to argue with. HAProxy's power is there when you need it.
Most homelabbers start with Nginx Proxy Manager, and many never leave — it does the job well. If you find yourself editing the Advanced tab constantly or wishing for automatic container discovery, that's when Traefik or Caddy becomes worth the migration effort. And if you're building a career in infrastructure, spinning up HAProxy in your homelab is some of the best hands-on learning you can get.
Pick one, deploy it, and move on to the services that actually matter to you. You can always switch later — and the concepts transfer between all of them.