Aller au contenu
medium

Traefik avec Docker : labels, réseaux et exemples pratiques

23 min de lecture

Logo traefik

Traefik détecte automatiquement vos conteneurs Docker et crée les routes correspondantes sans fichier de configuration à maintenir. Vous définissez le routage directement via des labels sur vos conteneurs : Traefik lit ces labels, génère la configuration et l'applique en temps réel.

Ce guide vous montre comment configurer le provider Docker, écrire les labels de routage, gérer les réseaux, et déployer des applications complètes avec Docker Compose. À la fin, vous saurez exposer n'importe quel conteneur en quelques secondes.

Le provider Docker remplace le fichier de configuration dynamique : Traefik interroge le daemon, lit les labels portés par chaque conteneur et en déduit les routers, services et middlewares correspondants. La configuration de routage vit donc à côté de l'application qu'elle expose, dans le même docker-compose.yml. Cette partie détaille les trois éléments du mécanisme : la découverte, l'accès au socket Docker et la grammaire des labels.

Quand le provider Docker est activé, Traefik :

  1. Se connecte au daemon Docker via le socket Unix
  2. Surveille les événements (création, suppression, modification de conteneurs)
  3. Lit les labels de chaque conteneur
  4. Génère la configuration de routage correspondante
  5. Applique immédiatement les changements (hot-reload)

Provider Docker : labels, socket et génération de configuration

Traefik communique avec Docker via le socket Unix (/var/run/docker.sock). Ce socket donne accès à toute l'API Docker, ce qui pose un risque de sécurité.

Les labels Docker sont des métadonnées clé-valeur attachées aux conteneurs. Traefik les utilise pour configurer :

  • Les routers : règles de routage (Host, Path...)
  • Les services : port du conteneur, load balancing
  • Les middlewares : authentification, headers, rate limiting

Ces réglages appartiennent à la configuration statique, celle que Traefik lit une seule fois : toute modification impose un redémarrage du conteneur Traefik, contrairement aux labels pris en compte à chaud. Deux options structurent tout le reste, l'endpoint qui désigne le daemon à interroger et exposedByDefault qui décide du sort d'un conteneur sans label.

Dans votre fichier traefik.yaml (configuration statique) :

traefik.yaml
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
OptionDescriptionRecommandation
endpointChemin du socket DockerToujours unix:///var/run/docker.sock sur Linux
exposedByDefaultExpose automatiquement tous les conteneursfalse (sécurité)

network évite de répéter traefik.docker.network sur chaque conteneur, et constraints restreint les conteneurs considérés, ce qui permet de faire cohabiter plusieurs instances de Traefik sur un même hôte.

providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: traefik-public # Réseau par défaut
watch: true # Surveiller les changements
defaultRule: "Host(`{{ normalize .Name }}.example.com`)" # Règle par défaut
constraints: "Label(`traefik.zone`, `public`)" # Filtrer les conteneurs
OptionDescription
networkRéseau Docker à utiliser pour la communication
watchSurveiller les événements Docker en temps réel
defaultRuleRègle de routage par défaut (utilise les templates Go)
constraintsExpression pour filtrer les conteneurs à considérer

