Using Traefik as a Dynamic Reverse Proxy with Docker Containers

Using Traefik as a Dynamic Reverse Proxy with Docker Containers

When I first started running multiple Docker containers on my homelab VPS, I realized static reverse proxy configs became a nightmare. Every time I spun up a new service—Nextcloud, Jellyfin, Vaultwarden, another instance of Open WebUI—I had to manually edit nginx config files and reload the service. Traefik changed everything. It watches your Docker daemon, auto-discovers containers with the right labels, and dynamically routes traffic. No restarts, no manual config updates. If you're serious about self-hosting at scale, Traefik is the tool to learn.

Why Traefik Over Nginx or Caddy?

I still use Nginx and Caddy in certain scenarios, but Traefik has unique advantages when you're running a containerized infrastructure. Traefik is a proxy designed from the ground up for container orchestration. It integrates natively with Docker, reads container labels, and updates routing rules without restarting. Caddy is simpler for static setups and easier to configure manually. Nginx is battle-tested and minimal. But if you're deploying 10+ services and you want them to appear and disappear without touching your proxy config, Traefik wins.

The key difference: Traefik is a dynamic reverse proxy. Nginx requires manual reload. Traefik watches Docker events and adapts in real-time.

Architecture: How Traefik Works

Traefik sits between your clients and your backend services. Here's the flow:

  1. You deploy a Docker container with a Traefik label (e.g., traefik.enable=true)
  2. Traefik reads that label and creates a route for that service
  3. When a request comes in for that hostname, Traefik forwards it to the container
  4. SSL/TLS certificates are obtained automatically via Let's Encrypt if you configure it
  5. No proxy restart needed—updates happen live

Traefik also provides a dashboard where you can see all discovered services, routes, and middleware in real-time. I check it regularly to verify new containers are picked up correctly.

Installing Traefik with Docker Compose

I'm running this on a €5/month Hetzner VPS (or around $40/year via RackNerd if you catch their promotions). Here's a production-ready Docker Compose setup:

version: '3.8'

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - traefik
    ports:
      - "80:80"
      - "443:443"
    environment:
      - [email protected]
      - CF_API_KEY=your-cloudflare-api-key
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/traefik.yml:ro
      - ./acme.json:/acme.json
      - ./config.yml:/config.yml:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik.rule=Host(`traefik.example.com`)"
      - "traefik.http.routers.traefik.entrypoints=websecure"
      - "traefik.http.routers.traefik.service=api@internal"
      - "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
      - "traefik.http.middlewares.traefik-auth.basicauth.users=admin:$$2y$$10$$abcdef..."
      - "traefik.http.routers.traefik.middlewares=traefik-auth"

networks:
  traefik:
    external: true
Watch out: The acme.json file stores your Let's Encrypt private keys. Make sure it has strict permissions: chmod 600 acme.json. If Traefik can't write to it, certificate renewal will fail silently.

Before running this, create the external network and the acme.json file:

docker network create traefik
touch acme.json && chmod 600 acme.json

Traefik Configuration Files

The compose file references traefik.yml and config.yml. Here's the main static configuration:

# traefik.yml
global:
  checkNewVersion: true
  sendAnonymousUsage: false

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entrypoint:
          regex: "^http://(.*)$$"
          replacement: "https://$${1}"
          permanent: true
  websecure:
    address: ":443"

api:
  dashboard: true
  debug: false

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    network: traefik
    swarmMode: false
  file:
    filename: /config.yml
    watch: true

certificatesResolvers:
  letsencrypt:
    acme:
      email: [email protected]
      storage: acme.json
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"
          - "8.8.8.8:53"

The config.yml file holds dynamic configuration (routes, middleware, etc.) that Traefik watches for changes:

# config.yml
http:
  middlewares:
    redirect-https:
      redirectScheme:
        scheme: https
        permanent: true
    
    rate-limit:
      rateLimit:
        average: 100
        burst: 50

    security-headers:
      headers:
        customResponseHeaders:
          X-Frame-Options: "SAMEORIGIN"
          X-Content-Type-Options: "nosniff"
          X-XSS-Protection: "1; mode=block"

Deploying Services Behind Traefik

Once Traefik is running, deploying a service with automatic routing is trivial. Here's how I deploy Vaultwarden (password manager):

version: '3.8'

services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      - DOMAIN=https://vault.example.com
      - SIGNUPS_ALLOWED=false
      - INVITATIONS_ORG_ALLOW_USER=false
      - LOG_LEVEL=info
    volumes:
      - ./vault-data:/data
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.vaultwarden.rule=Host(`vault.example.com`)"
      - "traefik.http.routers.vaultwarden.entrypoints=websecure"
      - "traefik.http.services.vaultwarden.loadbalancer.server.port=80"
      - "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt"
      - "traefik.http.routers.vaultwarden.middlewares=security-headers@file"

networks:
  traefik:
    external: true

That's it. Deploy the container, Traefik sees the labels, creates the route, obtains a Let's Encrypt certificate, and you're live at vault.example.com. No manual nginx config, no restarts.

Tip: Use Traefik's dashboard to verify your routes. Access it at traefik.example.com (with the basic auth you configured). You'll see all discovered services, their status, and any errors in real-time.

Advanced: Path-Based Routing

Sometimes you want multiple services on the same domain but different paths. Traefik handles this cleanly with path prefixes:

labels:
  - "traefik.enable=true"
  - "traefik.http.routers.api.rule=Host(`example.com`) && PathPrefix(`/api`)"
  - "traefik.http.routers.api.entrypoints=websecure"
  - "traefik.http.services.api.loadbalancer.server.port=3000"
  - "traefik.http.routers.api.tls.certresolver=letsencrypt"
  - "traefik.http.middlewares.strip-prefix.stripprefix.prefixes=/api"
  - "traefik.http.routers.api.middlewares=strip-prefix"

Now requests to example.com/api go to your service, and the /api prefix is stripped before forwarding (so your backend sees /, not /api).

SSL/TLS and Let's Encrypt

The configuration above uses Cloudflare's DNS challenge for Let's Encrypt validation. This works even if you're behind CGNAT or a non-standard port. Traefik handles certificate renewal automatically 30 days before expiry. Certificates are stored in acme.json.

If you don't use Cloudflare, switch to the HTTP challenge (simpler for public-facing services):

certificatesResolvers:
  letsencrypt:
    acme:
      email: [email protected]
      storage: acme.json
      httpChallenge:
        entrypoint: web

With HTTP challenge, port 80 must be reachable from the internet. Traefik listens on 80 for the challenge and automatically handles it.

Monitoring and Troubleshooting

Check Traefik logs to debug routing issues:

docker logs -f traefik

Common issues I've hit:

Production Hardening Tips

For a VPS running Traefik and multiple services, I recommend:

Next Steps

Now that Traefik is running, you can deploy services confidently. Try adding Nextcloud, Jellyfin, or Gitea. Each one just needs Docker labels—no proxy restarts, no manual config. For networking, pair Traefik with Tailscale or WireGuard for secure remote access to your services. And if you're looking for affordable VPS hosting to test this setup, RackNerd and Hetzner offer reliable infrastructure around $40–50 per year.

The beauty of Traefik is that it scales with you. Start with one service, grow to ten, and the proxy adapts automatically. That's the power of dynamic routing.

Discussion