Traefik v3: Automatic SSL and Reverse Proxy for Docker

Traefik v3: Automatic SSL and Reverse Proxy for Docker

The problem: managing SSL, routing, and middleware for your homelab services without manual configuration

Running 5-10 services in Docker across your homelab means managing individual Nginx configs, manually renewing Let's Encrypt certificates, and manually updating DNS records. Traefik v3 eliminates this by reading Docker labels directly from your containers and automatically provisioning SSL, routing traffic, and applying middleware. This post walks through a production-ready Traefik v3 setup for Docker—the reverse proxy that grows with your homelab without the manual overhead.

Prerequisites

I'm running this on a Dell T5810 with 24GB RAM on Ubuntu 24.04.1 LTS, Docker 26.1.3, Docker Compose 2.27.0, and Traefik v3.1.2. You'll need:

  • Docker Engine 24.0+
  • Docker Compose 2.20+
  • A domain name you control (e.g., lab.example.com)
  • Port 80 and 443 accessible from your network (or behind a reverse proxy/VPN)
  • Familiarity with Docker Compose and basic networking
  • A DNS provider API token (Cloudflare, DigitalOcean, Route53, or similar) if using DNS validation

Docker Compose setup with Traefik v3 as the entrypoint

Start by creating a Traefik container that listens on ports 80 and 443, reads Docker socket for labels, and exposes its dashboard. This is the core of your infrastructure.

version: '3.9'

services:
  traefik:
    image: traefik:v3.1.2
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - traefik
    ports:
      - 80:80
      - 443:443
    environment:
      - TRAEFIK_API_DASHBOARD=true
      - TRAEFIK_PROVIDERS_DOCKER=true
      - TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT=false
      - TRAEFIK_PROVIDERS_DOCKER_NETWORK=traefik
      - TRAEFIK_ENTRYPOINTS_WEB_ADDRESS=:80
      - TRAEFIK_ENTRYPOINTS_WEBSECURE_ADDRESS=:443
      - TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_TLSCHALLENGE=true
      - TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL=admin@example.com
      - TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_STORAGE=/letsencrypt/acme.json
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
      - ./config.yml:/traefik/config.yml:ro
    labels:
      - traefik.enable=true
      - traefik.http.routers.traefik.rule=Host(`traefik.lab.example.com`)
      - traefik.http.routers.traefik.service=api@internal
      - traefik.http.routers.traefik.middlewares=auth@file
      - traefik.http.routers.traefik.entrypoints=websecure
      - traefik.http.routers.traefik.tls.certresolver=letsencrypt

networks:
  traefik:
    driver: bridge

