Pourquoi un reverse proxy ?
Sans reverse proxy, chaque service écoute sur un port différent : Grafana sur :3000, Immich sur :2283, n8n sur :5678, Authentik sur :9000… On se retrouve à retenir des combinaisons IP:port pour chaque service.
Avec plus de 20 services, c’est ingérable. Sans parler de la sécurité : pas de HTTPS, pas de headers de sécurité, pas de centralisation des accès.
Un reverse proxy résout tout ça :
- Un seul point d’entrée — ports 80 et 443 uniquement
- Routage par nom de domaine —
grafana.mondomaine.ovh→ conteneur Grafana - HTTPS automatique avec Let’s Encrypt
- Middlewares centralisés — SSO, CrowdSec, headers de sécurité
- Métriques et access logs — tout le trafic HTTP est observable
Pourquoi Traefik v3 ?
| Critère | Traefik | Nginx Proxy Manager | Caddy |
|---|---|---|---|
| Découverte de services Docker | Automatique (labels) | Semi-auto (UI) | Plugin |
| Certificats Let’s Encrypt | Intégré (HTTP + DNS challenge) | Intégré | Intégré |
| Rechargement à chaud | Automatique | Via UI | Automatique |
| Configuration as Code | Labels Docker + YAML | Base SQLite | Caddyfile |
| Plugins/Middlewares | Écosystème riche | Limité | Riche |
| Métriques Prometheus | Natif | Non | Natif |
| SSO Forward Auth | Natif | Non | Non |
Pour un homelab Docker où tout est géré par Compose, Traefik est le choix naturel. La découverte automatique via les labels élimine toute configuration manuelle : au docker compose up, le service est automatiquement exposé, routé et sécurisé.
Traefik v3 (sorti mi-2024) apporte le support natif du HTTP/3, un moteur de plugins amélioré, et une configuration simplifiée par rapport à v2.
Architecture
Internet ─→ Box (port 80/443) ─→ Traefik (docker-infra)
│
┌───────────┴───────────┐
│ │
Entrypoint web (80) Entrypoint websecure (443)
│ │
Redirect → HTTPS ┌──────┴──────┐
│ │
[CrowdSec] [TLS + cert LE]
│ │
└──────┬───────┘
│
┌────────────┼─────────────┐
│ │ │
[SSO Auth] [Direct] [Custom MW]
│ │ │
Grafana Immich Prometheus
Headscale N8N (auth+sec)
pgAdmin ...
Traefik est le premier point de contact pour tout le trafic HTTP(S). Le flux :
- La requête arrive sur le port 443
- CrowdSec vérifie que l’IP source n’est pas bannie
- TLS est terminé avec un certificat Let’s Encrypt
- Le router matche le
Hostheader vers le bon service - Les middlewares s’appliquent (SSO, headers, rate limiting…)
- Le trafic est forwardé vers le conteneur backend
Configuration de base
Docker Compose
services:
traefik:
image: traefik:v3.6
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./dynamic:/etc/traefik/dynamic:ro
- letsencrypt:/letsencrypt
- /var/log/traefik:/logs
networks:
proxy:
ipv4_address: 172.18.0.2
security_opt:
- no-new-privileges
dns:
- 192.168.1.54
- 192.168.1.8
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
proxy:
external: true
Points importants :
- IP fixe
172.18.0.2— les autres services référencent Traefik par cette IP no-new-privileges— mesure de sécurité Docker, empêche l’escalade de privilèges- Socket Docker en lecture seule — Traefik a besoin de lire les labels des conteneurs, mais pas de les modifier
- DNS explicite — pointe vers AdGuard Home pour le split-horizon
Configuration statique (traefik.yml)
# Entrypoints
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt
middlewares:
- crowdsec@file # CrowdSec bouncer global
# Certificats Let's Encrypt
certificatesResolvers:
letsencrypt:
acme:
email: admin@mondomaine.ovh
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
letsencrypt-dns:
acme:
email: admin@mondomaine.ovh
storage: /letsencrypt/acme-dns.json
dnsChallenge:
provider: ovh
delayBeforeCheck: 120
resolvers:
- "1.1.1.1:53"
# Providers
providers:
docker:
exposedByDefault: false
network: proxy
file:
directory: /etc/traefik/dynamic
watch: true
# Access logs (JSON pour parsing par Promtail/CrowdSec)
accessLog:
filePath: /logs/access.log
format: json
bufferingSize: 100
# Métriques Prometheus
metrics:
prometheus:
addEntryPointsLabels: true
addServicesLabels: true
addRoutersLabels: true
# Plugins
experimental:
plugins:
crowdsec-bouncer-traefik-plugin:
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
version: v1.4.7
Deux cert resolvers : HTTP et DNS
J’utilise deux resolvers Let’s Encrypt différents selon le besoin :
| Resolver | Challenge | Usage |
|---|---|---|
letsencrypt | HTTP-01 (port 80) | La plupart des services (grafana.mondomaine.ovh, etc.) |
letsencrypt-dns | DNS-01 (API OVH) | Certificats wildcard (*.s3.mondomaine.ovh) |
Le challenge DNS est nécessaire pour les certificats wildcard car Let’s Encrypt ne peut pas vérifier un wildcard via HTTP. Le delayBeforeCheck: 120 laisse 2 minutes à la propagation DNS OVH avant la vérification.
Exposer un service
C’est la magie de Traefik : pour exposer un nouveau service, il suffit d’ajouter des labels dans son compose.yml :
services:
grafana:
image: grafana/grafana:11.4.0
labels:
- traefik.enable=true
- traefik.http.routers.grafana.rule=Host(`grafana.mondomaine.ovh`)
- traefik.http.routers.grafana.entrypoints=websecure
- traefik.http.routers.grafana.tls=true
- traefik.http.routers.grafana.tls.certresolver=letsencrypt
- traefik.http.services.grafana.loadbalancer.server.port=3000
networks:
- proxy
Au docker compose up :
- Traefik détecte le nouveau conteneur via le socket Docker
- Il crée un router pour
grafana.mondomaine.ovh - Il demande un certificat Let’s Encrypt via le HTTP challenge
- Le certificat est stocké dans
acme.jsonet renouvelé automatiquement (avant expiration) - Le trafic HTTPS est routé vers le port 3000 du conteneur
Zéro intervention manuelle. Ajouter un service revient à écrire 5 lignes de labels.
Middlewares
Les middlewares sont des traitements appliqués aux requêtes avant qu’elles n’atteignent le backend. Traefik supporte le chaînage : on peut appliquer plusieurs middlewares en séquence.
1. CrowdSec Bouncer (global)
Appliqué sur toutes les requêtes HTTPS via l’entrypoint :
# dynamic/crowdsec.yml
http:
middlewares:
crowdsec:
plugin:
crowdsec-bouncer-traefik-plugin:
crowdsecLapiScheme: http
crowdsecLapiHost: "172.18.0.100:8080"
crowdsecMode: live
updateIntervalSeconds: 60
defaultDecisionSeconds: 60
Le bouncer interroge le LAPI CrowdSec pour chaque requête. Avec le cache de 60 secondes, les vérifications se font en < 1 ms en mémoire.
2. Headers de sécurité
# dynamic/security-headers.yml
http:
middlewares:
secure-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
forceSTSHeader: true
contentTypeNosniff: true
browserXssFilter: true
frameDeny: true
referrerPolicy: strict-origin-when-cross-origin
permissionsPolicy: "camera=(), microphone=(), geolocation=()"
Ces headers protègent contre :
- HSTS : force HTTPS, empêche le downgrade vers HTTP. Le
preloadpermet l’inclusion dans la liste HSTS des navigateurs - X-Content-Type-Options: nosniff : empêche le MIME-type sniffing
- X-Frame-Options: DENY : bloque le clickjacking (inclusion dans un iframe)
- Referrer-Policy : contrôle les informations de referrer envoyées aux sites tiers
- Permissions-Policy : désactive caméra, micro, géoloc par défaut
3. Authentik SSO (Forward Auth)
Le middleware le plus puissant : Forward Auth avec Authentik (Identity Provider / SSO). Il permet d’ajouter une couche d’authentification devant n’importe quel service, même ceux qui n’ont pas de système de login.
# dynamic/authentik.yml
http:
middlewares:
authentik:
forwardAuth:
address: http://172.18.0.10:9000/outpost.goauthentik.io/auth/traefik
trustForwardHeader: true
authResponseHeaders:
- X-authentik-username
- X-authentik-groups
- X-authentik-email
- X-authentik-name
- X-authentik-uid
- X-authentik-jwt
Le fonctionnement :
- La requête arrive sur Traefik
- Traefik envoie une sous-requête à Authentik (
/outpost.goauthentik.io/auth/traefik) - Si l’utilisateur est authentifié → Authentik retourne 200, la requête est forwardée au backend avec les headers
X-authentik-* - Si l’utilisateur n’est pas authentifié → Authentik retourne 401, Traefik redirige vers la page de login Authentik
Pour l’utilisateur, l’expérience est transparente : au premier accès, il est redirigé vers le portail SSO, s’authentifie une fois, et tous les services protégés sont accessibles sans re-login (session SSO partagée).
Application du middleware SSO
# Exemple : protéger Prometheus avec SSO
labels:
- traefik.http.routers.prometheus.middlewares=authentik@file,secure-headers@file
Services protégés par Forward Auth dans mon homelab :
| Service | Middleware | Raison |
|---|---|---|
| Traefik Dashboard | authentik@file | Interface d’admin sensible |
| Prometheus | authentik@file | Métriques internes |
| Headscale UI | authentik@file | Gestion VPN |
| N8N | authentik@file | Workflows automatisés |
| Stirling PDF | authentik@file | Outil interne |
| pgAdmin | authentik@file | Accès bases de données |
| CloudBeaver | authentik@file | Accès bases de données |
Certains services utilisent OIDC natif (pas de forward auth) car ils ont un système d’authentification intégré :
| Service | Auth | Raison |
|---|---|---|
| Grafana | OIDC natif | Role mapping automatique (Admin/Viewer) |
| Open WebUI | OIDC natif | Gestion fine des utilisateurs |
| Immich | OAuth2 natif | Apps mobiles |
Pourquoi Forward Auth vs OIDC natif ?
- Forward Auth : idéal pour protéger des services qui n’ont pas de système d’auth, ou dont l’auth est basique. Ajouté en une ligne de label
- OIDC natif : idéal quand le service a besoin de connaître l’utilisateur (mapping de rôles, profils, etc.)
Redirections
Un middleware utile : la redirection du domaine apex vers le www :
# Sur le service blog
labels:
- traefik.http.routers.www-redirect.rule=Host(`mondomaine.ovh`)
- traefik.http.routers.www-redirect.middlewares=www-redirect@docker
- traefik.http.middlewares.www-redirect.redirectregex.regex=^https://mondomaine.ovh/(.*)
- traefik.http.middlewares.www-redirect.redirectregex.replacement=https://www.mondomaine.ovh/$${1}
- traefik.http.middlewares.www-redirect.redirectregex.permanent=true
Résultat : https://mondomaine.ovh/article → 301 → https://www.mondomaine.ovh/article
Configuration dynamique par fichiers
En plus des labels Docker, Traefik supporte la configuration par fichiers YAML dans le dossier dynamic/. C’est utile pour :
- Les services qui ne sont pas dans Docker (Proxmox, serveurs baremetal)
- Les middlewares réutilisables (SSO, headers, CrowdSec)
- Les routes complexes
Mon dossier dynamic/ contient 14 fichiers de configuration pour les routes vers des services externes :
dynamic/
├── adguard.yml # Interface AdGuard (LXC)
├── authentik.yml # Middleware SSO
├── crowdsec.yml # Middleware bouncer
├── garage.yml # API S3 (LXC)
├── headscale.yml # Headscale (LXC)
├── homeassistant.yml # Home Assistant (VM)
├── immich.yml # Config spécifique Immich
├── proxmox.yml # Interface Proxmox (hôte)
├── redirect.yml # Redirections globales
├── security-headers.yml # Headers de sécurité
└── ...
Exemple pour Proxmox (pas dans Docker) :
# dynamic/proxmox.yml
http:
routers:
proxmox:
rule: Host(`proxmox.mondomaine.ovh`)
entryPoints: [websecure]
tls:
certResolver: letsencrypt
service: proxmox
middlewares:
- authentik@file
- secure-headers@file
services:
proxmox:
loadBalancer:
servers:
- url: https://192.168.1.72:8006
serversTransport: proxmox-transport
serversTransports:
proxmox-transport:
insecureSkipVerify: true # cert auto-signé Proxmox
Monitoring
Traefik expose un endpoint /metrics au format Prometheus avec des métriques riches :
# prometheus.yml
- job_name: traefik
static_configs:
- targets: ['traefik.:8080']
Métriques disponibles :
| Métrique | Description |
|---|---|
traefik_entrypoint_requests_total | Requêtes par entrypoint, code HTTP |
traefik_router_requests_total | Requêtes par router (= par service) |
traefik_service_request_duration_seconds | Temps de réponse par service |
traefik_tls_certs_not_after | Expiration des certificats |
Le dashboard Grafana dédié montre en temps réel :
- Le nombre de requêtes par service (qui est populaire, qui a des erreurs)
- Les temps de réponse par service (détecter les ralentissements)
- La répartition des codes HTTP (2xx, 4xx, 5xx)
- Les dates d’expiration des certificats (anticipation des problèmes de renouvellement)
Access logs pour l’observabilité et la sécurité
Les access logs sont en format JSON, ce qui facilite le parsing par Promtail et CrowdSec :
{
"ClientAddr": "82.x.x.x:54321",
"RequestMethod": "GET",
"RequestPath": "/api/v1/data",
"OriginStatus": 200,
"Duration": 12345678,
"RouterName": "grafana@docker",
"ServiceName": "grafana@docker"
}
Ces logs alimentent :
- Loki (via Promtail) pour la recherche et les dashboards
- CrowdSec pour la détection d’attaques
- Prometheus pour les métriques agrégées
Avec le bufferingSize: 100, les logs sont écrits par paquets de 100 pour réduire les I/O disque.
Pièges courants et solutions
1. Service inaccessible : “Bad Gateway 502”
Cause habituelle : le service et Traefik ne sont pas sur le même réseau Docker.
# ❌ Service sur un réseau différent
networks:
- default # réseau interne de la stack
# ✅ Service sur le réseau proxy
networks:
- proxy
- default # garder aussi le réseau interne si besoin
2. Port du loadbalancer incorrect
Si le service écoute sur un port non standard (pas 80), il faut le spécifier :
# ❌ Traefik essaie le port 80 par défaut
labels:
- traefik.enable=true
# ✅ Spécifier le port du service
labels:
- traefik.http.services.mon-service.loadbalancer.server.port=8080
3. Permissions acme.json
Le fichier de stockage des certificats doit avoir les permissions 600 :
chmod 600 /path/to/acme.json
Traefik refuse de démarrer si les permissions sont trop ouvertes — c’est une protection contre la fuite des clés privées des certificats.
4. WebSocket
Certains services (MeshCentral, Authentik, Home Assistant) utilisent WebSocket. Traefik les supporte nativement, mais il faut parfois ajouter des sticky sessions :
labels:
- traefik.http.services.meshcentral.loadbalancer.sticky.cookie=true
- traefik.http.services.meshcentral.loadbalancer.sticky.cookie.name=meshcentral_sticky
5. Search domain Docker (le piège DNS)
Un classique avec le split-horizon DNS : si la VM Docker a un search domain configuré, les noms de conteneurs sont résolus via AdGuard au lieu du DNS Docker interne. Résultat : 502 sur tous les services.
Solution : search: [] dans la configuration netplan de la VM Docker. (J’en parle en détail dans l’article sur le DNS split-horizon.)
Le réseau proxy : colonne vertébrale de l’architecture
Le réseau Docker proxy est le lien entre Traefik et tous les services exposés. Il est créé manuellement (externe) et chaque stack s’y connecte :
docker network create proxy --subnet 172.18.0.0/16
Chaque service a une IP fixe sur ce réseau. Ça permet :
- Des références stables dans les configurations (pas de dépendance au DNS Docker)
- Le forward auth Authentik via IP (
172.18.0.10) plutôt que par nom - CrowdSec bouncer connecté au LAPI via IP fixe (
172.18.0.100)
Conclusion
Traefik v3 est la colonne vertébrale de mon homelab. Une fois la configuration initiale en place :
- Chaque nouveau service est exposé en 5 lignes de labels
- Les certificats Let’s Encrypt sont automatiques et renouvelés sans intervention
- Le SSO est ajouté en 1 ligne (
middlewares=authentik@file) - La sécurité est globale : CrowdSec, headers, HSTS sur tout le trafic
- L’observabilité est complète : métriques Prometheus, access logs JSON, dashboards Grafana
C’est un investissement d’une demi-journée de configuration qui fait gagner des heures à chaque nouveau service déployé.