L'exemple monte deux conteneurs sur un réseau commun : Traefik, qui écoute sur le port 80 et lit le socket, et whoami, qui renvoie les en-têtes de la requête reçue. Cette seconde image sert de sonde : la présence des en-têtes X-Forwarded-* dans sa réponse prouve que la requête est bien passée par le proxy, et non directement par le conteneur.

  1. Créer le fichier docker-compose.yml

    docker-compose.yml
    services:
    traefik:
    image: traefik:v3.7.6
    container_name: traefik
    restart: unless-stopped
    command:
    - "--api.dashboard=true"
    - "--api.insecure=true"
    - "--providers.docker=true"
    - "--providers.docker.exposedbydefault=false"
    - "--entrypoints.web.address=:80"
    ports:
    - "80:80"
    - "8080:8080"
    volumes:
    - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
    - traefik-public
    whoami:
    image: traefik/whoami:v1.11.0
    container_name: whoami
    labels:
    - "traefik.enable=true"
    - "traefik.http.routers.whoami.rule=Host(`whoami.localhost`)"
    - "traefik.http.routers.whoami.entrypoints=web"
    networks:
    - traefik-public
    networks:
    traefik-public:
    name: traefik-public
  2. Lancer les conteneurs

    Fenêtre de terminal
    docker compose up -d
    # Vérifier le statut
    docker compose ps

    Résultat attendu :

    NAME IMAGE STATUS PORTS
    traefik traefik:v3.7.6 running 0.0.0.0:80->80/tcp, 0.0.0.0:8080->8080/tcp
    whoami traefik/whoami running
  3. Tester le routage

    Fenêtre de terminal
    # Tester avec le header Host
    curl -H "Host: whoami.localhost" http://localhost

    Résultat attendu :

    Hostname: whoami
    IP: 172.19.0.3
    RemoteAddr: 172.19.0.2:56842
    GET / HTTP/1.1
    Host: whoami.localhost
    User-Agent: curl/8.5.0
    Accept: */*
    Accept-Encoding: gzip
    X-Forwarded-For: 172.19.0.1
    X-Forwarded-Host: whoami.localhost
    X-Forwarded-Port: 80
    X-Forwarded-Proto: http
    X-Forwarded-Server: traefik
    X-Real-Ip: 172.19.0.1

Tous les labels suivent le même schéma : traefik.<protocole>.<type>.<nom>.<option>. Le nom est libre et ne sert qu'à relier entre eux les labels d'un même router ou d'un même service ; deux conteneurs qui réutilisent le même nom entrent en conflit. Les sections ci-dessous couvrent le routage de base, le TLS, les middlewares et les entrées TCP/UDP.

Le label traefik.enable conditionne tous les autres tant que exposedByDefault vaut false. Le port n'est à préciser que si le conteneur en expose plusieurs, ou aucun.

LabelDescriptionExemple
traefik.enableActiver l'exposition du conteneurtrue ou false
traefik.http.routers.<n>.ruleRègle de routage HTTPHost('app.example.com')
traefik.http.routers.<n>.entrypointsPoints d'entrée à utiliserweb,websecure
traefik.http.routers.<n>.priorityPriorité du router (plus élevé = prioritaire)100
traefik.http.services.<n>.loadbalancer.server.portPort du conteneur8080

Note : Remplacez <n> par le nom de votre router/service (ex: myapp, api, frontend).

tls=true suffit à servir en HTTPS avec un certificat déjà chargé ; ajouter certresolver déclenche la demande automatique auprès de l'autorité déclarée dans la configuration statique. Les entrées domains[0] ne servent que pour un certificat wildcard ou multi-domaines.

labels:
# Activer TLS
- "traefik.http.routers.myapp.tls=true"
# Utiliser un certificat resolver (Let's Encrypt)
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
# Domaines pour le certificat
- "traefik.http.routers.myapp.tls.domains[0].main=example.com"
- "traefik.http.routers.myapp.tls.domains[0].sans=www.example.com,api.example.com"

Deux familles de labels cohabitent ici : ceux qui définissent un middleware et celui qui l'attache à un router. Un middleware défini mais jamais attaché reste sans effet, et l'ordre de la liste correspond à l'ordre d'exécution sur la requête.

labels:
# Attacher un ou plusieurs middlewares
- "traefik.http.routers.myapp.middlewares=auth,compress,headers"
# Définir un middleware BasicAuth
- "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$xxx"
# Définir un middleware Headers
- "traefik.http.middlewares.headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.headers.headers.contentTypeNosniff=true"
# Définir un RateLimit
- "traefik.http.middlewares.ratelimit.ratelimit.average=100"
- "traefik.http.middlewares.ratelimit.ratelimit.burst=50"

Pour les services TCP ou UDP (bases de données, DNS...) :

labels:
- "traefik.tcp.routers.mysql.rule=HostSNI(`*`)"
- "traefik.tcp.routers.mysql.entrypoints=mysql"
- "traefik.tcp.services.mysql.loadbalancer.server.port=3306"

La communication entre Traefik et un conteneur passe par le réseau Docker, pas par les ports publiés sur l'hôte : un conteneur sans section ports: reste parfaitement joignable par le proxy. Encore faut-il que les deux partagent un réseau. C'est le rôle du réseau externe décrit ici, qui survit aux docker compose down et laisse la possibilité d'isoler les bases de données sur un second réseau privé.

Par défaut, Docker crée un réseau bridge pour chaque projet Compose. Problème : Traefik ne peut pas atteindre les conteneurs d'autres projets.

Solution : Créer un réseau Docker externe partagé entre Traefik et tous les services à exposer.

┌─────────────────────────────────────────────────────────────────────────┐
│ Réseau traefik-public │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Traefik │ │ App A │ │ App B │ │ App C │ │
│ │ │──│──────────│──│──────────│──│──────────│ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────────────┘

Le réseau doit exister avant le premier docker compose up, d'où sa création manuelle. La mention external: true indique à Compose de le réutiliser au lieu d'en créer un nouveau préfixé par le nom du projet.

  1. Créer le réseau externe :

    Fenêtre de terminal
    docker network create traefik-public
  2. Référencer le réseau dans docker-compose.yml :

    docker-compose.yml
    services:
    myapp:
    image: nginx
    networks:
    - traefik-public
    - internal # Réseau interne pour base de données
    networks:
    traefik-public:
    external: true
    internal:
    driver: bridge

Un conteneur peut appartenir à plusieurs réseaux. Utilisez cette technique pour séparer :

  • traefik-public : communication avec Traefik
  • internal : communication entre services (base de données, cache)
services:
wordpress:
image: wordpress
networks:
- traefik-public # Accessible par Traefik
- backend # Accès à MySQL
labels:
- "traefik.http.routers.wp.rule=Host(`blog.example.com`)"
- "traefik.docker.network=traefik-public" # Préciser le réseau Traefik
mysql:
image: mysql:8
networks:
- backend # PAS sur traefik-public = pas accessible depuis Internet

Les quatre montages ci-dessous sont complets et fonctionnent tels quels, à deux conditions : que le réseau traefik-public existe déjà et qu'un certresolver nommé letsencrypt soit déclaré dans la configuration statique de Traefik. Ils traitent des problèmes distincts : exposition simple, isolation d'une base de données, répartition sur plusieurs réplicas, et partage d'un même domaine entre deux services.

Le conteneur ne publie aucun port sur l'hôte : seul Traefik l'atteint, à travers le réseau partagé.

services:
nginx:
image: nginx:1.31.3-alpine
container_name: web
volumes:
- ./html:/usr/share/nginx/html:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.web.rule=Host(`www.example.com`)"
- "traefik.http.routers.web.entrypoints=websecure"
- "traefik.http.routers.web.tls.certresolver=letsencrypt"
networks:
- traefik-public
networks:
traefik-public:
external: true

MySQL n'est attaché qu'au réseau backend, il reste donc inaccessible depuis Traefik comme depuis Internet. WordPress appartenant à deux réseaux, le label traefik.docker.network lève l'ambiguïté sur celui que le proxy doit emprunter.

services:
wordpress:
image: wordpress:6
container_name: wordpress
environment:
WORDPRESS_DB_HOST: mysql
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DB_USER: wp_user
WORDPRESS_DB_PASSWORD: ${WP_DB_PASSWORD}
volumes:
- wordpress_data:/var/www/html
labels:
- "traefik.enable=true"
- "traefik.http.routers.wordpress.rule=Host(`blog.example.com`)"
- "traefik.http.routers.wordpress.entrypoints=websecure"
- "traefik.http.routers.wordpress.tls.certresolver=letsencrypt"
- "traefik.docker.network=traefik-public"
networks:
- traefik-public
- backend
mysql:
image: mysql:8
container_name: wordpress-db
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wp_user
MYSQL_PASSWORD: ${WP_DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
networks:
- backend # Pas exposé sur Internet
networks:
traefik-public:
external: true
backend:
driver: bridge
volumes:
wordpress_data:
mysql_data:

Traefik répartit les requêtes entre les réplicas dès lors qu'ils portent les mêmes labels de service, sans configuration supplémentaire. Le healthcheck est ce qui retire un réplica défaillant de la rotation au lieu de continuer à lui adresser du trafic.

services:
api:
image: myapi:latest
deploy:
replicas: 3
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`api.example.com`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.tls.certresolver=letsencrypt"
- "traefik.http.services.api.loadbalancer.server.port=3000"
# Healthcheck pour le load balancing
- "traefik.http.services.api.loadbalancer.healthcheck.path=/health"
- "traefik.http.services.api.loadbalancer.healthcheck.interval=10s"
networks:
- traefik-public

Les deux routers partagent le même Host et se départagent par le PathPrefix, Traefik retenant la règle la plus spécifique. Le middleware stripprefix retire /api avant transmission, l'API recevant ainsi ses routes à la racine.

services:
frontend:
image: myfrontend:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.frontend.rule=Host(`app.example.com`)"
- "traefik.http.routers.frontend.entrypoints=websecure"
- "traefik.http.routers.frontend.tls.certresolver=letsencrypt"
- "traefik.http.services.frontend.loadbalancer.server.port=80"
networks:
- traefik-public
api:
image: myapi:latest
labels:
- "traefik.enable=true"
# Même domaine, path différent
- "traefik.http.routers.api.rule=Host(`app.example.com`) && PathPrefix(`/api`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.tls.certresolver=letsencrypt"
- "traefik.http.services.api.loadbalancer.server.port=3000"
# Middleware pour supprimer le préfixe /api
- "traefik.http.routers.api.middlewares=api-stripprefix"
- "traefik.http.middlewares.api-stripprefix.stripprefix.prefixes=/api"
networks:
- traefik-public

Le mode Swarm change l'unité observée : Traefik ne suit plus des conteneurs mais des services, dont les labels se déclarent sous deploy.labels. Un label placé au mauvais niveau est simplement ignoré, sans message d'erreur, ce qui rend le symptôme difficile à interpréter. Le provider s'appelle swarm depuis la v3 et le réseau partagé doit être de type overlay.

Ces cinq écarts expliquent la quasi-totalité des configurations qui fonctionnent en local puis restent invisibles une fois déployées en Swarm.

AspectDocker standaloneDocker Swarm
Providerdockerswarm
UnitéConteneurService
LabelsSur le conteneurDans deploy.labels
RéseauBridge ou overlayOverlay obligatoire
Scalingdocker compose up --scaledocker service scale

Le bloc remplace le provider docker par swarm. L'option network devient ici pratiquement indispensable : les tâches sont raccordées à un réseau overlay que Traefik doit connaître pour les joindre.

traefik.yaml
providers:
swarm:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: traefik-public
watch: true

En Swarm, les labels doivent être dans la section deploy.labels :

services:
myapp:
image: myapp:latest
deploy:
replicas: 3
labels:
- "traefik.enable=true"
- "traefik.http.routers.myapp.rule=Host(`app.example.com`)"
- "traefik.http.services.myapp.loadbalancer.server.port=8080"
networks:
- traefik-public

L'option --attachable sur le réseau overlay autorise le raccordement de conteneurs lancés hors Swarm, ce qui est utile pendant une migration progressive.

Fenêtre de terminal
# Initialiser Swarm (si pas fait)
docker swarm init
# Créer le réseau overlay
docker network create --driver=overlay --attachable traefik-public
# Déployer Traefik en mode global
docker stack deploy -c traefik-stack.yml traefik
# Déployer l'application
docker stack deploy -c myapp-stack.yml myapp

Le socket Docker est le seul moyen pour Traefik de découvrir les conteneurs, et c'est aussi le point faible de l'architecture : quiconque le lit et l'écrit dispose des droits root sur l'hôte. Le drapeau :ro porte sur le fichier monté, pas sur les requêtes qui transitent par la socket : il ne bloque donc aucun endpoint de l'API. Les deux approches ci-dessous correspondent à deux niveaux d'exposition, l'un tolérable en développement, l'autre attendu en production.

En développement, monter le socket directement est acceptable :

volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro

Le :ro (read-only) est une protection minimale, mais Traefik peut toujours lire toutes les informations Docker.

Le proxy de socket s'intercale entre Traefik et le daemon : il n'expose qu'un sous-ensemble de l'API Docker en lecture, sur une socket TCP interne à un réseau dédié. Traefik perd alors tout accès aux endpoints permettant de créer un conteneur, d'exécuter une commande ou de monter un volume, ce qui borne les conséquences d'une compromission.

En production, utilisez docker-socket-proxy pour limiter les endpoints API accessibles :

services:
socket-proxy:
image: tecnativa/docker-socket-proxy:v0.4.2
container_name: socket-proxy
restart: unless-stopped
environment:
# Endpoints autorisés (lecture seule)
CONTAINERS: 1
SERVICES: 1
NETWORKS: 1
TASKS: 1
# Endpoints bloqués
BUILD: 0
COMMIT: 0
CONFIGS: 0
DISTRIBUTION: 0
EXEC: 0
IMAGES: 0
INFO: 0
NODES: 0
PLUGINS: 0
SECRETS: 0
SWARM: 0
SYSTEM: 0
VOLUMES: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- socket-proxy
traefik:
image: traefik:v3.7.6
depends_on:
- socket-proxy
command:
- "--providers.docker.endpoint=tcp://socket-proxy:2375"
# ...autres options
networks:
- socket-proxy
- traefik-public
networks:
socket-proxy:
driver: bridge
traefik-public:
external: true

Quand plusieurs conteneurs portent les mêmes labels de service, Traefik les traite comme un pool unique et répartit le trafic entre eux. Sans healthcheck, une instance encore en cours de démarrage ou déjà plantée continue de recevoir des requêtes : c'est ce réglage qui sépare une mise à jour transparente d'une série de 502 côté utilisateur.

Traefik vérifie régulièrement la santé des backends :

labels:
# Healthcheck HTTP
- "traefik.http.services.api.loadbalancer.healthcheck.path=/health"
- "traefik.http.services.api.loadbalancer.healthcheck.port=8080"
- "traefik.http.services.api.loadbalancer.healthcheck.interval=10s"
- "traefik.http.services.api.loadbalancer.healthcheck.timeout=3s"
OptionDescriptionValeur par défaut
pathChemin à vérifier/
portPort à vérifierPort du service
intervalIntervalle entre vérifications30s
timeoutTimeout de la vérification5s

Les sticky sessions renvoient un client vers le même backend grâce à un cookie ajouté par Traefik. À réserver aux applications qui conservent un état en mémoire, car elles neutralisent en partie l'équilibrage.

labels:
# Round Robin (défaut)
- "traefik.http.services.api.loadbalancer.strategy=round-robin"
# Sticky sessions (affinité)
- "traefik.http.services.api.loadbalancer.sticky.cookie=true"
- "traefik.http.services.api.loadbalancer.sticky.cookie.name=server_id"
- "traefik.http.services.api.loadbalancer.sticky.cookie.secure=true"

Trois symptômes couvrent l'essentiel des blocages avec le provider Docker, et ils partagent une même origine : la configuration attendue n'a jamais été générée. Le dashboard et l'API de Traefik affichent les routers réellement chargés, ce qui permet de distinguer un label ignoré d'un problème de réseau entre le proxy et le conteneur.

Symptômes : Le conteneur ne s'affiche pas dans le dashboard Traefik.

Vérifications :

  1. Le label traefik.enable=true est présent ?

    Fenêtre de terminal
    docker inspect mycontainer | jq '.[0].Config.Labels'
  2. Le conteneur est sur le bon réseau ?

    Fenêtre de terminal
    docker network inspect traefik-public
  3. exposedByDefault est-il à true ou false ?

Symptômes : Erreur 502 ou 504 après plusieurs secondes.

Causes possibles :

  1. Port incorrect : Vérifiez le label loadbalancer.server.port

    - "traefik.http.services.myapp.loadbalancer.server.port=8080"
  2. Réseau différent : Traefik et le conteneur ne sont pas sur le même réseau

    - "traefik.docker.network=traefik-public"
  3. Application non démarrée : Vérifiez les logs du conteneur

    Fenêtre de terminal
    docker logs mycontainer

Symptômes : "router already exists" dans les logs Traefik.

Solution : Utilisez des noms uniques pour chaque router et service :

# Mauvais : même nom "app" pour deux services
- "traefik.http.routers.app.rule=..." # Conflit !
# Bon : noms uniques
- "traefik.http.routers.frontend-app.rule=..."
- "traefik.http.routers.api-app.rule=..."

L'API sur le port 8080 renvoie les routers et services effectivement chargés : si le vôtre n'y figure pas, le problème vient des labels et non du routage. Le niveau DEBUG journalise en plus chaque événement Docker reçu par le provider.

Fenêtre de terminal
# Logs Traefik
docker logs traefik -f
# Niveau debug
docker run traefik:v3.7.6 --log.level=DEBUG
# Vérifier les routers via API
curl http://localhost:8080/api/http/routers | jq
# Vérifier les services
curl http://localhost:8080/api/http/services | jq

Ces huit points regroupent les décisions qui rendent une configuration Traefik + Docker reproductible : périmètre d'exposition, topologie réseau, accès au socket et nommage des routers.

  1. Labels = Configuration : Les labels Docker définissent le routage sans fichier de configuration externe

  2. exposedByDefault: false : Toujours désactiver pour éviter d'exposer des conteneurs par accident

  3. Réseau dédié : Créez un réseau traefik-public externe et attachez-y tous les services à exposer

  4. traefik.docker.network : Obligatoire quand un conteneur est sur plusieurs réseaux

  5. Socket proxy en production : Utilisez docker-socket-proxy pour sécuriser l'accès au daemon Docker

  6. Noms uniques : Chaque router et service doit avoir un nom unique pour éviter les conflits

  7. Healthchecks : Configurez-les pour retirer automatiquement les backends en échec

  8. Swarm : Utilisez le provider swarm avec labels dans deploy.labels

Bientôt : guides sur les middlewares de sécurité, la configuration TLS avancée, et l'observabilité avec Traefik.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn