Skip to content
SumGuy's Ramblings
Go back

Traefik vs Nginx Proxy Manager: Reverse Proxies for Humans

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:

  1. Click “Proxy Hosts”
  2. Click “Add Proxy Host”
  3. Type in your domain name
  4. Type in the container name or IP and port
  5. Flip the SSL toggle, pick “Request a new SSL Certificate,” check “Force SSL”
  6. 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.

MetricTraefikNPM (Nginx)
Memory usage (idle)~30-50 MB~20-40 MB (Nginx) + ~80 MB (Node admin)
Requests/sec (ballpark)Very highVery high (slight edge)
Startup timeFastFast
Hot reloadYes (auto)Yes (via GUI apply)
Written inGoC (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:

NPM is GUI-first. This means:

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:

When to Use Nginx Proxy Manager

Pick NPM if:

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.


Share this post on:

Previous Post
Home Lab Hardware Guide 2026: What to Buy, What to Avoid, and What to Beg For
Next Post
Proxmox vs XCP-ng: Hypervisors for People Who Like Their Data Center at Home