You know that moment when you’ve got six Docker containers running and you think, “I should probably stop accessing everything by port number like some kind of animal”? That’s the moment you need a reverse proxy.
But here’s the thing — picking one feels like choosing a faction in a video game. Traefik people will tell you labels are the way. NPM people will show you their beautiful GUI and ask why you’re writing YAML at 2 AM. Both camps are right. Both camps are wrong. Let’s figure out which camp you belong to.
What Even Is a Reverse Proxy?
If you already know this, skip ahead. For everyone else: a reverse proxy sits in front of your services and routes incoming traffic to the right place. Someone hits jellyfin.yourdomain.com? The reverse proxy says “ah yes, that goes to container X on port 8096” and forwards the request.
Think of it like a receptionist at a building. You don’t walk into an office complex and start opening random doors — you tell the front desk who you’re here to see, and they point you to the right floor. A reverse proxy is that receptionist, but for HTTP traffic. And it never takes a lunch break.
Both Traefik and Nginx Proxy Manager do this job well. They also handle SSL certificates (so your browser doesn’t scream at you), load balancing, and header manipulation. The difference is how they do it.
Traefik: The “I Read Your Docker Socket” Approach
Traefik’s whole pitch is auto-discovery. You slap some labels on your Docker containers, and Traefik figures out the rest. No config files to update when you add a new service. No clicking through menus. You define everything where the service lives — in the Docker Compose file.
Here’s what a basic Traefik setup looks like:
Traefik Docker Compose
version: "3.8"
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./acme.json:/acme.json
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.yourdomain.com`)"
- "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
- "traefik.http.routers.traefik-dashboard.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik-dashboard.service=api@internal"
networks:
proxy:
external: true
And the static config (traefik.yml):
api:
dashboard: true
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: you@yourdomain.com
storage: acme.json
httpChallenge:
entryPoint: web
Now when you want to add a service — say, Uptime Kuma — you just add labels to that service’s compose file:
services:
uptime-kuma:
image: louislam/uptime-kuma:1
container_name: uptime-kuma
restart: unless-stopped
volumes:
- ./data:/app/data
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.uptime-kuma.rule=Host(`status.yourdomain.com`)"
- "traefik.http.routers.uptime-kuma.entrypoints=websecure"
- "traefik.http.routers.uptime-kuma.tls.certresolver=letsencrypt"
- "traefik.http.services.uptime-kuma.loadbalancer.server.port=3001"
That’s it. No touching Traefik’s config. No restarting the proxy. Traefik sees the new container, reads the labels, provisions an SSL cert from Let’s Encrypt, and starts routing traffic. It’s genuinely magical the first time you see it work.
Traefik Middleware (Headers, Rate Limiting, etc.)
One of Traefik’s superpowers is middleware. You define it once and attach it to any router. Want security headers on everything?
# In your traefik dynamic config or as Docker labels
labels:
- "traefik.http.middlewares.secure-headers.headers.browserXssFilter=true"
- "traefik.http.middlewares.secure-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.secure-headers.headers.frameDeny=true"
- "traefik.http.middlewares.secure-headers.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.secure-headers.headers.stsSeconds=31536000"
- "traefik.http.routers.uptime-kuma.middlewares=secure-headers"
Rate limiting, IP whitelisting, basic auth, path stripping, redirect regex — it’s all middleware. The label syntax gets verbose (we’ll get to that in the downsides), but the flexibility is real.
Nginx Proxy Manager: The “Just Give Me a GUI” Approach
Nginx Proxy Manager (NPM) wraps Nginx in a web interface that makes reverse proxying feel like filling out a form. And honestly? For a lot of people, that’s exactly what they want.
NPM Docker Compose
version: "3.8"
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:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
- proxy
networks:
proxy:
external: true
That’s the entire setup. Spin it up, go to http://your-server-ip:81, log in with the default credentials (admin@example.com / changeme), and you’re in.
Adding a proxy host is literally:
- Click “Proxy Hosts”
- Click “Add Proxy Host”
- Type in your domain name
- Type in the container name or IP and port
- Flip the SSL toggle, pick “Request a new SSL Certificate,” check “Force SSL”
- Save
Done. SSL cert requested, HTTPS enforced, traffic routing. Five clicks and two text fields. Your grandmother could do this. (No shade to grandmothers — some of them run Kubernetes clusters.)
NPM’s Custom Nginx Config
NPM isn’t just a pretty face. Each proxy host has an “Advanced” tab where you can inject raw Nginx configuration directives:
# Custom headers
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;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
So you get the GUI for the 90% case and raw power when you need it. Not bad.
SSL / Let’s Encrypt: How They Compare
Both tools integrate with Let’s Encrypt. Both support HTTP challenge and DNS challenge. But the experience differs.
Traefik: You configure your certificate resolver once in the static config. Every new service that references that resolver gets a cert automatically. If you’re using DNS challenge (for wildcard certs), you set your provider credentials in environment variables and Traefik handles the rest. It supports a ridiculous number of DNS providers out of the box.
# Wildcard cert with Cloudflare DNS challenge
certificatesResolvers:
letsencrypt:
acme:
email: you@yourdomain.com
storage: acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
# Environment variables for Traefik container
environment:
- CF_API_EMAIL=you@yourdomain.com
- CF_DNS_API_TOKEN=your-api-token
NPM: You configure SSL per proxy host through the GUI. For DNS challenge, you add your API credentials in the SSL certificate section. It supports fewer DNS providers than Traefik, but covers the popular ones (Cloudflare, DigitalOcean, Route53, etc.). Wildcard certs are supported but require DNS challenge — same as Traefik.
The key difference: Traefik’s cert management is fully automatic and declarative. NPM’s is manual but visual. If you’re running 3 services, NPM’s approach is fine. If you’re running 30, Traefik’s automation saves real time.
Performance: Does It Actually Matter?
Let’s be honest — if you’re running a homelab or a small project, both are going to be fast enough. You’ll hit the limits of your services long before the reverse proxy becomes a bottleneck.
That said, here’s the breakdown for the curious:
Traefik is written in Go. It’s a single binary, low memory footprint, and handles concurrent connections efficiently. It was designed for cloud-native environments where containers scale up and down constantly.
NPM is Nginx under the hood, which is one of the most battle-tested web servers on the planet. Nginx can handle tens of thousands of concurrent connections without breaking a sweat. NPM adds a Node.js admin panel on top, but the actual proxying is pure Nginx.
In raw throughput benchmarks, Nginx typically edges out Traefik slightly, especially under extreme load. But “extreme load” here means thousands of requests per second — not exactly homelab territory. For practical purposes, call it a tie.
| Metric | Traefik | NPM (Nginx) |
|---|---|---|
| Memory usage (idle) | ~30-50 MB | ~20-40 MB (Nginx) + ~80 MB (Node admin) |
| Requests/sec (ballpark) | Very high | Very high (slight edge) |
| Startup time | Fast | Fast |
| Hot reload | Yes (auto) | Yes (via GUI apply) |
| Written in | Go | C (Nginx) + Node.js (admin) |
The Real Differences (AKA What Actually Matters)
Configuration Philosophy
Traefik is infrastructure-as-code to its core. Everything is in files, labels, or environment variables. This means:
- Version control friendly (commit your labels, track changes)
- Reproducible (spin up the whole stack from compose files)
- Scriptable (automate everything)
- Harder to learn (YAML and label syntax have a learning curve)
NPM is GUI-first. This means:
- Fast to set up (click, type, done)
- Visually clear (see all your hosts in one dashboard)
- Harder to version control (config lives in a SQLite database)
- Harder to reproduce (you’d need to back up the database or re-click everything)
Docker Integration
Traefik watches the Docker socket and reacts to container events in real time. Container starts? Route added. Container stops? Route removed. It’s seamless.
NPM doesn’t know about Docker. It proxies to hostnames and ports. If you change a container’s port or name, you update it in the GUI. This is fine for stable setups but annoying if you’re constantly iterating.
Multi-Service Scaling
Adding service number 50 in Traefik means adding 5 labels to a compose file. Adding service number 50 in NPM means 5 more clicks. Neither is hard, but one scales more gracefully with automation.
Debugging
Traefik has a built-in dashboard that shows routers, services, middlewares, and their health. It’s genuinely useful. But when something goes wrong, you’re reading logs and checking label syntax. Misspell a label? Traefik silently ignores it. Good luck figuring out which of your 47 labels has a typo.
NPM shows you errors in the GUI and Nginx logs are well-documented. When things break, the error messages are usually clear. The Nginx community is massive, and most problems have been solved on Stack Overflow already.
Access Lists and Authentication
Traefik handles this through middleware. BasicAuth, ForwardAuth (for integration with Authelia, Authentik, etc.), IP whitelisting — all configurable via labels.
# BasicAuth middleware
labels:
- "traefik.http.middlewares.my-auth.basicauth.users=admin:$$apr1$$xyz..."
- "traefik.http.routers.my-service.middlewares=my-auth"
# ForwardAuth with Authelia
labels:
- "traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://auth.yourdomain.com"
- "traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true"
- "traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email"
NPM has built-in access lists where you can restrict by IP or add HTTP basic auth through the GUI. For more advanced auth (like SSO), you’d add custom Nginx directives in the advanced config tab or run a separate auth proxy.
When to Use Traefik
Pick Traefik if:
- You’re all-in on Docker and want your proxy config to live alongside your services
- You have lots of services and don’t want to manually configure each one
- You value infrastructure-as-code and want everything in Git
- You’re comfortable with YAML and don’t mind a learning curve
- You need dynamic scaling — services coming and going frequently
- You want Kubernetes support down the road (Traefik is a popular ingress controller)
- You like middleware chains for auth, headers, rate limiting, etc.
When to Use Nginx Proxy Manager
Pick NPM if:
- You want something working in 10 minutes without reading documentation
- You prefer GUIs over config files (no judgment, seriously)
- Your setup is relatively stable — you’re not adding/removing services daily
- You’re newer to self-hosting and want visual feedback
- You need raw Nginx config for specific use cases (NPM lets you inject it)
- You want something your non-technical housemate can manage while you’re away
- You already know Nginx and want a friendly wrapper around it
The “Why Not Both?” Option
Here’s a spicy take: you can actually run both. Some people use Traefik for their Docker services and NPM for non-Docker services or specific edge cases. Is it elegant? Not really. Does it work? Absolutely. But unless you have a specific reason, pick one and stick with it.
Migration Tips
Moving from NPM to Traefik: Document every proxy host in NPM (domain, target, port, SSL settings, custom config). Then translate each one into Docker labels. The hardest part is usually replicating custom Nginx directives as Traefik middleware.
Moving from Traefik to NPM: Export your domains and port mappings from your compose labels. Re-create each as a proxy host in NPM. Copy any custom headers into the advanced config tab.
Either migration takes an afternoon for a typical homelab. Budget extra time for DNS propagation if you’re changing server IPs.
The Verdict
There’s no wrong answer here — just different answers for different people.
Traefik is for the person who wants to docker compose up and have everything work, tracked in Git, reproducible on a new server in minutes. The learning curve is real, but once you’re past it, adding new services is almost effortless.
Nginx Proxy Manager is for the person who wants to see their reverse proxy config on a screen, click buttons, and get on with their life. It’s approachable, it’s powerful enough for most use cases, and it has Nginx’s legendary reliability underneath.
If you’re just starting out with self-hosting, NPM will get you up and running faster with less frustration. If you’re the type who has a Git repo for your entire homelab (you know who you are), Traefik will feel like home.
Either way, you’re done typing port numbers into your browser. And that, friends, is what civilization looks like.