Using Traefik as a Reverse Proxy: Dynamic Routing for Docker Containers

Using Traefik as a Reverse Proxy: Dynamic Routing for Docker Containers

We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.

When I first started self-hosting multiple services on Docker, I spent weeks manually configuring Nginx entries, restarting the container every time I added a new app, and wrestling with SSL certificate renewals. Then I discovered Traefik, and it changed everything. Unlike static reverse proxies, Traefik watches your Docker daemon in real-time, automatically discovers containers, and routes traffic to them based on labels you define—all with zero downtime. If you're running 3 or more services in Docker, Traefik will save you hours of repetitive configuration.

Why Traefik Over Nginx or Caddy?

I respect both Nginx and Caddy—they're rock-solid. But Traefik's killer feature is its tight integration with Docker. Every container label becomes routing logic. You don't edit config files and reload; you spin up a container with the right labels, and Traefik automatically wires it in. The middleware system is also cleaner than Nginx's location blocks—think built-in auth, rate limiting, and header manipulation without wrestling with regex.

Caddy is simpler if you're running bare metal, but in a containerized environment, Traefik's service discovery saves you weeks of operational burden. I prefer Traefik because:

Architecture Overview

Before we code, let me explain the mental model. Traefik sits in front of all your containers, listening on ports 80 and 443. It queries the Docker socket to discover running containers. When you label a container with traefik.enable=true and traefik.http.routers.myapp.rule=Host(`myapp.example.com`), Traefik creates a router pointing to that container's port. If the container is removed, the route vanishes automatically.

Traefik also manages SSL. It watches your routers and automatically requests certificates from Let's Encrypt, stores them, and renews them 30 days before expiry—all in the background.

Tip: Give your domain's DNS at least 24 hours to propagate before starting Traefik with Let's Encrypt enabled. If Traefik tries to validate DNS challenge and fails, Let's Encrypt will rate-limit your domain for a week. Test with staging certs first: set caServer = "https://acme-staging-v02.api.letsencrypt.org/directory" in traefik.yml to avoid quota issues.

Setting Up Traefik with Docker Compose

I'll walk you through a production-ready setup. You'll need a VPS (around $40/year from RackNerd gets you a decent box with 1-2 vCPU and enough bandwidth for a homelab), a domain name, and basic Docker knowledge.

First, create a directory structure:

mkdir -p ~/traefik/{config,data}
cd ~/traefik
touch docker-compose.yml traefik.yml

Now the main Docker Compose file. This spins up Traefik, mounts the socket (so it can talk to Docker), and sets up Let's Encrypt storage:

version: '3.8'

services:
  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: always
    security_opt:
      - no-new-privileges:true
    networks:
      - traefik
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    environment:
      - TRAEFIK_API_DASHBOARD=true
      - TRAEFIK_PROVIDERS_DOCKER=true
      - TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT=false
      - TRAEFIK_ENTRYPOINTS_WEB_ADDRESS=:80
      - TRAEFIK_ENTRYPOINTS_WEBSECURE_ADDRESS=:443
      - TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE_ENTRYPOINT=web
      - TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL=your-email@example.com
      - TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_STORAGE=/letsencrypt/acme.json
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/letsencrypt:/letsencrypt
      - ./traefik.yml:/traefik.yml:ro
    labels:
      - traefik.enable=true
      - traefik.http.routers.traefik.rule=Host(`traefik.example.com`)
      - traefik.http.routers.traefik.service=api@internal
      - traefik.http.routers.traefik.entrypoints=websecure
      - traefik.http.routers.traefik.tls.certresolver=letsencrypt
      - traefik.http.middlewares.traefik-auth.basicauth.users=admin:$$2y$$05$$BCRYPTHASHEDPASSWORD
      - traefik.http.routers.traefik.middlewares=traefik-auth

  whoami:
    image: traefik/whoami:latest
    container_name: whoami
    restart: always
    networks:
      - traefik
    labels:
      - traefik.enable=true
      - traefik.http.routers.whoami.rule=Host(`whoami.example.com`)
      - traefik.http.routers.whoami.entrypoints=websecure
      - traefik.http.routers.whoami.tls.certresolver=letsencrypt
      - traefik.http.services.whoami.loadbalancer.server.port=80

