← All articles
NETWORKING Homelab Reverse Proxy Showdown: Nginx Proxy Manager ... 2026-02-09 · 10 min read · reverse-proxy · nginx-proxy-manager · traefik

Homelab Reverse Proxy Showdown: Nginx Proxy Manager vs Traefik vs Caddy vs HAProxy

Networking 2026-02-09 · 10 min read reverse-proxy nginx-proxy-manager traefik caddy haproxy networking docker ssl

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).

Caddy web server logo

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

  1. Navigate to http://your-server:81
  2. Click Proxy Hosts > Add Proxy Host
  3. Enter your domain (e.g., jellyfin.home.lab)
  4. Set the forward hostname to the container name or IP
  5. Set the forward port (e.g., 8096)
  6. Under the SSL tab, request a new certificate
  7. 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:

Weaknesses:

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:

Weaknesses:

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:

Weaknesses:

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:

Weaknesses:

When to Choose What

Choose Nginx Proxy Manager if:

Choose Traefik if:

Choose Caddy if:

Choose HAProxy if:

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:

  1. Local CA with step-ca: Run your own certificate authority and have your proxy request certs from it
  2. Caddy's internal TLS: tls internal generates self-signed certs trusted by Caddy
  3. Wildcard cert with DNS challenge: Get a real wildcard cert for *.home.lab using a DNS provider API
  4. 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:

  1. Pi-hole/AdGuard Home: Add DNS rewrites for your domains
  2. Local DNS records: Add entries to your router's DNS or a local DNS server
  3. Split-horizon DNS: Use a real domain with internal and external DNS views
  4. 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:

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:

  1. Document your current routes — export your NPM config or save your config files
  2. Run both proxies on different ports during migration — old on 80/443, new on 8080/8443
  3. Migrate one service at a time and test before moving on
  4. Update DNS last — once everything works on the new proxy, switch DNS to point to it
  5. 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.