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 domainegrafana.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èreTraefikNginx Proxy ManagerCaddy
Découverte de services DockerAutomatique (labels)Semi-auto (UI)Plugin
Certificats Let’s EncryptIntégré (HTTP + DNS challenge)IntégréIntégré
Rechargement à chaudAutomatiqueVia UIAutomatique
Configuration as CodeLabels Docker + YAMLBase SQLiteCaddyfile
Plugins/MiddlewaresÉcosystème richeLimitéRiche
Métriques PrometheusNatifNonNatif
SSO Forward AuthNatifNonNon

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 :

  1. La requête arrive sur le port 443
  2. CrowdSec vérifie que l’IP source n’est pas bannie
  3. TLS est terminé avec un certificat Let’s Encrypt
  4. Le router matche le Host header vers le bon service
  5. Les middlewares s’appliquent (SSO, headers, rate limiting…)
  6. 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 :

ResolverChallengeUsage
letsencryptHTTP-01 (port 80)La plupart des services (grafana.mondomaine.ovh, etc.)
letsencrypt-dnsDNS-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 :

  1. Traefik détecte le nouveau conteneur via le socket Docker
  2. Il crée un router pour grafana.mondomaine.ovh
  3. Il demande un certificat Let’s Encrypt via le HTTP challenge
  4. Le certificat est stocké dans acme.json et renouvelé automatiquement (avant expiration)
  5. 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 preload permet 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 :

  1. La requête arrive sur Traefik
  2. Traefik envoie une sous-requête à Authentik (/outpost.goauthentik.io/auth/traefik)
  3. Si l’utilisateur est authentifié → Authentik retourne 200, la requête est forwardée au backend avec les headers X-authentik-*
  4. 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 :

ServiceMiddlewareRaison
Traefik Dashboardauthentik@fileInterface d’admin sensible
Prometheusauthentik@fileMétriques internes
Headscale UIauthentik@fileGestion VPN
N8Nauthentik@fileWorkflows automatisés
Stirling PDFauthentik@fileOutil interne
pgAdminauthentik@fileAccès bases de données
CloudBeaverauthentik@fileAccès bases de données

Certains services utilisent OIDC natif (pas de forward auth) car ils ont un système d’authentification intégré :

ServiceAuthRaison
GrafanaOIDC natifRole mapping automatique (Admin/Viewer)
Open WebUIOIDC natifGestion fine des utilisateurs
ImmichOAuth2 natifApps 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étriqueDescription
traefik_entrypoint_requests_totalRequêtes par entrypoint, code HTTP
traefik_router_requests_totalRequêtes par router (= par service)
traefik_service_request_duration_secondsTemps de réponse par service
traefik_tls_certs_not_afterExpiration 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é.