networks:
  traefik:
    driver: bridge

Replace [email protected] and example.com with your actual email and domain. The whoami service is a test app that returns the request details—useful for debugging.

For the basic auth password, generate a bcrypt hash:

htpasswd -nB admin | sed 's/\$/\$\$/g'

Copy the output and paste it into the label (the double $$ is YAML escaping for single $).

Next, create traefik.yml with advanced settings:

api:
  dashboard: true
  insecure: false

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https

  websecure:
    address: ":443"
    http:
      tls:
        certResolver: letsencrypt

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    network: traefik

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

This config redirects all HTTP traffic to HTTPS automatically. The exposedByDefault: false means containers are invisible to Traefik until you explicitly label them.

Start Traefik:

docker-compose up -d

Check the logs:

docker-compose logs -f traefik

You should see Traefik starting, discovering the whoami container, and requesting certificates. Access the dashboard at https://traefik.example.com (using the basic auth credentials you set).

Watch out: If you see "acme challenge failed," your DNS isn't resolving yet or port 80 isn't reachable from the internet. Check your firewall rules and confirm your domain's A record points to your server's IP.

Adding Your Own Services

Once Traefik is running, adding services is dead simple. Here's a Nextcloud example:

  nextcloud:
    image: nextcloud:latest
    container_name: nextcloud
    restart: always
    networks:
      - traefik
    environment:
      - NEXTCLOUD_ADMIN_USER=admin
      - NEXTCLOUD_ADMIN_PASSWORD=secure-password-here
    volumes:
      - ./data/nextcloud:/var/www/html
    labels:
      - traefik.enable=true
      - traefik.http.routers.nextcloud.rule=Host(`cloud.example.com`)
      - traefik.http.routers.nextcloud.entrypoints=websecure
      - traefik.http.routers.nextcloud.tls.certresolver=letsencrypt
      - traefik.http.services.nextcloud.loadbalancer.server.port=80

Add this to your docker-compose.yml under services, then run:

docker-compose up -d nextcloud

Traefik will detect it immediately, request a cert for cloud.example.com, and route traffic. No manual Nginx config, no service restart. Just add the service and labels.

Middleware: Authentication, Rate Limits, Headers

Traefik's middleware lets you modify requests and responses without touching application code. For example, adding HTTP Basic Auth to a service:

labels:
  - traefik.enable=true
  - traefik.http.routers.myapp.rule=Host(`myapp.example.com`)
  - traefik.http.routers.myapp.entrypoints=websecure
  - traefik.http.routers.myapp.tls.certresolver=letsencrypt
  - traefik.http.middlewares.myapp-auth.basicauth.users=user:$$2y$$05$$HASHHERE
  - traefik.http.routers.myapp.middlewares=myapp-auth
  - traefik.http.services.myapp.loadbalancer.server.port=3000

Or rate limiting (max 10 requests per 60 seconds):

  - traefik.http.middlewares.myapp-ratelimit.ratelimit.average=10
  - traefik.http.middlewares.myapp-ratelimit.ratelimit.period=60s
  - traefik.http.routers.myapp.middlewares=myapp-ratelimit

You can chain multiple middleware with comma-separated values:

  - traefik.http.routers.myapp.middlewares=myapp-auth,myapp-ratelimit,my-headers

Monitoring and Debugging

The dashboard is your best friend. Visit https://traefik.example.com to see:

For deeper troubleshooting, check logs:

docker-compose logs traefik | grep -i error

To verify routing, use curl:

curl -I https://myapp.example.com

Or inspect Traefik's entrypoints and service discovery:

docker exec traefik traefik --help | grep -A 20 providers

Production Hardening

Before exposing Traefik to the internet, harden it: