◀ Back to blog
Docker

Traefik and Docker: reverse proxy and automatic HTTPS

Published on 25 Aug 2026· 5 min read
#Docker#Traefik#HTTPS#Reverse proxy

Why Traefik?

As soon as a server hosts several containerized applications, you need a reverse proxy in front of them: a single entry point on ports 80 and 443 that routes each domain to the right container and handles HTTPS. Nginx does this very well, but every new application means writing a virtual host, reloading the configuration and generating a certificate.

Traefik flips the logic: it reads the Docker API, discovers containers and configures them from their labels. Starting a container is enough to publish it over HTTPS, with a Let's Encrypt certificate obtained and renewed automatically.

The shared network

Traefik and the applications it exposes must share a Docker network. Create it once and for all:

docker network create proxy

Traefik itself

A /opt/traefik/compose.yaml file:

services:
  traefik:
    image: traefik:v3.5
    restart: unless-stopped
    command:
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --providers.docker.network=proxy
      - --entrypoints.web.address=:80
      - --entrypoints.web.http.redirections.entrypoint.to=websecure
      - --entrypoints.web.http.redirections.entrypoint.scheme=https
      - --entrypoints.websecure.address=:443
      - --certificatesresolvers.le.acme.email=contact@example.com
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
      - --api.dashboard=true
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)
      - traefik.http.routers.dashboard.entrypoints=websecure
      - traefik.http.routers.dashboard.tls.certresolver=le
      - traefik.http.routers.dashboard.service=api@internal
      - traefik.http.routers.dashboard.middlewares=dashboard-auth
      - traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$Hq3vN2kS$$exempleDeHashAremplacer

networks:
  proxy:
    external: true

A few important points:

  • exposedbydefault=false: no container is published without traefik.enable=true. Without this option, a database started by mistake could end up exposed.
  • The HTTP → HTTPS redirect is declared once, on the web entrypoint.
  • The Docker socket is mounted read-only. It still gives a lot of power over the host: for extra hardening, put a socket proxy (such as tecnativa/docker-socket-proxy) between Traefik and Docker.
  • In a Compose file, the $ signs of the hash must be doubled ($$). Generate the hash with htpasswd -nb admin password (from the apache2-utils package).
cd /opt/traefik && docker compose up -d
docker compose logs -f traefik

Publishing an application

On the application side, all it takes is joining the proxy network and describing the routing with labels. Example with a Symfony container (PHP-FPM + Nginx in the image, on port 8080):

services:
  app:
    image: ghcr.io/moi/app:latest
    restart: unless-stopped
    env_file: .env.prod
    networks:
      - proxy
      - internal
    labels:
      - traefik.enable=true
      - traefik.http.routers.app.rule=Host(`app.example.com`) || Host(`www.app.example.com`)
      - traefik.http.routers.app.entrypoints=websecure
      - traefik.http.routers.app.tls.certresolver=le
      - traefik.http.routers.app.middlewares=secure-headers,rate-limit
      - traefik.http.services.app.loadbalancer.server.port=8080
      - traefik.http.middlewares.secure-headers.headers.stsSeconds=31536000
      - traefik.http.middlewares.secure-headers.headers.stsIncludeSubdomains=true
      - traefik.http.middlewares.secure-headers.headers.contentTypeNosniff=true
      - traefik.http.middlewares.secure-headers.headers.frameDeny=true
      - traefik.http.middlewares.rate-limit.ratelimit.average=50
      - traefik.http.middlewares.rate-limit.ratelimit.burst=100

  database:
    image: mysql:8.4
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: app
      MYSQL_USER: app
      MYSQL_PASSWORD_FILE: /run/secrets/db_password
      MYSQL_RANDOM_ROOT_PASSWORD: "yes"
    secrets:
      - db_password
    volumes:
      - db-data:/var/lib/mysql
    networks:
      - internal

networks:
  proxy:
    external: true
  internal:

volumes:
  db-data:

secrets:
  db_password:
    file: ./secrets/db_password.txt

The database is only attached to the internal network: Traefik cannot reach it, and it publishes no port. On the first docker compose up -d, Traefik detects the container, obtains the certificate for both domains and starts routing traffic, within seconds and without any restart.

Reading a routing label

Labels all follow the same grammar: traefik.http.<type>.<name>.<option>.

  • routers: which request (Host rule, PathPrefix…) arrives on which entrypoint, with which TLS settings and which middlewares
  • services: which container port the traffic is sent to (required if the image exposes several ports)
  • middlewares: the transformations applied along the way (headers, authentication, rate limiting, redirects, compression)

A middleware declared on one container can be reused by the others: define your security headers once, on the Traefik container, then reference them with the @docker suffix, for example secure-headers@docker.

Wildcard certificates with the DNS challenge

The HTTP challenge requires the server to be reachable on port 80. For a *.example.com certificate, or a server that is not exposed, use the DNS challenge. With Cloudflare, for example:

      - --certificatesresolvers.le.acme.dnschallenge=true
      - --certificatesresolvers.le.acme.dnschallenge.provider=cloudflare
    environment:
      CF_DNS_API_TOKEN_FILE: /run/secrets/cf_token

The Cloudflare token only needs the Zone → DNS → Edit permission on the relevant zone.

Troubleshooting

  • 404 "page not found": the router does not exist. Check traefik.enable=true and the Host rule (the backticks are mandatory).
  • 502 Bad Gateway: Traefik finds the container but cannot reach it. Is the container on the proxy network? Is the loadbalancer port correct?
  • Self-signed "TRAEFIK DEFAULT CERT" certificate: the ACME challenge failed. Check Traefik's logs; does the DNS point to the server, and is port 80 open?
  • Let's Encrypt rate limits: while experimenting, use the staging server (--certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory) so you do not hit the quotas.

Once in place, adding an application comes down to writing three or four labels. That is what makes Traefik so pleasant for a personal server or a small infrastructure hosting many services.