[Article]|20 octobre 2026|Lire en EN

Conserver la vraie IP des clients IPv6 derrière Traefik sur Dokploy (Docker Swarm)

Traefik enregistre chaque visiteur IPv6 sous l'adresse de la passerelle Docker ? Recréez docker_gwbridge avec IPv6 avant d'initialiser le Swarm.

Conserver la vraie IP des clients IPv6 derrière Traefik sur Dokploy (Docker Swarm)
Des visiteurs IPv4 et IPv6 atteignent Traefik via un docker_gwbridge compatible IPv6 et gardent leur vraie IP

Si Traefik sur votre hôte Dokploy enregistre chaque visiteur IPv6 sous l’adresse de la passerelle Docker (172.18.0.1 sur un Swarm par défaut) alors que les visiteurs IPv4 apparaissent avec leur vraie adresse, la solution est de recréer docker_gwbridge avec IPv6 activé avant d’initialiser le Swarm.

Nous l’avons remarqué via Matomo, déployé sur Dokploy derrière Traefik : son journal des visites affichait l’adresse du réseau Docker au lieu des vraies IP. Nous anonymisons les IP des visiteurs dans Matomo, donc l’adresse stockée est de toute façon masquée ; ce dont nous avons besoin, c’est de la localisation approximative de la visite, que la géolocalisation de Matomo déduit de la vraie IP. Avec l’adresse de la passerelle, chaque visite perdait sa localisation. L’analytique n’est que la partie visible : les limites de débit et les bannissements des outils de prévention d’intrusion frappent aussi la mauvaise adresse, et plus il y a de visiteurs en IPv6, plus l’angle mort grandit.

Journal des visites Matomo avant correction : la visite affiche l’adresse du réseau Docker 172.18.0.0 et aucune localisation

Cet article suppose un Swarm à nœud unique et un reverse proxy exécuté comme un conteneur ordinaire. Les noms, sous-réseaux et digests ci-dessous sont des valeurs d’exemple.

Attention : sauvegardez d’abord, et testez la restauration. La correction détruit tous les services, secrets et réseaux overlay du Swarm. Faites un snapshot complet du serveur et une sauvegarde séparée, au niveau fichiers, de tous les volumes et données montées, conservée hors du serveur. Nous utilisons restic piloté par Backrest, une interface web pour les dépôts, planifications, rétentions et restaurations. Les deux sont excellents, libres et gratuits. Quel que soit votre outil, restaurez au moins une fois avant la fenêtre de maintenance.

Pourquoi l’IP disparaît

Visiteur IPv4  (fonctionne)
┌────────────────────────┐       ┌────────────────────────┐       ┌────────────────────────────────┐
│ Client                 │──────►│ NAT noyau (DNAT)       │──────►│ Traefik voit la vraie IPv4     │
└────────────────────────┘       └────────────────────────┘       └────────────────────────────────┘

Visiteur IPv6, Swarm par défaut : docker_gwbridge IPv4 seul  (cassé)
┌────────────────────────┐       ┌────────────────────────┐       ┌────────────────────────────────┐
│ Client                 │──────►│ docker-proxy           │──────►│ Traefik voit 172.18.0.1        │
└────────────────────────┘       │ (source réécrite)      │       │ (la passerelle)                │
                                 └────────────────────────┘       └────────────────────────────────┘

Visiteur IPv6, docker_gwbridge avec IPv6  (fonctionne)
┌────────────────────────┐       ┌────────────────────────┐       ┌────────────────────────────────┐
│ Client                 │──────►│ NAT noyau              │──────►│ Traefik voit la vraie IPv6     │
└────────────────────────┘       │ IPv6 de bout en bout   │       └────────────────────────────────┘
                                 └────────────────────────┘

Un conteneur sur un réseau overlay Swarm reçoit aussi une interface sur docker_gwbridge, le bridge créé par Swarm pour le trafic vers et depuis l’extérieur (réseau Swarm). Ce bridge est IPv4 uniquement par défaut. Quand un port publié est atteint en IPv6 et que la cible n’a pas d’adresse IPv6 derrière, Docker se rabat sur son docker-proxy en espace utilisateur, qui ouvre une nouvelle connexion et réécrit la source avec l’adresse de la passerelle. L’IPv4 passe par le NAT du noyau et conserve sa source.

C’est notre lecture, confirmée par l’expérience plutôt que par le code de Docker. Les tests ci-dessous montrent comment nous l’avons vérifié.

D’abord, confirmer

