Traefik in Docker Compose

services: traefik: image: traefik:v3.7 ports: - "8080:8080" - "80:80" volumes: - $PWD/traefik.yml:/etc/traefik/traefik.yml - /var/run/docker.sock:/var/run/docker.sock

In Docker Compose, Traefik finds the containers to route through the Docker socket, mounted with the line /var/run/docker.sock:/var/run/docker.sock, and each service opts in with traefik.* labels. The static configuration goes in /etc/traefik/traefik.yml.

The docker run command, converted

The v3 command from the image’s Docker Hub page, with the tag pinned to the current minor release:

docker run -d -p 8080:8080 -p 80:80 \
  -v $PWD/traefik.yml:/etc/traefik/traefik.yml \
  -v /var/run/docker.sock:/var/run/docker.sock \
  traefik:v3.7
services:
  traefik:
    image: traefik:v3.7
    ports:
      - "8080:8080"
      - "80:80"
    volumes:
      - $PWD/traefik.yml:/etc/traefik/traefik.yml
      - /var/run/docker.sock:/var/run/docker.sock

The service is named traefik after the image, since the command has no --name. Compose fills in $PWD from the environment of whoever runs it, which is the current directory, not the file’s. Write ./traefik.yml, which Compose resolves next to the file.

A routed service

# traefik.yml
entryPoints:
  web:
    address: ":80"
providers:
  docker:
    exposedByDefault: false
api:
  insecure: true
ping: {}
services:
  traefik:
    image: traefik:v3.7
    ports:
      - "80:80"
      - "127.0.0.1:8080:8080"
    volumes:
      - ./traefik.yml:/etc/traefik/traefik.yml:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    healthcheck:
      test: ["CMD", "traefik", "healthcheck", "--ping"]
      interval: 10s
      timeout: 3s
      retries: 3
  whoami:
    image: traefik/whoami:v1.12
    labels:
      traefik.enable: "true"
      traefik.http.routers.whoami.rule: Host(`whoami.docker.localhost`)
    depends_on:
      traefik:
        condition: service_healthy

traefik healthcheck --ping calls the /ping endpoint, which is off until the static configuration has ping. curl -H 'Host: whoami.docker.localhost' http://127.0.0.1/ then reaches whoami.

Gotchas

  • If traefik.yml doesn’t exist when the container starts, Docker creates a directory by that name, and Traefik stops with read /etc/traefik/traefik.yml: is a directory.
  • exposedByDefault is true unless you turn it off, and then Traefik makes a router for every container the Docker daemon runs, other projects’ included.
  • The Docker Hub page’s defaultRule builds the host from the container’s .Name. Under Compose that name is <service>-<project>, so its test.docker.localhost becomes test-myproject.docker.localhost.
  • :ro on the socket doesn’t make the Docker API read-only: connecting to a socket isn’t a write to the file, so Traefik, or anything that takes it over, can still start and stop containers. A socket proxy that allows only reads is the stricter setup.
  • api.insecure: true serves the dashboard on 8080 with no login, so publish it on 127.0.0.1 only.

Try it in the docker run to Compose Converter

compose.yaml

1 service · 1 thing left out

Left out, and why

  • -dCompose starts services in the background already, so this flag needs no key.

Runs in your browser. Nothing you enter is sent anywhere.