Skip to main content

Traefik vs Caddy vs Nginx: Reverse Proxy for Self-Hosters 2026

·OSSAlt Team
traefikcaddynginxreverse-proxyself-hostingdockertls2026

TL;DR

Three great reverse proxies for self-hosters, each with a different philosophy: Caddy (Apache 2.0, ~58K stars, Go) is the simplest — automatic HTTPS with zero config, readable Caddyfile syntax. Traefik (MIT, ~52K stars, Go) is Docker-native — auto-discovers containers via labels, no config file changes needed when adding new services. Nginx (BSD) is the battle-tested veteran — maximum flexibility and performance, steeper config curve. Most homelabs start with Caddy, teams running many Docker services prefer Traefik.

Key Takeaways

  • Caddy: Simplest, automatic HTTPS, great for static sites and straightforward proxying
  • Traefik: Docker-native, zero-downtime reloads, automatic service discovery via labels
  • Nginx: Most flexible, highest performance, requires manual SSL cert renewal (or nginx-proxy)
  • Auto TLS: Caddy and Traefik handle Let's Encrypt automatically; Nginx needs Certbot
  • Config hot-reload: All three support it; Traefik does it without a restart
  • Performance: All can handle tens of thousands of concurrent connections — not a bottleneck

Feature Comparison

FeatureCaddyTraefik v3Nginx
LicenseApache 2.0MITBSD-2-Clause
GitHub Stars~58K~52K~20K (mainline)
Auto HTTPSYes (built-in)Yes (ACME)No (need Certbot)
Docker label routingNo (manual)Yes (native)No (need nginx-proxy)
Config languageCaddyfileTOML/YAML/labelsNginx config (nginx.conf)
Config hot-reloadYesYes (zero downtime)Yes (nginx -s reload)
Built-in authBasic authBasic authBasic auth
Rate limitingPluginMiddlewareModule (nginx-plus or lua)
Load balancingYesYesYes
WebSocket supportYesYesYes
gRPC supportYesYesYes
Metrics/metrics endpointPrometheus built-inWith stub_status
RAM usage~30MB~50MB~10MB

Option 1: Caddy — Simplest Auto-HTTPS

Caddy has the most human-friendly configuration. One line per site, automatic HTTPS, no certificate management.

Caddy Docker Setup

# docker-compose.yml
services:
  caddy:
    image: caddy:alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"    # HTTP/3
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - proxy

volumes:
  caddy_data:
  caddy_config:

networks:
  proxy:
    external: true
# Create shared network first:
docker network create proxy

Caddyfile Examples

# Single site — auto HTTPS:
app.yourdomain.com {
    reverse_proxy localhost:8080
}

# Multiple sites:
grafana.yourdomain.com {
    reverse_proxy grafana:3000
}

paperless.yourdomain.com {
    reverse_proxy paperless:8000
}

# Basic auth:
private.yourdomain.com {
    basicauth {
        admin JDJhJDE0JG...   # caddy hash-password
    }
    reverse_proxy localhost:9000
}

# Strip path prefix:
yourdomain.com/api/* {
    uri strip_prefix /api
    reverse_proxy api-service:8080
}

# Multiple upstreams (load balance):
api.yourdomain.com {
    reverse_proxy {
        to localhost:8081
        to localhost:8082
        lb_policy round_robin
    }
}

Wildcard TLS (DNS challenge)

For *.yourdomain.com wildcard certs (requires DNS API):

*.yourdomain.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
    @grafana host grafana.yourdomain.com
    handle @grafana {
        reverse_proxy grafana:3000
    }
}

Use caddy:cloudflare Docker image (with DNS plugin):

image: ghcr.io/caddybuilds/caddy-cloudflare:latest

Option 2: Traefik — Docker-Native Auto-Discovery

Traefik discovers services automatically by reading Docker container labels. Add a new container → Traefik routes to it without any config file changes.

Traefik Docker Setup

# docker-compose.yml
services:
  traefik:
    image: traefik:v3
    container_name: traefik
    restart: unless-stopped
    command:
      - "--api.dashboard=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.network=proxy"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.email=you@yourdomain.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik_certs:/letsencrypt
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.yourdomain.com`)"
      - "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.middlewares=auth"
      - "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$hash"
    networks:
      - proxy

volumes:
  traefik_certs:

networks:
  proxy:
    external: true

Adding Services via Labels

Any container on the proxy network with Traefik labels gets automatically routed:

# Any other service's docker-compose.yml:
services:
  grafana:
    image: grafana/grafana:latest
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.grafana.rule=Host(`grafana.yourdomain.com`)"
      - "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
      - "traefik.http.services.grafana.loadbalancer.server.port=3000"

networks:
  proxy:
    external: true

No Traefik config changes needed. Start the container → routing is live.

Traefik Middlewares

Apply reusable middleware via labels:

labels:
  # Rate limiting:
  - "traefik.http.middlewares.ratelimit.ratelimit.average=100"
  - "traefik.http.middlewares.ratelimit.ratelimit.burst=50"
  
  # Apply to router:
  - "traefik.http.routers.myapp.middlewares=ratelimit@docker"
  
  # Strip prefix:
  - "traefik.http.middlewares.stripprefix.stripprefix.prefixes=/app"
  
  # Forward auth (SSO via Authentik):
  - "traefik.http.middlewares.authentik.forwardauth.address=https://auth.yourdomain.com/outpost.goauthentik.io/auth/traefik"

Option 3: Nginx — Battle-Tested Flexibility

Nginx is the most widely deployed web server and reverse proxy. Maximum flexibility, best for complex routing rules, requires manual TLS management.

Nginx Docker Setup

services:
  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./conf.d:/etc/nginx/conf.d:ro
      - /etc/letsencrypt:/etc/letsencrypt:ro
      - certbot_webroot:/var/www/certbot:ro

  certbot:
    image: certbot/certbot:latest
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt
      - certbot_webroot:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

volumes:
  certbot_webroot:

Nginx Config Example

# /etc/nginx/conf.d/app.conf

server {
    listen 80;
    server_name app.yourdomain.com;
    
    # Let's Encrypt renewal:
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
    
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name app.yourdomain.com;
    
    ssl_certificate /etc/letsencrypt/live/app.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.yourdomain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    
    location / {
        proxy_pass http://localhost:8080;
        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;
    }
}

Get certificates with Certbot:

certbot certonly --webroot -w /var/www/certbot \
  -d app.yourdomain.com --email you@example.com --agree-tos

Decision Guide

Choose Caddy if:

  • You're new to reverse proxies
  • You want the fastest path to HTTPS — minimal config
  • You run a small number of services (under ~20)
  • You prefer editing a config file to managing labels
  • Your sites are mostly static or simple reverse proxies

Choose Traefik if:

  • You run many Docker services and want zero-touch routing
  • Services are added/removed frequently
  • You want a dashboard showing all routes
  • You're already in a Docker Compose-heavy workflow
  • You need advanced middleware (rate limiting, auth, headers) per-service

Choose Nginx if:

  • You need maximum flexibility and fine-grained control
  • You have complex routing rules (regex, geo-IP, etc.)
  • You serve static files at high scale
  • You need the nginx ecosystem (ModSecurity WAF, GeoIP2, etc.)
  • You're familiar with Nginx and don't want to learn a new tool

See all open source infrastructure tools at OSSAlt.com/categories/infrastructure.

Comments