LinuxcontainersTested on real hardware

Traefik + Let's Encrypt Reverse Proxy (Docker Compose)

A complete Traefik v3 docker-compose.yml with automatic Let's Encrypt certificates, an HTTPS redirect, and label-based routing — plus the DNS-challenge variant for wildcard certs on internal services.

Distrosubuntu, debian
Shellyaml
Updated
Compose file
yaml
# docker-compose.yml — Traefik v3 edge router with automatic Let's Encrypt
# certificates via the HTTP-01 challenge. Every container that adds the
# traefik.* labels is discovered and routed automatically. Ports 80/443
# on the host must be free and reachable from the internet for HTTP-01.
services:
  traefik:
    image: traefik:v3.3
    container_name: traefik
    restart: unless-stopped
    command:
      # --- entrypoints ---
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      # redirect all HTTP to HTTPS
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      # --- docker provider (auto-discovery) ---
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      # --- Let's Encrypt (HTTP-01 challenge) ---
      - "--certificatesresolvers.le.acme.httpchallenge=true"
      - "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
      - "--certificatesresolvers.le.acme.email=you@example.com"   # YOUR email
      - "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./letsencrypt:/letsencrypt"

  # Example service: Traefik discovers it from the labels below.
  whoami:
    image: traefik/whoami
    container_name: whoami
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.example.com`)"  # YOUR domain
      - "traefik.http.routers.whoami.entrypoints=websecure"
      - "traefik.http.routers.whoami.tls.certresolver=le"

What this does

Runs Traefik v3 as the single HTTPS entry point for your homelab and issues browser-trusted certificates from Let’s Encrypt automatically. Any container that carries the traefik.* labels — like the bundled whoami test service — is discovered and routed with no config-file reload. All plain HTTP is redirected to HTTPS.

Why this approach

Traefik’s Docker provider watches the Docker socket and builds its routing table from container labels, so adding a service is just adding four labels — the proxy config maintains itself. The HTTP-01 ACME challenge is the simplest way to get real certificates: Let’s Encrypt confirms you control the domain by fetching a token over port 80, which Traefik answers on the web entrypoint. exposedbydefault=false means no container is routed unless it explicitly opts in with traefik.enable=true, so nothing leaks by accident. The concepts behind all of this are in What Is Traefik? and the walkthrough is in How to Set Up Traefik with Let’s Encrypt.

Prerequisites

  • Docker Engine + Compose plugin (docker-install-linux playbook)
  • A domain you control with an A record for each hostname (e.g. whoami.example.com) pointing at this host’s public IP
  • Ports 80 and 443 free on the host and reachable from the internet (HTTP-01 needs port 80 open)

Deploy

Save the script above as /opt/traefik/docker-compose.yml, set your real email and domain, then:

bash

cd /opt/traefik
# acme.json must be private or Traefik refuses to use it
mkdir -p letsencrypt && touch letsencrypt/acme.json && chmod 600 letsencrypt/acme.json

sudo docker compose up -d
sudo docker compose logs -f traefik   # watch for "certificate obtained"

Then visit https://whoami.example.com — a valid padlock, no warning. Add any other service by copying the four whoami labels and changing the router name and Host() rule.

Internal services: DNS-01 wildcard variant

HTTP-01 needs port 80 open to the internet, which you may not want for a purely internal lab. The DNS-01 challenge proves domain control through an API record instead, so it works with no inbound ports and can issue a single wildcard (*.example.com) covering every internal service. Swap the resolver command lines for your DNS provider (Cloudflare shown — see the full provider list):

bash

# replace the httpchallenge lines with:
- "--certificatesresolvers.le.acme.dnschallenge=true"
- "--certificatesresolvers.le.acme.dnschallenge.provider=cloudflare"
- "--certificatesresolvers.le.acme.email=you@example.com"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"

# and pass the provider API token to the container:
environment:
- "CF_DNS_API_TOKEN=YOUR_SCOPED_TOKEN"

Pair this with Pi-hole local DNS records so *.example.com resolves to the Traefik host on your LAN.

Notes

  • chmod 600 acme.json is mandatory — Traefik exits if the ACME store is world-readable.
  • Use the staging CA while testing to avoid Let’s Encrypt rate limits: add --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory, confirm it works, then remove it and delete acme.json to fetch real certs.
  • Mount the Docker socket read-only (:ro). The socket is root-equivalent; for a hardened setup put a socket proxy in front of it.
  • Never publish your real domain or API token — keep the token in an .env file outside version control.