Test 1 : comparer IPv4 et IPv6 sur le même site. Marquez chaque requête et retrouvez-la dans le journal d’accès JSON du proxy. Lancez curl depuis une autre machine, car un serveur ne peut souvent pas joindre son propre nom public (hairpin NAT).

curl -4 -sI "https://app.example.com/?t=v4test" >/dev/null
curl -6 -sI "https://app.example.com/?t=v6test" >/dev/null

# sur le serveur
grep -E 'v4test|v6test' /path/to/access.log \
  | grep -o -E '"(ClientHost|RequestPath)":"[^"]*"'

Chez nous, l’IPv4 montrait la vraie adresse et l’IPv6 montrait 172.18.0.1. Cette asymétrie est la signature du problème.

Test 2 : prouver que Docker peut préserver la source en IPv6. Un bridge IPv6 jetable avec un conteneur whoami, sans toucher à votre Swarm :

docker network create --ipv6 \
  --subnet 172.30.0.0/16 --subnet fd00:172:30::/64 ipv6test
docker run -d --rm --name whoami6 --network ipv6test -p 18080:80 traefik/whoami

# depuis une autre machine (ouvrez d'abord le port dans votre pare-feu)
curl -6 "http://[<ipv6-du-serveur>]:18080/"

docker rm -f whoami6 && docker network rm ipv6test

Si RemoteAddr est votre vraie IPv6, Docker fait ce qu’il faut dès que le réseau a IPv6, et le bridge de passerelle du Swarm est la pièce manquante.

La correction, et ce qu’elle coûte

Recréez docker_gwbridge avec IPv6 et le même sous-réseau IPv4, puis initialisez le Swarm, qui adopte le bridge existant. La méthode vient de guides communautaires, pas de la documentation Docker : le manuel des opérateurs Co-op Cloud et un gist Dokploy sur Hetzner (une installation IPv6 seul, cité pour la recréation du bridge). Nous avons vérifié que docker network inspect docker_gwbridge indique toujours IPv6 après swarm init. Créer le bridge avec --ipv6 nous a suffi ; nous n’avons pas eu besoin de "ipv6": true dans daemon.json.

Le coût est réel, car le bridge ne peut pas être remplacé tant que le nœud est dans le Swarm :

  • docker swarm leave --force détruit tous les services, secrets et réseaux overlay.
  • Les volumes nommés et les bind mounts survivent. Les conteneurs autonomes continuent de tourner mais perdent leurs rattachements overlay.
  • Tout ce que la plateforme avait créé doit être recréé, puis chaque application redéployée.

Un palliatif moins coûteux si vous n’avez besoin de l’IP que sur quelques sites : supprimez leurs enregistrements AAAA. Les visiteurs retombent en IPv4 et gardent leur vraie adresse, au prix de l’IPv6 sur ces sites.

Procédure

Avant la fenêtre

  1. Sauvegardez (voir l’avertissement en haut). Faites aussi un dump des bases avec leur outil natif, car un snapshot d’une base en cours d’exécution n’est cohérent qu’après un crash. Mettez la supervision en mode maintenance.

  2. Exportez chaque secret Swarm, octet pour octet. docker secret inspect n’affiche jamais les valeurs, mais un conteneur qui monte le secret peut l’afficher. Le service de la plateforme peut en monter plusieurs (un mot de passe de base et un secret d’authentification, par exemple). En perdre un invalide les sessions même si la base revient correctement.

    cid=$(docker ps -q --filter label=com.docker.swarm.service.name=<service-plateforme>)
    docker exec "$cid" cat /run/secrets/<nom-monté> > ~/<fichier-secret>
    wc -c ~/<fichier-secret>; sha256sum ~/<fichier-secret>

    Copiez les fichiers hors du serveur dans votre gestionnaire de mots de passe et comparez taille et somme de contrôle. Une redirection comme ssh host 'sudo cat file' > fichier-local produit silencieusement un fichier vide si sudo demande un mot de passe.

  3. Sauvegardez les specs de ce que vous allez recréer : docker service inspect pour chaque service, docker network inspect pour l’overlay, docker_gwbridge et ingress. Conservez les digests d’images, l’environnement, les montages, les noms de secrets, les contraintes, les ports publiés et les alias des conteneurs autonomes sur l’overlay.

  4. Nettoyez avec soin. Les services morts et les anciennes previews ajoutent chacun un redéploiement. Ne lancez jamais de docker volume prune ou docker system prune à l’aveugle : des volumes « inutilisés » peuvent contenir de vraies données, et vos applications peuvent être des images locales.

Pendant la fenêtre

1 sauvegardes, secrets, specs    site en ligne
        ▼
2 arrêt du proxy, sortie Swarm   ◄── coupure à partir d'ici
        ▼