Key details:

  • TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT=false forces you to explicitly enable each service with traefik.enable=true—this prevents accidentally exposing services.
  • Mounting /var/run/docker.sock gives Traefik read-only access to Docker metadata; this is safe but restrict it with :ro.
  • The letsencrypt volume persists your acme.json certificate file across restarts—don't lose this.
  • The Traefik dashboard itself is protected by the auth@file middleware (we'll define this next).

Configuring Let's Encrypt with TLS challenge and a static middleware file

Let's Encrypt certificates need validation. Traefik v3 supports TLS challenge (fast, no DNS API needed) or HTTP challenge. For DNS validation across multiple subdomains, you'd use DNS challenge with a provider plugin. We'll use TLS challenge here since it's simpler for most homelabs.

Create a config.yml file for static middleware definitions—particularly basic auth for the dashboard:

http:
  middlewares:
    auth:
      basicAuth:
        users:
          - "admin:$apr1$r07rnpxq$HqJZMxTLZ0izhjHrGt5O70"
    
    security-headers:
      headers:
        accessControlMaxAge: 100
        accessControlAllowOriginList:
          - "https://lab.example.com"
        accessControlAllowMethods:
          - GET
          - POST
          - PUT
        sslRedirect: true
        sslHost: "lab.example.com"
        sslForceHost: true
        contentTypeNosniff: true
        browserXssFilter: true
        referrerPolicy: "strict-origin-when-cross-origin"

    rate-limit:
      rateLimit:
        average: 100
        period: 1m
        burst: 50

Generate the bcrypt hash for basic auth using htpasswd (or an online tool for testing):

htpasswd -nb admin password123 | sed 's/:/:/g'

Gotcha: The hash format is critical—use $apr1$ or bcrypt. If authentication keeps failing, verify the hash was generated correctly by testing it outside Traefik first.

Routing and exposing services with Docker labels

Here's a complete example exposing a Portainer instance and a simple HTTP service:

  portainer:
    image: portainer/portainer-ce:2.21.0
    container_name: portainer
    restart: unless-stopped
    networks:
      - traefik
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - portainer_data:/data
    labels:
      - traefik.enable=true
      - traefik.http.routers.portainer.rule=Host(`portainer.lab.example.com`)
      - traefik.http.routers.portainer.entrypoints=websecure
      - traefik.http.routers.portainer.tls.certresolver=letsencrypt
      - traefik.http.routers.portainer.middlewares=security-headers@file
      - traefik.http.services.portainer.loadbalancer.server.port=9000

  whoami:
    image: traefik/whoami:v1.10.3
    container_name: whoami
    restart: unless-stopped
    networks:
      - traefik
    labels:
      - traefik.enable=true
      - traefik.http.routers.whoami.rule=Host(`whoami.lab.example.com`)
      - traefik.http.routers.whoami.entrypoints=websecure
      - traefik.http.routers.whoami.tls.certresolver=letsencrypt
      - traefik.http.routers.whoami.middlewares=rate-limit@file

volumes:
  portainer_data:

Breaking down the labels:

  • traefik.enable=true — Traefik will watch this container.
  • traefik.http.routers.<name>.rule — Matches incoming requests; Host() is the most common rule.
  • traefik.http.routers.<name>.entrypoints=websecure — Listen on the HTTPS entrypoint only.
  • traefik.http.routers.<name>.tls.certresolver=letsencrypt — Use the Let's Encrypt resolver defined in the main compose file.
  • traefik.http.routers.<name>.middlewares=security-headers@file — Chain the middleware; @file refers to static config, @docker refers to dynamic labels.
  • traefik.http.services.<name>.loadbalancer.server.port — The container's internal port Traefik should forward to.

Gotcha: If you have multiple replicas of a service, Traefik automatically load-balances between them. However, if the service isn't responding on the specified port within 5 seconds, Traefik marks it unhealthy and removes it from rotation temporarily. Check container logs if a route 502s.

Accessing the dashboard and monitoring your routes

Once you deploy with docker compose up -d, visit https://traefik.lab.example.com (replace with your domain). You'll see the dashboard listing all routers, services, middlewares, and certificate status. The dashboard is read-only by default in v3, which is good for security.

Check Traefik logs in real time:

docker logs -f traefik

Look for messages like Entrypoint request on websecure|myservice to confirm routing is working. ACME certificate requests appear as acme: Trying to solve DNS challenge (for DNS challenge) or acme: Trying to solve TLSSNI challenge (for TLS challenge).

Advanced: DNS challenge for wildcard certificates and dynamic middleware

If you want a wildcard certificate (*.lab.example.com), use DNS challenge with a provider plugin. For Cloudflare:

  traefik:
    image: traefik:v3.1.2
    environment:
      - TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_DNSCHALLENGE=true
      - TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_DNSCHALLENGE_PROVIDER=cloudflare
      - TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_DNSCHALLENGE_RESOLVERS=1.1.1.1:53
      - [email protected]
      - CF_API_KEY=your-cloudflare-api-key
      - TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_STORAGE=/letsencrypt/acme.json

You can also define dynamic middleware directly as Docker labels:

labels:
  - traefik.http.middlewares.custom-auth.basicauth.users=admin:$apr1$...
  - traefik.http.routers.myservice.middlewares=custom-auth@docker

Common issues and troubleshooting

Issue: Services return 502 Bad Gateway

Check that the service is on the same Docker network as Traefik and that the port in traefik.http.services.*.loadbalancer.server.port matches the container's exposed port. Verify the container is healthy:

docker ps --filter="name=myservice" --format="table {{.Names}}\t{{.Status}}"

Issue: Certificate not provisioning or stuck in "pending" state

Let's Encrypt's TLS challenge requires the router to be listening on port 443 and the domain to resolve to your server. Check that your firewall allows inbound on 443, and validate DNS resolution:

nslookup myservice.lab.example.com
curl -vI https://myservice.lab.example.com

Inspect the letsencrypt/acme.json file—certificates are stored there as base64. If it's malformed, delete it and restart Traefik to regenerate.

Issue: Docker socket permission denied

Verify Traefik container can read the socket:

docker exec traefik stat /var/run/docker.sock

The socket should be readable by the container's user (usually root inside the container). If permissions are wrong, restart Docker or adjust the socket's group permissions on the host.

Issue: Middleware not applying or showing 404 on dashboard

Middleware names must exactly match the router label. If you reference auth@file, the middleware must be named auth in config.yml`. Reload the static config by restarting the Traefik container if you edit it.

What you now have

You're running a production-grade reverse proxy that automatically:

  • Provisions HTTPS certificates from Let's Encrypt without manual intervention
  • Routes traffic based on Docker container labels
  • Applies security headers, rate limiting, and basic auth to services
  • Handles certificate renewal 30 days before expiry
  • Provides a dashboard to inspect routes and certificate status in real time

Next steps: Layer in a VPN or WAF in front of Traefik if you're exposing this on the internet. Consider setting up Traefik's Prometheus metrics endpoint and scraping it with a monitoring stack. For larger setups, explore Traefik's IngressRoute CRD support if you add Kubernetes.

See also: Traefik official documentation for the latest features and provider reference, and Let's Encrypt challenge types for detailed validation mechanics.

Disclosure: This post contains affiliate links. If you purchase through these links, we may earn a small commission at no extra cost to you. We only recommend services we've tested and trust.

Read more