Deploy Traefik v3 on Docker: Automatic Service Discovery and Let’s Encrypt SSL for Your HomeLab

HomeLab tutorial - IT technology blog
HomeLab tutorial - IT technology blog

Quick Start: Running Traefik v3 in 5 Minutes

Self-hosting half a dozen apps quickly turns into port-forwarding headaches. You end up memorizing port 8096 for Jellyfin, 8123 for Home Assistant, and click past red browser warnings for untrusted SSL certs. Traefik v3 kills that complexity. It listens to the Docker daemon, generates reverse proxy routing on the fly, and fetches free Let’s Encrypt SSL certificates automatically.

Start by creating a clean directory layout on your Docker host:

mkdir -p ~/homelab/traefik/data
cd ~/homelab/traefik
touch data/acme.json
chmod 600 data/acme.json

The acme.json file holds private keys and minted certificates. The chmod 600 step is strictly enforced. If file permissions permit read access to group or other users, Traefik aborts on startup with an error code 1.

Next, build an external bridge network so containers across different compose files can communicate without sharing host network stacks:

docker network create traefik-proxy

Now drop this production-ready docker-compose.yml into ~/homelab/traefik:

services:
  traefik:
    image: traefik:v3.1
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/acme.json:/acme.json
    command:
      - "--log.level=INFO"
      - "--api.dashboard=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.network=traefik-proxy"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--certificatesresolvers.myresolver.acme.tlschallenge=true"
      - "[email protected]"
      - "--certificatesresolvers.myresolver.acme.storage=/acme.json"
    networks:
      - traefik-proxy

networks:
  traefik-proxy:
    external: true

Swap out [email protected] with your actual mailbox so Let’s Encrypt can notify you before certs expire. Fire up the daemon:

docker compose up -d

Traefik is now alive. It monitors the Docker socket, pushes HTTP traffic to HTTPS over port 443, and stands ready to provision TLS certs the second a new container spins up.

Deep Dive: Architecture and Automatic Container Discovery

Spinning up an internal app with legacy web servers usually means manual labor. You write upstream IP blocks, edit Nginx server files, run syntax checks, and reload daemons. Miss one semicolon, and the whole proxy reloads with errors.

Traefik takes a different approach. You define routing rules as metadata labels directly inside each application’s Compose file. Traefik handles the discovery, load balancing, and cert provisioning behind the scenes.

Core Components of Traefik v3

Four primary components handle traffic inside Traefik:

  • Providers: Data sources that broadcast infrastructure states. The Docker provider queries the Docker socket API for lifecycle events like container starts and stops.
  • EntryPoints: Network listeners bound to incoming ports. In our config, web listens on port 80 and websecure listens on port 443.
  • Routers: Rule evaluation engines. They analyze Host headers, URL paths, and query strings, then route packets to designated backends.
  • Services: Destination definitions containing target IP addresses and container listening ports.

How Docker Labels Drive Dynamic Routing

Whenever you execute docker compose up, Traefik catches the socket event in roughly 200 milliseconds. It parses container labels and updates its routing table in RAM. No process restarts. Zero dropped connections.

Here is an example running the lightweight whoami diagnostics container:

