Path : infrastructure/traefik
Version : Traefik v3
Serveur : lorva-public (/docker/traefik/)
Traefik est le reverse proxy central de l'infra LORVA. Il tourne en conteneur Docker sur lorva-public et expose tous les services publics sous *.lorva.dev ainsi que taskori.app / *.taskori.app.
Rôles principaux :
Règle importante : Traefik gère intégralement les certificats via l'API Cloudflare (DNS-01). Le proxy Cloudflare (orange cloud) est actif sur tous les domaines — le challenge HTTP-01 est incompatible avec cette configuration et ne doit jamais être utilisé.
/docker/traefik/
├── docker-compose.yml # Définition du conteneur Traefik
├── traefik.yml # Config statique (entrypoints, providers, certresolver)
├── acme.json # Certificats Let's Encrypt (chmod 600)
└── config/ # Configs dynamiques (file provider)
├── config-sync.yml
├── dockhand.yml
├── fw.yml
├── gitlab.yml
├── netbox.yml
├── pve.yml
├── registry.yml
├── taskori.yml
└── transports.yml
traefik.ymlLa configuration statique est chargée au démarrage. Tout changement nécessite un redémarrage du conteneur.
api:
dashboard: true
insecure: false # Dashboard exposé via router sécurisé, pas en clair
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt
certificatesResolvers:
letsencrypt:
acme:
email: [email protected]
storage: /acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
providers:
docker:
exposedByDefault: false
network: traefik-public
file:
directory: /config
watch: true
log:
level: INFO
# forwardedHeaders : configurer ici si nécessaire (voir section dédiée)
# entryPoints.websecure.forwardedHeaders.trustedIPs: [...]
exposedByDefault: false— Chaque conteneur doit explicitement opt-in avec le labeltraefik.enable=true.
traefik-publicTraefik utilise un réseau Docker externe nommé traefik-public. Tout conteneur que Traefik doit router doit être attaché à ce réseau.
# Créer le réseau (opération unique, déjà fait sur lorva-public)
docker network create traefik-public
Dans chaque docker-compose.yml de service :
networks:
traefik-public:
external: true
Et le service doit être attaché :
services:
mon-service:
networks:
- traefik-public
- default # réseau interne si besoin
Traefik ne monte jamais le socket Docker directement. Un conteneur socket-proxy (image tecnativa/docker-socket-proxy) expose uniquement les endpoints nécessaires en lecture seule.
/docker/socket-proxy/
└── docker-compose.yml
Le socket-proxy tourne sur le réseau socket-proxy-net. Traefik se connecte à lui via tcp://socket-proxy:2375.
services:
mon-app:
image: mon-image:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.mon-app.rule=Host(`mon-app.lorva.dev`)"
- "traefik.http.routers.mon-app.entrypoints=websecure"
- "traefik.http.routers.mon-app.tls=true"
- "traefik.http.routers.mon-app.tls.certresolver=letsencrypt"
- "traefik.http.services.mon-app.loadbalancer.server.port=8080"
networks:
- traefik-public
networks:
traefik-public:
external: true
labels:
- "traefik.enable=true"
- "traefik.http.routers.mon-app.rule=Host(`mon-app.lorva.dev`)"
- "traefik.http.routers.mon-app.entrypoints=websecure"
- "traefik.http.routers.mon-app.tls=true"
- "traefik.http.routers.mon-app.tls.certresolver=letsencrypt"
- "traefik.http.routers.mon-app.middlewares=mon-app-auth"
- "traefik.http.middlewares.mon-app-auth.basicauth.users=user:$$apr1$$hash..."
- "traefik.http.services.mon-app.loadbalancer.server.port=8080"
Les
$dans les hash htpasswd doivent être doublés ($$) dans les labels docker-compose YAML.
Générer un hash :echo $(htpasswd -nB user) | sed -e s/\\$/\\$\\$/g
labels:
- "traefik.enable=true"
- "traefik.http.routers.taskori.rule=(Host(`taskori.app`) || HostRegexp(`^.+\\.taskori\\.app$`)) && !Host(`support.taskori.app`)"
- "traefik.http.routers.taskori.entrypoints=websecure"
- "traefik.http.routers.taskori.tls=true"
- "traefik.http.routers.taskori.tls.certresolver=letsencrypt"
- "traefik.http.routers.taskori.tls.domains[0].main=taskori.app"
- "traefik.http.routers.taskori.tls.domains[0].sans=*.taskori.app"
- "traefik.http.services.taskori.loadbalancer.server.port=80"
- "traefik.http.services.taskori.loadbalancer.passHostHeader=true"
| Label | Description |
|---|---|
traefik.enable=true |
Opt-in obligatoire (exposedByDefault=false) |
traefik.http.routers.<name>.rule |
Règle de routage (Host, Path, Headers…) |
traefik.http.routers.<name>.entrypoints |
websecure (443) ou web (80) |
traefik.http.routers.<name>.tls=true |
Activer TLS |
traefik.http.routers.<name>.tls.certresolver |
letsencrypt |
traefik.http.routers.<name>.middlewares |
Middlewares séparés par virgule |
traefik.http.services.<name>.loadbalancer.server.port |
Port interne du conteneur |
traefik.http.services.<name>.loadbalancer.passHostHeader |
Transmettre le Host original |
traefik.docker.network |
Forcer le réseau si multi-réseau |
/docker/traefik/config/*.ymlLe file provider surveille le dossier /config en temps réel (watch: true). Les modifications sont appliquées sans redémarrage de Traefik.
| Cas d'usage | Mécanisme recommandé |
|---|---|
| Service Docker sur lorva-public | Labels dans docker-compose.yml |
| Service sur une autre VM (upstream interne) | Config dynamique config/*.yml |
Service avec insecureSkipVerify (Proxmox, pfSense) |
Config dynamique |
| Wildcard TLS multi-domaines complexes | Config dynamique ou labels |
| Transport personnalisé (skip-verify) | Config dynamique transports.yml |
# /docker/traefik/config/monservice.yml
http:
routers:
monservice:
rule: "Host(`monservice.lorva.dev`)"
entryPoints:
- websecure
tls:
certResolver: letsencrypt
service: monservice
services:
monservice:
loadBalancer:
servers:
- url: "http://10.10.10.10:9000"
# /docker/traefik/config/pve.yml
http:
routers:
pve:
rule: "Host(`pve.lorva.dev`)"
entryPoints:
- websecure
tls:
certResolver: letsencrypt
service: pve
services:
pve:
loadBalancer:
serversTransport: skip-verify
servers:
- url: "https://10.255.255.1:8006"
# /docker/traefik/config/transports.yml
http:
serversTransports:
skip-verify:
insecureSkipVerify: true
Services concernés par insecureSkipVerify : Proxmox (pve.lorva.dev) et pfSense (fw.lorva.dev).
| Fichier | Domaine | Upstream |
|---|---|---|
config-sync.yml |
config.lorva.dev |
http://10.10.10.10:8082 (basicAuth) |
dockhand.yml |
dockhand.lorva.dev |
http://10.10.10.10:3000 |
fw.yml |
fw.lorva.dev |
https://10.10.40.1:8443 (skip-verify) |
gitlab.yml |
gitlab.lorva.dev |
http://10.10.10.10:80 |
netbox.yml |
netbox.lorva.dev |
http://10.10.10.10:8080 |
pve.yml |
pve.lorva.dev |
https://10.255.255.1:8006 (skip-verify) |
registry.yml |
registry.lorva.dev |
http://10.10.10.10:5050 |
taskori.yml |
taskori.app + *.taskori.app |
http://10.10.20.10:80 |
L'infra LORVA utilise Cloudflare avec le proxy activé (orange cloud) sur tous les domaines publics. Le challenge HTTP-01 de Let's Encrypt requiert que le serveur cible réponde directement depuis l'IP publique — ce qui est impossible derrière le proxy Cloudflare.
Le challenge DNS-01 contourne ce problème : Let's Encrypt vérifie un enregistrement _acme-challenge.domaine.tld créé via l'API Cloudflare. Aucune connexion entrante n'est nécessaire.
Traefik → API Cloudflare → crée TXT _acme-challenge → Let's Encrypt valide → cert émis
Les credentials Cloudflare sont injectés via variables d'environnement dans le docker-compose.yml de Traefik. Ne jamais les mettre en clair — utiliser les variables d'environnement du serveur ou un fichier .env non versionné. Valeurs disponibles dans Flatnotes ou les variables CI/CD GitLab.
Variables nécessaires pour le provider cloudflare :
CF_DNS_API_TOKEN — Token API Cloudflare avec permissions Zone:DNS:Edit et Zone:Zone:ReadPour obtenir un wildcard *.lorva.dev, déclarer les domaines dans la config du router :
tls:
certResolver: letsencrypt
domains:
- main: "lorva.dev"
sans:
- "*.lorva.dev"
Lorsque Traefik se trouve derrière Cloudflare (ou un autre proxy), les IP clientes dans les logs et middlewares sont celles de Cloudflare, pas des visiteurs réels. Pour préserver les vraies IPs :
Règle absolue : Configurer
forwardedHeaders.trustedIPsuniquement danstraefik.yml(config statique globale), jamais dans les labels individuels ni dans les configs dynamiques.
# traefik.yml — section entryPoints
entryPoints:
websecure:
address: ":443"
forwardedHeaders:
trustedIPs:
- "173.245.48.0/20"
- "103.21.244.0/22"
- "103.22.200.0/22"
- "103.31.4.0/22"
- "141.101.64.0/18"
- "108.162.192.0/18"
- "190.93.240.0/20"
- "188.114.96.0/20"
- "197.234.240.0/22"
- "198.41.128.0/17"
- "162.158.0.0/15"
- "104.16.0.0/13"
- "104.24.0.0/14"
- "172.64.0.0/13"
- "131.0.72.0/22"
# IPv6 Cloudflare si nécessaire
La liste officielle et à jour des plages IP Cloudflare est disponible sur : https://www.cloudflare.com/ips/
Le dashboard est activé (api.dashboard: true) mais non exposé en insecure (insecure: false). Il est accessible via un router sécurisé avec authentification basicAuth.
URL : https://traefik.lorva.dev (ou le domaine configuré dans le router du dashboard)
Le router dashboard est défini soit par labels dans le docker-compose.yml de Traefik, soit via une config dynamique. Exemple de labels :
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik-dashboard.rule=Host(`traefik.lorva.dev`)"
- "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
- "traefik.http.routers.traefik-dashboard.tls=true"
- "traefik.http.routers.traefik-dashboard.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik-dashboard.service=api@internal"
- "traefik.http.routers.traefik-dashboard.middlewares=dashboard-auth"
- "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$..."
Les credentials du dashboard sont dans Flatnotes.
ssh lorva-public
cd /docker/traefik
docker compose restart traefik
Les configs dynamiques (
/config/*.yml) sont rechargées automatiquement sans redémarrage.
docker logs -f traefik
docker logs traefik 2>&1 | grep -i "error\|warn\|level=error\|level=warn"
docker exec traefik cat /acme.json | python3 -m json.tool | grep -A2 '"main"'
Supprimer le domaine dans acme.json, puis redémarrer Traefik :
# Editer acme.json pour retirer l'entrée du domaine concerné
docker compose restart traefik
docker exec traefik wget -q -O- http://localhost:8080/api/http/routers | python3 -m json.tool | grep -A5 '"name"'
| Symptôme | Cause probable | Solution |
|---|---|---|
certificate not found dans les logs |
Token CF invalide ou expiré | Vérifier CF_DNS_API_TOKEN dans l'env, tester avec curl l'API CF |
NXDOMAIN sur _acme-challenge |
Délai de propagation DNS | Attendre 30-60s, Traefik réessaie automatiquement |
too many certificates |
Rate limit Let's Encrypt | Utiliser le staging ACME pour les tests |
| Cert généré mais HTTPS ne fonctionne pas | acme.json non monté en volume |
Vérifier le docker-compose.yml, le fichier doit persister |
traefik-public :docker inspect mon-conteneur | grep -A20 '"Networks"'
traefik.enable=true est présentcurl -v http://10.10.10.10:PORT/
traefik-public (pas seulement default)labels:
- "traefik.docker.network=traefik-public"
traefik.yml : network: traefik-publicapi@internal est bien référencé dans les labelsapi.insecure: false signifie que le port 8080 interne n'est pas exposé publiquement — le dashboard passe obligatoirement par un router HTTPSSi une URL entre en boucle HTTP → HTTPS → HTTP :
web a bien la redirection vers websecure dans traefik.ymlConfigurer forwardedHeaders.trustedIPs dans traefik.yml (section entryPoints). Voir la section dédiée ci-dessus. Ne jamais configurer cela au niveau d'un middleware individuel ou d'une config dynamique.
Changer le niveau de log dans traefik.yml :
log:
level: WARNING # ou ERROR pour moins de bruit
Puis redémarrer Traefik. En DEBUG, les logs incluent chaque requête et chaque tentative de routage — utile pour diagnostiquer mais très verbeux en production.
Pour exposer un service qui tourne sur une autre VM (ex. lorva-infra à 10.10.10.10) :
/docker/traefik/config/monservice.yml# Vérifier la détection
docker logs traefik 2>&1 | tail -20
| Domaine | Type | Backend |
|---|---|---|
lorva.dev |
Docker label | lorva-web conteneur |
mailer.lorva.dev |
Docker label | lorva-mailer conteneur |
wiki.lorva.dev |
Docker label | wiki-wiki-1 conteneur |
chatwoot.lorva.dev |
Docker label | chatwoot-rails-1 conteneur |
mail.lorva.dev |
Docker label | mailcow nginx conteneur |
klibri-demo.lorva.dev |
Docker label | kavita-demo conteneur |
gitlab.lorva.dev |
Config dynamique | http://10.10.10.10:80 |
registry.lorva.dev |
Config dynamique | http://10.10.10.10:5050 |
netbox.lorva.dev |
Config dynamique | http://10.10.10.10:8080 |
pve.lorva.dev |
Config dynamique | https://10.255.255.1:8006 (skip-verify) |
fw.lorva.dev |
Config dynamique | https://10.10.40.1:8443 (skip-verify) |
dockhand.lorva.dev |
Config dynamique | http://10.10.10.10:3000 |
config.lorva.dev |
Config dynamique | http://10.10.10.10:8082 (basicAuth) |
taskori.app + *.taskori.app |
Config dynamique | http://10.10.20.10:80 |
Chaîne : Cloudflare → Traefik public (lorva-public) → Traefik taskori-prod → container.
Pour que le vrai IP client arrive jusqu'aux apps (et pas une IP edge Cloudflare 172.x), chaque Traefik doit faire confiance à l'amont via entryPoints.<ep>.forwardedHeaders.trustedIPs :
/docker/traefik/traefik.yml, sur 10.10.20.10) : trustedIPs: ["10.10.30.10", "127.0.0.1"] sur l'entryPoint web. ✅/docker/traefik/traefik.yml, sur 10.10.30.10) : trustedIPs = plages IP Cloudflare (IPv4 + IPv6), via une ancre YAML &cloudflare réutilisée sur web et websecure.entryPoints:
web:
address: ":80"
forwardedHeaders:
trustedIPs: &cloudflare
- "173.245.48.0/20"
- "103.21.244.0/22"
# … (liste complète des plages Cloudflare IPv4 + IPv6)
- "172.64.0.0/13"
- "131.0.72.0/22"
- "2400:cb00::/32"
- "2606:4700::/32"
http:
redirections:
entryPoint: { to: websecure, scheme: https }
websecure:
address: ":443"
forwardedHeaders:
trustedIPs: *cloudflare
La config statique nécessite docker restart traefik (pas de hot-reload). Toujours sauvegarder le fichier + valider le YAML + vérifier qu'un site répond après restart (ce Traefik fronte tous les sites publics). Plages Cloudflare à rafraîchir un jour depuis https://www.cloudflare.com/ips/.
Indispensable au ban/log IP du login (taskori-auth → Sécurité du login).
Dernière mise à jour : 2026-05-30