3 recréer gwbridge avec --ipv6
        ▼
4 swarm init, réseaux, secrets
        ▼
5 recréer services et proxy
        ▼
6 redéployer les apps, valider   site de retour
  1. Arrêtez le proxy et détachez les conteneurs autonomes.

    docker rm -f <conteneur-proxy>
    docker network disconnect -f <overlay> <conteneur-autonome>
  2. Quittez le Swarm et supprimez l’ancien bridge. Tous les sites sont coupés à partir de là. Si Docker signale des endpoints actifs, trouvez-les avec docker network inspect docker_gwbridge, déconnectez-les et recommencez.

    docker swarm leave --force
    docker network rm docker_gwbridge
  3. Recréez le bridge avec IPv6, en reprenant les options de votre network inspect sauvegardé.

    docker network create --driver bridge \
      --subnet 172.18.0.0/16 --gateway 172.18.0.1 \
      --ipv6 --subnet fd00:172:18::/64 \
      --opt com.docker.network.bridge.name=docker_gwbridge \
      --opt com.docker.network.bridge.enable_icc=false \
      --opt com.docker.network.bridge.enable_ip_masquerade=true \
      docker_gwbridge
  4. Initialisez le Swarm avec la même adresse d’annonce, et vérifiez que le bridge a gardé IPv6.

    docker swarm init --advertise-addr <même-adresse>
    docker network inspect docker_gwbridge --format '{{.EnableIPv6}}'   # true
  5. Recréez le réseau overlay et les secrets. Créez les secrets depuis les fichiers, jamais avec echo ou printf, pour que les octets (y compris un retour à la ligne final) soient identiques.

    docker network create --driver overlay --attachable <overlay>
    docker secret create <nom-secret> <fichier-secret>
  6. Recréez les services de la plateforme dans l’ordre des dépendances : base de données, cache, plateforme, avec les digests, montages, cibles de secrets et réseaux sauvegardés. Attendez le « ready to accept connections » de la base avant de démarrer la plateforme.

  7. Démarrez le proxy avec les mêmes bind mounts et ports publiés (80/tcp, 443/tcp, 443/udp) sur l’overlay, et vérifiez qu’ils sont publiés en IPv4 et sur [::].

  8. Rattachez les conteneurs autonomes avec leurs alias d’origine (docker network connect --alias <alias> <overlay> <conteneur>), puis redéployez vos stacks et applications depuis l’interface de la plateforme.

Valider

Refaites le Test 1 depuis l’extérieur avec un nouveau marqueur :

curl -6 -sI "https://app.example.com/?t=final6" | head -1
grep final6 /path/to/access.log | grep -o '"ClientHost":"[^"]*"'

ClientHost doit maintenant être votre propre IPv6. Vérifiez ensuite que l’application l’utilise : beaucoup d’applications ne font confiance à l’en-tête forwarded-for que des proxys connus, donc le réglage de proxys de confiance doit couvrir le sous-réseau overlay. Pour Matomo, voir sa documentation sur le reverse proxy (proxy_client_headers[], proxy_ips[]) ; la nôtre était déjà configurée, donc la correction côté Docker a suffi. Les nouvelles visites retrouvent leur localisation approximative (les IP stockées restent anonymisées).

Journal des visites Matomo après correction : la visite affiche une vraie IPv6 (anonymisée) et sa localisation, Anvers

Pièges

  • Politiques de redémarrage. Après notre premier reboot, deux conteneurs autonomes ne sont pas revenus car leur politique était no. Corrigez un conteneur en cours avec docker update --restart=unless-stopped <conteneur>, et mettez la même chose dans le fichier compose pour qu’un redéploiement ne l’annule pas.
  • Volumes attachés. Une entrée nofail dans /etc/fstab laisse le système démarrer sans le volume, et les conteneurs qui montent un chemin dessous démarreraient sur un dossier vide. Lancez findmnt <point-de-montage> en premier après chaque reboot.
  • Retestez après un redémarrage du démon et un reboot. Un guide prévient que le bridge peut revenir sans IPv6. Sur notre hôte, une mise à jour du paquet Docker et un reboot complet l’ont conservé, et la vraie adresse client a survécu. Voyez cela comme un seul point de données.
  • Supprimez les secrets exportés du serveur (shred -u) une fois qu’ils sont en sécurité dans votre gestionnaire de mots de passe.

Nous avons fait cela sur un Swarm à nœud unique. Les clusters multi-nœuds, où chaque nœud a son propre bridge de passerelle, n’ont pas été testés et demanderaient probablement des adaptations.