services:
  whoami:
    image: traefik/whoami
    container_name: test-whoami
    restart: unless-stopped
    networks:
      - traefik-proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.yourdomain.com`)"
      - "traefik.http.routers.whoami.entrypoints=websecure"
      - "traefik.http.routers.whoami.tls=true"
      - "traefik.http.routers.whoami.tls.certresolver=myresolver"
      - "traefik.http.services.whoami.loadbalancer.server.port=80"

networks:
  traefik-proxy:
    external: true

Because we set exposedbydefault=false, internal databases like PostgreSQL or Redis stay private. Only services explicitly stamped with traefik.enable=true get assigned public-facing routes.

Advanced Usage: DNS-01 Challenges and Custom Middleware Chains

HTTP-01 validation requires ports 80 and 443 open to the public internet. However, residential connections often run behind Carrier-Grade NAT (CGNAT) or restrictive ISP firewalls. The DNS-01 challenge bypasses open ports entirely by verifying domain ownership through temporary DNS TXT records.

Automated Wildcard SSL via Cloudflare DNS-01

DNS-01 lets you request valid wildcard certificates like *.lab.yourdomain.com without exposing a single port on your router. Everything stays secure on your 10.0.0.0/8 or 192.168.1.0/24 subnet.

Head to your Cloudflare dashboard, generate an API token with Zone.DNS:Edit scope, and update your Traefik service:

services:
  traefik:
    image: traefik:v3.1
    container_name: traefik
    restart: unless-stopped
    environment:
      - CF_DNS_API_TOKEN=your_cloudflare_secret_token_here
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/acme.json:/acme.json
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.dnsresolver.acme.dnschallenge=true"
      - "--certificatesresolvers.dnsresolver.acme.dnschallenge.provider=cloudflare"
      - "--certificatesresolvers.dnsresolver.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53"
      - "[email protected]"
      - "--certificatesresolvers.dnsresolver.acme.storage=/acme.json"
    networks:
      - traefik-proxy

Protecting Internal Services with Middleware

Middleware sits between the incoming router and the target service. It gives you fine-grained control to modify requests, strip URL paths, rate-limit brute-force attacks, or layer on HTTP Basic Authentication.

Hash an admin password using htpasswd. Be sure to escape the dollar signs with double dollar signs so Docker Compose does not parse them as shell variables:

echo $(htpasswd -nb admin your_secure_password) | sed -e s/\$/\$\$/g

Lock down Traefik’s internal web dashboard by applying authentication and recommended HSTS security headers:

    labels:
      - "traefik.enable=true"
      # Dashboard Router
      - "traefik.http.routers.dashboard.rule=Host(`traefik.yourdomain.com`)"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.entrypoints=websecure"
      - "traefik.http.routers.dashboard.tls.certresolver=dnsresolver"
      # Attach Middleware
      - "traefik.http.routers.dashboard.middlewares=auth-admin,sec-headers"
      # Basic Auth Middleware Definition
      - "traefik.http.middlewares.auth-admin.basicauth.users=admin:$$apr1$$XQZg..."
      # Security Headers Middleware Definition
      - "traefik.http.middlewares.sec-headers.headers.browserxssfilter=true"
      - "traefik.http.middlewares.sec-headers.headers.contenttypenosniff=true"
      - "traefik.http.middlewares.sec-headers.headers.framedenial=true"
      - "traefik.http.middlewares.sec-headers.headers.stsseconds=31536000"

Practical Tips for Production HomeLabs

1. Guarding the Docker Socket with a Socket Proxy

Mounting /var/run/docker.sock into any internet-facing container creates an unnecessary attack vector. A vulnerability inside Traefik could let an attacker escape the container and execute root commands on the underlying host OS.

Put tecnativa/docker-socket-proxy in front of the socket instead. It operates as an access filter, dropping write actions like volume deletion while letting read-only container events pass through safely:

services:
  docker-proxy:
    image: tecnativa/docker-socket-proxy
    container_name: docker-proxy
    restart: unless-stopped
    environment:
      - CONTAINERS=1
      - NETWORKS=1
      - SERVICES=1
      - TASKS=1
      - EVENTS=1
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - socket-net

Set Traefik’s provider endpoint to tcp://docker-proxy:2375. That keeps your host socket completely off public-facing bridge networks.

2. Avoiding Let’s Encrypt Rate Limits

Let’s Encrypt enforces a ceiling of 5 duplicate certificates per domain every 7 rolling days. Spin up and tear down a new service repeatedly while debugging, and you will quickly lock yourself out for a full week.

Route test traffic to Let’s Encrypt’s staging ACME endpoint first:

      - "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"

Verify that your browser throws a certificate error from “Fake LE Intermediate X1”. That proves DNS challenges and routing function correctly. Delete the caserver flag, redeploy, and grab your production certificate.

3. Verifying Live Logs and Routing Tables

If a new service returns a 404 or fails to route, stream the Traefik daemon logs in real time:

docker logs -f traefik

Two mistakes cause 90% of all routing failures: forgetting to join the container to the external traefik-proxy network (triggering 504 Gateway Timeouts), or omitting server.port when a container exposes multiple ports such as 80 and 8080. Check those two settings first whenever a service refuses to respond.

Share: