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.
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 --forcedé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
-
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.
-
Exportez chaque secret Swarm, octet pour octet.
docker secret inspectn’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-localproduit silencieusement un fichier vide sisudodemande un mot de passe. -
Sauvegardez les specs de ce que vous allez recréer :
docker service inspectpour chaque service,docker network inspectpour l’overlay,docker_gwbridgeet 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. -
Nettoyez avec soin. Les services morts et les anciennes previews ajoutent chacun un redéploiement. Ne lancez jamais de
docker volume pruneoudocker 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
-
Arrêtez le proxy et détachez les conteneurs autonomes.
docker rm -f <conteneur-proxy> docker network disconnect -f <overlay> <conteneur-autonome> -
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 -
Recréez le bridge avec IPv6, en reprenant les options de votre
network inspectsauvegardé.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 -
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 -
Recréez le réseau overlay et les secrets. Créez les secrets depuis les fichiers, jamais avec
echoouprintf, 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> -
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.
-
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
[::]. -
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).
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 avecdocker 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
nofaildans/etc/fstablaisse le système démarrer sans le volume, et les conteneurs qui montent un chemin dessous démarreraient sur un dossier vide. Lancezfindmnt <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.
