Port Docker publié mais inaccessible : la vraie checklist (pas d’improvisation)

Cet article vous a aidé ?

Vous avez lancé le conteneur. Vous avez publié le port. docker ps affiche avec arrogance 0.0.0.0:8080->80/tcp.
Et pourtant votre navigateur fait un timeout comme s’il attendait le bus sous la pluie.

« Port publié mais inaccessible » est l’un de ces incidents qui poussent des gens intelligents à faire des choses stupides : redémarrer Docker, basculer des règles de pare-feu au hasard,
reconstruire des images, et murmurer des menaces au NAT. Arrêtez ça. C’est un système déterministe. Si un port publié n’est pas joignable,
quelque chose bloque spécifiquement le paquet ou l’application n’écoute pas là où vous pensez qu’elle écoute.

Le modèle mental : ce que « publié » signifie réellement

Quand vous publiez un port avec Docker (-p 8080:80), vous n’« ouvrez » pas un port à l’intérieur du conteneur.
Vous demandez au moteur Docker d’organiser le trafic pour que les paquets arrivant sur le port de l’hôte (8080) soient transférés vers le port du conteneur (80).
Ce transfert peut être implémenté de plusieurs façons selon la plateforme et le mode :

  • Docker Linux avec privilèges (classique) : règles NAT iptables/nftables (DNAT) plus un petit proxy en espace utilisateur dans certains scénarios.
  • Docker rootless sous Linux : utilise souvent slirp4netns / routage en espace utilisateur, avec des contraintes et des performances différentes.
  • Docker Desktop (Mac/Windows) : il y a une VM, et le transfert de ports traverse la frontière hôte ↔ VM, avec une couche supplémentaire de « surprises ».

« Publié » signifie que Docker a enregistré l’intention. Cela ne garantit pas que le paquet survivra :
pare-feu de l’hôte, groupes de sécurité cloud, liaison sur la mauvaise interface, route manquante, proxy inverse mal configuré, ou un processus n’écoutant que sur 127.0.0.1
peuvent tous produire le même symptôme : un port qui semble ouvert mais se comporte comme un mur de briques.

L’astuce diagnostique consiste à arrêter de traiter ça comme du « réseau Docker » et à commencer à le voir comme un problème de chemin :
client → réseau → interface hôte → pare-feu → NAT → veth du conteneur → processus dans le conteneur.
Trouvez le premier endroit où la réalité diverge de l’attendu.

Une idée paraphrasée de Werner Vogels (CTO d’Amazon) : tout tombe en panne un jour ; concevez pour détecter, isoler et récupérer rapidement.
Les ports publiés ne font pas exception — instrumentez le chemin et la vérité apparaît.

Playbook de diagnostic rapide (premier/deuxième/troisième)

Première étape : confirmez que le service écoute vraiment (à l’intérieur du conteneur)

Si l’application n’écoute pas sur le port que vous avez mappé, Docker peut transférer des paquets toute la journée et vous aurez quand même des timeouts ou des resets.
Ne commencez pas par iptables. Commencez par le processus.

Deuxième étape : validez que l’hôte écoute et transfère (sur la bonne interface)

Vérifiez que le port de l’hôte est lié, sur quelle interface il est lié, et si Docker a inséré les règles NAT attendues.
Si l’hôte n’écoute pas, ou n’écoute que sur 127.0.0.1, les clients distants ne pourront pas se connecter.

Troisième étape : éliminez les « bloqueurs externes » (pare-feu, groupes de sécurité cloud, routage)

Si localhost fonctionne mais pas le distant, arrêtez de blâmer Docker. C’est un problème de périmètre : politique UFW/firewalld/nftables, groupe de sécurité cloud,
routage de l’hôte, ou une vérification de santé du load balancer qui frappe au mauvais endroit.

Quatrième étape : vérifiez les modes spéciaux et fonctionnalités limites

Docker rootless, IPv6, réseau host, macvlan, réseaux overlay, reverse proxies et hairpin NAT ont chacun des angles vifs.
Si vous êtes dans l’un de ces cas, soyez explicite et suivez la branche pertinente de la checklist ci-dessous.

Blague #1 : NAT, c’est comme la politique de bureau — tout le monde dit comprendre, puis vous les voyez accuser l’imprimante.

Tâches pratiques : commandes, sorties, décisions (12+)

Ce sont des vérifications de niveau production. Chacune inclut une commande, une sortie typique, ce que cela signifie et la décision suivante.
Exécutez-les dans l’ordre jusqu’à trouver la première chose « incorrecte ». C’est à ce point que vous arrêtez et réparez.

Tâche 1 : Confirmez le mapping que Docker pense avoir créé

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Ports}}'
NAMES          PORTS
web-1          0.0.0.0:8080->80/tcp, [::]:8080->80/tcp
db-1           5432/tcp

Sens : Docker affirme avoir publié 8080 sur toutes les interfaces IPv4 et aussi IPv6.
Si vous voyez seulement 127.0.0.1:8080->80/tcp, c’est lié au localhost et les connexions distantes échoueront.

Décision : Si le mapping semble erroné, corrigez la configuration run/compose d’abord. S’il semble correct, continuez.

Tâche 2 : Inspectez les liaisons de port du conteneur (vérité terrain)

cr0x@server:~$ docker inspect -f '{{json .NetworkSettings.Ports}}' web-1
{"80/tcp":[{"HostIp":"0.0.0.0","HostPort":"8080"}]}

Sens : la configuration du conteneur indique : hôte 0.0.0.0:8080 → conteneur 80/tcp.
Si null apparaît pour le port, il n’est pas publié.

Décision : Si la liaison n’est pas celle attendue, redéployez avec le bon -p ou la clé ports: dans Compose.

Tâche 3 : Vérifiez que le service écoute dans le conteneur

cr0x@server:~$ docker exec -it web-1 sh -lc "ss -lntp | sed -n '1,6p'"
State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN 0      4096   0.0.0.0:80         0.0.0.0:*     users:(("nginx",pid=1,fd=6))

Sens : quelque chose (nginx) écoute sur 0.0.0.0:80 à l’intérieur du conteneur.
Si vous voyez seulement 127.0.0.1:80, cela fonctionnera souvent car le trafic arrive localement dans le conteneur,
mais certaines applis se lient uniquement en IPv6 ou via des sockets UNIX.

Décision : Si rien n’écoute, corrigez l’application/le conteneur (commande incorrecte, crash loop, config).
Si c’est ok, continuez.

Tâche 4 : Curl du service depuis l’intérieur du conteneur

cr0x@server:~$ docker exec -it web-1 sh -lc "apk add --no-cache curl >/dev/null 2>&1; curl -sS -D- http://127.0.0.1:80/ | head"
HTTP/1.1 200 OK
Server: nginx/1.25.3
Date: Tue, 02 Jan 2026 14:01:12 GMT
Content-Type: text/html

Sens : l’application répond localement. Si cela échoue, stoppez. La publication de ports ne réparera pas une appli cassée.

Décision : Si le curl interne échoue, diagnosez l’application. S’il réussit, allez vers l’extérieur.

Tâche 5 : Curl via l’IP du conteneur depuis l’hôte

cr0x@server:~$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web-1
172.17.0.4
cr0x@server:~$ curl -sS -D- http://172.17.0.4:80/ | head
HTTP/1.1 200 OK
Server: nginx/1.25.3
Date: Tue, 02 Jan 2026 14:01:20 GMT
Content-Type: text/html

Sens : l’hôte peut atteindre le conteneur via le réseau bridge. Si cela échoue, le problème est dans le réseau hôte/contener
(bridge down, routage par politique, chaîne DOCKER-USER, modules de sécurité, ou conteneur attaché à un autre réseau).

Décision : Si hôte → IP du conteneur échoue, inspectez le réseau docker, iptables et les politiques hôte. Si ça marche, vérifiez le chemin de publication.

Tâche 6 : Curl via le port publié sur l’hôte

cr0x@server:~$ curl -sS -D- http://127.0.0.1:8080/ | head
HTTP/1.1 200 OK
Server: nginx/1.25.3
Date: Tue, 02 Jan 2026 14:01:28 GMT
Content-Type: text/html

Sens : le forwarding de port fonctionne localement. Si localhost marche mais pas le distant, vous êtes maintenant en territoire pare-feu/interface.

Décision : Si localhost échoue, inspectez l’écoute de l’hôte et les règles NAT ensuite. Si localhost fonctionne, passez aux vérifications périmétriques.

Tâche 7 : Voyez ce qui écoute réellement sur le port de l’hôte

cr0x@server:~$ sudo ss -lntp | grep ':8080'
LISTEN 0      4096         0.0.0.0:8080      0.0.0.0:*    users:(("docker-proxy",pid=2314,fd=4))

Sens : l’hôte a un écouteur, souvent docker-proxy. Sur des setups plus récents, vous ne verrez peut-être pas docker-proxy,
parce que le DNAT suffit ; alors ss peut ne rien montrer même si ça fonctionne.

Décision : Si vous voyez seulement 127.0.0.1:8080, corrigez la liaison (ports Compose ou IP hôte explicite).
Si vous ne voyez rien et que ça échoue, vérifiez les règles NAT et la config du daemon Docker.

Tâche 8 : Confirmez que Docker a inséré des règles NAT (vue iptables legacy)

cr0x@server:~$ sudo iptables -t nat -S DOCKER | sed -n '1,6p'
-N DOCKER
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.4:80

Sens : les paquets arrivant sur TCP/8080 de interfaces non-docker0 sont DNATés vers l’IP du conteneur/80.
Si la règle manque, Docker ne programme pas le NAT (fréquent en rootless, avec flags daemon custom ou mismatch nftables).

Décision : Si les règles manquent ou sont incorrectes, réparez le backend réseau de Docker, ou redéployez Docker avec l’intégration iptables appropriée.

Tâche 9 : Vérifiez la chaîne DOCKER-USER (la chaîne « vous vous êtes bloqués vous-mêmes »)

cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -j DROP

Sens : Cet hôte droppe le trafic forwardé avant les propres règles de Docker. Cela rendra les ports publiés inaccessibles depuis le réseau
tandis que localhost peut encore fonctionner (selon le chemin).

Décision : Remplacez les drops globaux par des règles d’autorisation explicites, ou déplacez la politique vers une couche de firewall contrôlée qui tient compte de Docker.

Tâche 10 : Si vous utilisez nftables, inspectez le ruleset (vue moderne)

cr0x@server:~$ sudo nft list ruleset | sed -n '1,40p'
table ip nat {
  chain PREROUTING {
    type nat hook prerouting priority dstnat; policy accept;
    tcp dport 8080 dnat to 172.17.0.4:80
  }
  chain OUTPUT {
    type nat hook output priority -100; policy accept;
    tcp dport 8080 dnat to 172.17.0.4:80
  }
}

Sens : le DNAT existe en prerouting et en output (connexions locales de l’hôte).
Si la règle existe seulement en OUTPUT, le trafic distant ne sera pas transféré ; si seulement en PREROUTING, le comportement localhost peut différer.

Décision : Assurez-vous que Docker est correctement intégré à nftables, et que vous ne mélangez pas des backends iptables incompatibles.

Tâche 11 : Vérifiez le forwarding du kernel et les réglages bridge netfilter

cr0x@server:~$ sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptables 2>/dev/null
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1

Sens : le forwarding est activé, et le trafic sur les bridges est visible par iptables.
Certains baselines durcis désactivent ces options, puis se demandent pourquoi les conteneurs sont inaccessibles.

Décision : Si ip_forward=0 et vous attendez du routage/NAT, activez-le (et documentez-le dans votre baseline).

Tâche 12 : Vérifiez l’état d’UFW et s’il bloque silencieusement Docker

cr0x@server:~$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip

Sens : « deny (routed) » est le classique tueur de conteneurs si vous comptez sur du trafic forwardé.
UFW peut bloquer le forwarding du bridge Docker même si vous autorisez le port sur l’hôte.

Décision : Soit configurez UFW pour autoriser le trafic routé pour les réseaux Docker, soit gérez le firewall avec des règles iptables/nft explicites.

Tâche 13 : Vérifiez firewalld zones et masquerade (courant sur RHEL/CentOS)

cr0x@server:~$ sudo firewall-cmd --state
running
cr0x@server:~$ sudo firewall-cmd --get-active-zones
public
  interfaces: eth0
docker
  interfaces: docker0

Sens : firewalld peut placer docker0 dans sa propre zone. Si cette zone interdit le forwarding/masquerade, les ports publiés cassent.

Décision : Assurez-vous que la zone docker autorise le forwarding comme requis, ou unifiez les zones de manière intentionnelle.

Tâche 14 : Test depuis une machine distante et comparez le chemin

cr0x@server:~$ curl -sS -m 2 -D- http://$(hostname -I | awk '{print $1}'):8080/ | head
HTTP/1.1 200 OK
Server: nginx/1.25.3
Date: Tue, 02 Jan 2026 14:01:39 GMT
Content-Type: text/html

Sens : cela simule « pas localhost » en utilisant l’IP de l’hôte. Si cela échoue mais que 127.0.0.1 marche,
votre liaison/pare-feu/route diffère entre loopback et interface externe.

Décision : Si ça échoue, inspectez l’IP de liaison et le pare-feu par interface. Si ça réussit, le problème peut être externe à l’hôte (SG cloud, LB, route client).

Tâche 15 : Capture de paquets sur l’hôte pour voir si le SYN arrive

cr0x@server:~$ sudo tcpdump -ni eth0 tcp port 8080 -c 5
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
14:02:01.123456 IP 203.0.113.10.51922 > 192.0.2.20.8080: Flags [S], seq 123456789, win 64240, options [mss 1460,sackOK,TS val 1 ecr 0,nop,wscale 7], length 0

Sens : si vous voyez des SYN arriver, le chemin réseau vers l’hôte est OK. Si vous ne voyez rien, le problème est en amont
(groupe de sécurité, NACL, routeur, load balancer, DNS pointant ailleurs).

Décision : Pas de SYN : arrêtez de déboguer Docker et regardez vers l’extérieur. SYN arrivé : continuez le débogage du pare-feu/NAT/hôte.

Tâche 16 : Capture sur docker0 pour confirmer que le forwarding se produit

cr0x@server:~$ sudo tcpdump -ni docker0 tcp port 80 -c 5
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on docker0, link-type EN10MB (Ethernet), capture size 262144 bytes
14:02:01.124001 IP 203.0.113.10.51922 > 172.17.0.4.80: Flags [S], seq 123456789, win 64240, options [mss 1460,sackOK,TS val 1 ecr 0,nop,wscale 7], length 0

Sens : le paquet a été DNATé et a atteint docker0. S’il arrive sur eth0 mais pas sur docker0,
vos règles NAT/forwarding sont le goulot d’étranglement.

Décision : Réparez iptables/nftables, les réglages de forwarding, ou les règles de la chaîne DOCKER-USER.

Où ça casse : les vrais modes de défaillance

1) L’application écoute sur la mauvaise interface ou le mauvais port

Beaucoup de frameworks choisissent par défaut 127.0.0.1 pour « sécurité ». C’est parfait sur un laptop. Dans des conteneurs, c’est une erreur fréquente.
Node, serveurs de développement Python, et certains microframeworks Java sont souvent responsables.

Ce que vous voyez : le conteneur est « healthy », le port est publié, mais les connexions pendent ou sont reset. Le curl interne peut ne fonctionner qu’en localhost,
ou pas du tout s’il se lie à un socket UNIX.

Que faire : forcez l’application à se lier sur 0.0.0.0 (ou explicitement sur l’interface du conteneur) et confirmez avec ss -lntp.
Si vous utilisez un serveur de dev, arrêtez. Utilisez un vrai serveur (gunicorn/uvicorn, nginx, etc.) pour tout ce qui n’est pas sur votre laptop.

2) Le port est publié seulement sur localhost

Compose et docker run autorisent des liaisons comme 127.0.0.1:8080:80. Cela signifie exactement ce que ça dit.
Ça marche depuis l’hôte, échoue depuis le réseau, et fait perdre des heures parce que « ça marche chez moi » est un puissant sédatif.

Correction : liez sur 0.0.0.0 ou sur l’IP de l’interface externe spécifique, intentionnellement.
Rendez la liaison explicite dans Compose quand c’est ce que vous voulez.

3) Le pare-feu de l’hôte bloque le trafic forwardé (UFW/firewalld) même si le port semble ouvert

Un point subtil : un « port publié » est souvent implémenté avec NAT + forwarding.
Les pare-feu peuvent autoriser INPUT vers TCP/8080 mais bloquer FORWARD vers docker0, résultant en timeouts.
Localement, ça peut encore fonctionner parce que le trafic loopback passe par un autre chemin/hook.

Si vous utilisez UFW avec « deny routed », supposez qu’il est impliqué jusqu’à preuve du contraire.
Si vous utilisez firewalld, supposez que les zones/masquerade ont leur importance.

4) La chaîne DOCKER-USER le bloque (intentionnellement ou accidentellement)

DOCKER-USER existe précisément pour que vous puissiez insérer une politique en amont des propres chaînes de Docker. C’est un bon design.
C’est aussi l’endroit où des drops « temporaires » vivent pendant des années.

Un unique -j DROP dans DOCKER-USER peut noyer vos ports publiés. Ne saupoudrez pas de drops globaux sans documentation adéquate.

5) Vous mélangez des backends iptables (legacy vs nft) et Docker programme le « mauvais »

Sur certaines distributions, iptables est un wrapper de compatibilité au-dessus de nftables, et vous pouvez finir avec des règles insérées dans une vue
tandis que les paquets sont évalués par l’autre, selon la version du kernel/userspace et la configuration.

Symptomatique : Docker affirme que les ports sont publiés ; des règles apparaissent dans iptables -t nat -S mais n’affectent pas le trafic,
ou des règles apparaissent dans nft mais la sortie iptables semble vide.

Correction : choisissez un backend cohérent et configurez Docker en conséquence. Et arrêtez de traiter la pile de firewall comme un roman d’aventure.

6) Docker rootless : mécanique de forwarding différente, surprises différentes

Docker rootless évite le réseau privilégié. Bon pour la sécurité, moins parfait pour « se comporter comme Docker rootful ».
Les ports publiés sont implémentés via du forwarding en espace utilisateur ; performances, comportement de liaison et interactions avec le pare-feu diffèrent.

Symptomatique : les ports ne fonctionnent que sur localhost, ou seulement pour des ports élevés, ou échouent quand on se lie à des adresses spécifiques.
Les règles n’apparaîtront pas dans les iptables système parce que rootless ne les programme pas.

Correction : confirmez que vous êtes en rootless, puis suivez les recommandations spécifiques (comme une configuration explicite de redirection de ports).
Si vous avez besoin d’un comportement NAT classique, exécutez Docker rootful sur des hôtes durcis au lieu de demi-simuler.

7) Mismatch proxy inverse (mauvais upstream, mauvais réseau, mauvaises attentes TLS)

Vous publiez un port, mais le trafic passe en réalité par nginx/HAProxy/Traefik sur l’hôte ou dans un autre conteneur.
Votre problème peut être que le proxy parle au mauvais IP de conteneur, au mauvais réseau, ou attend TLS sur HTTP clair (ou l’inverse).

Symptomatique : le conteneur marche via un curl direct, mais le proxy renvoie 502/504.
Les gens appellent ça « port inaccessible » parce que le symptôme côté utilisateur est « site down ».

Correction : testez la connectivité upstream depuis le contexte du proxy, pas depuis votre laptop émotionnel.

8) Hairpin NAT / « me connecter à mon IP publique depuis le même hôte »

Vous êtes sur l’hôte et vous faites un curl sur l’IP publique de l’hôte:8080 et ça échoue, mais localhost fonctionne.
C’est souvent du hairpin NAT. Certains setups réseau (notamment avec rp_filter strict ou certains routages cloud)
traitent ce chemin différemment.

Correction : testez sur la bonne interface et comprenez si vous traversez un routeur/LB externe et revenez.
Si vous avez besoin de hairpin, configurez-le explicitement (ou ne dépendez pas de lui).

9) IPv6 « publié » mais pas réellement joignable

Docker peut afficher des liaisons [::]:8080. Cela ne veut pas dire que votre hôte a la connectivité IPv6,
que votre pare-feu l’autorise, ou que le chemin conteneur est prêt pour IPv6.

Symptomatique : IPv4 marche ; IPv6 fait timeout. Ou les clients préfèrent IPv6 et échouent alors que IPv4 fonctionnerait.

Correction : confirmez le routage et les règles pare-feu IPv6, et soyez explicite sur le support IPv6. L’IPv6 accidentel n’est pas une stratégie.

Erreurs courantes : symptôme → cause racine → correction

1) « Ça marche en localhost mais pas depuis une autre machine »

Cause : port lié à 127.0.0.1 seulement, ou pare-feu autorisant local mais bloquant externe, ou groupe de sécurité cloud bloquant.

Correction : publiez sur 0.0.0.0 (ou la bonne interface) et ouvrez le port au bon niveau (pare-feu hôte + SG cloud).

2) « docker ps montre 0.0.0.0:PORT, mais ss ne montre rien d’à l’écoute »

Cause : dépendance au DNAT iptables sans proxy en espace utilisateur ; ss ne montrera pas d’écouteur même si le NAT fonctionne.

Correction : testez avec curl 127.0.0.1:PORT. Si ça échoue, inspectez iptables/nft. Ne traitez pas la sortie de ss comme la seule vérité.

3) « Connection refused immédiatement »

Cause : pas de processus à l’écoute dans le conteneur, mauvais port de conteneur, ou conteneur crashé et remplacé.

Correction : docker exec ss -lntp et docker logs. Confirmez que l’appli se lie au port attendu.

4) « Timeout (SYN envoyé, pas de SYN-ACK) »

Cause : paquet dropé par le pare-feu, groupe de sécurité, DOCKER-USER, ou routage/NACL.

Correction : tcpdump sur l’interface externe de l’hôte. Si le SYN n’arrive jamais, c’est en amont. S’il arrive, inspectez le pare-feu/NAT sur l’hôte.

5) « Marche de l’hôte vers l’IP du conteneur, mais pas via le port publié »

Cause : NAT/forwarding non programmé ou bloqué ; mismatch backend iptables ; politique DOCKER-USER.

Correction : inspectez les règles NAT, DOCKER-USER, et les sysctls de forwarding. Rendez la politique firewall explicite et testée.

6) « Seuls certains clients peuvent joindre (d’autres timeout) »

Cause : problèmes MTU (VPNs), routage asymétrique, préférence IPv6, ou DNS split-horizon pointant vers des IP différentes.

Correction : capturez les paquets ; testez en forçant IPv4/IPv6 ; vérifiez MTU et routes ; ne supposez pas que le réseau est uniforme.

7) « Ça a cassé après durcissement UFW/firewalld »

Cause : trafic routé/forwardé bloqué ; docker0 placé dans une zone restrictive.

Correction : autorisez explicitement le forwarding pour les réseaux Docker, ou implémentez des règles Docker-aware dans DOCKER-USER avec prudence.

8) « Le proxy inverse renvoie 502 mais le port direct marche »

Cause : upstream du proxy pointe vers le mauvais IP réseau du conteneur, mauvais protocole (HTTP vs HTTPS), ou DNS résout différemment à l’intérieur du proxy.

Correction : testez la connectivité depuis le conteneur/host du proxy, vérifiez les cibles upstream, et faites partager le même réseau Docker au proxy et à l’appli si vous utilisez des noms DNS container-to-container.

Checklists / plan pas-à-pas (faites ça, pas de ressentis)

Checklist A : Vous êtes sur un hôte Linux et le port est mort de partout

  1. Dans le conteneur : confirmez le processus à l’écoute sur le port attendu avec ss -lntp.
  2. Dans le conteneur : curl 127.0.0.1:PORT (ou équivalent) pour valider la réponse de l’application.
  3. Hôte → IP conteneur : curl CONTAINER_IP:PORT. Si ça échoue, réparez le réseau docker ou l’appli.
  4. Hôte via port publié : curl 127.0.0.1:PUBLISHED. Si ça échoue alors que le précédent marche, c’est NAT/forwarding.
  5. Règles NAT : inspectez iptables -t nat -S DOCKER ou nft list ruleset.
  6. Chaînes de politique : inspectez iptables -S DOCKER-USER et toute politique globale FORWARD.
  7. Paramètres kernel : vérifiez net.ipv4.ip_forward et les réglages bridge netfilter.
  8. Ensuite seulement : redémarrez Docker si vous avez changé de backend de règles ou la config du daemon. Ne redémarrez pas le système comme outil de diagnostic.

Checklist B : Localhost marche, distant échoue

  1. Liaison : vérifiez que ce n’est pas 127.0.0.1:PUBLISHED dans docker ps / inspect.
  2. Test « externe-ish » local : curl l’IP de l’hôte au lieu de localhost.
  3. Pare-feu : vérifiez les politiques UFW/firewalld pour le trafic routé/forwardé.
  4. Présence des paquets : lancez tcpdump sur l’interface externe pendant un test distant.
  5. Périmètre cloud : confirmez que les security groups/NACL/load balancer pointent vers le bon hôte/port.
  6. Sanité DNS : assurez-vous que les clients résolvent la bonne IP (pas d’enregistrement obsolète ou de split-horizon).

Checklist C : C’est « joignable » mais l’appli est mauvaise (502/boucles redirect/SSL bizarre)

  1. Test direct : curl le port publié directement sur l’hôte. Obtenez un 200 propre (ou la réponse attendue).
  2. Chemin proxy : testez depuis le contexte du proxy (conteneur ou hôte) vers la cible upstream.
  3. Protocole : vérifiez HTTP vs HTTPS. Ne parlez pas TLS à un port non chiffré.
  4. En-têtes : confirmez que Host et X-Forwarded-Proto sont correctement définis si l’appli s’en sert.
  5. Réseau : assurez-vous que le proxy et l’appli partagent le même réseau Docker si vous utilisez des noms DNS de conteneur à conteneur.

Blague #2 : Si votre solution est « redémarrer tout », vous n’avez pas corrigé — vous avez lancé les dés et appelé ça de l’ingénierie.

Trois mini-histoires du monde corporate

Mini-histoire 1 : L’incident causé par une mauvaise hypothèse

Une équipe a migré un petit service interne de VMs vers Docker sur un baseline Linux durci. Le déploiement était propre : le conteneur tournait, les checks passaient,
et le port était publié. L’ingénieur on-call a vérifié curl 127.0.0.1:PORT sur l’hôte et a vu la réponse attendue. Déployez.

Dix minutes plus tard, les utilisateurs réels ne pouvaient plus y accéder. L’erreur n’était pas un 500 ; c’était rien — des timeouts. Cela déclencha le rituel prévisible :
redéployer, redémarrer Docker, reconstruire l’image, et finalement « peut-être que c’est le réseau ». Pendant ce temps, le load balancer marqua le service unhealthy et le vidait.

L’hypothèse erronée était subtile : « Si localhost marche, le réseau doit marcher. » Sur cet hôte, UFW était configuré avec deny entrant par défaut (OK),
et deny routed par défaut (pas OK pour le forwarding Docker). Les requêtes localhost n’exerçaient jamais la même politique de forwarding que les requêtes externes.
L’équipe avait donc prouvé que l’application marchait, mais pas le chemin publié.

La correction fut banale et efficace : une politique de pare-feu documentée autorisant le trafic routé vers le sous-réseau des conteneurs et ports spécifiques,
plus une étape de runbook exigeant un curl distant (depuis un bastion sur le même réseau) avant de clore le ticket.
Après cela, cette classe d’incident a presque disparu. Pas parce que Docker était devenu plus gentil — parce que l’équipe a arrêté de supposer.

Mini-histoire 2 : L’optimisation qui s’est retournée

Une autre équipe voulait « performance maximale » et a retiré tout ce qui semblait une surcharge. Ils ont désactivé le proxy userland dans la config du daemon Docker,
resserré les règles de firewall, et consolidé la gestion d’iptables sous un agent de sécurité hôte. En test, tout semblait plus rapide et plus propre.
Les benchmarks étaient splendides. Les slides aussi.

Puis vint la production. Un sous-ensemble de connexions vers des ports publiés a commencé à échouer de façon intermittente — surtout depuis des sous-réseaux spécifiques.
Les échecs n’étaient pas suffisamment cohérents pour être évidents, mais suffisamment pour gâcher la journée de quelqu’un. L’incident rebondit entre « réseau » et « plateforme »
plus longtemps que souhaitable.

La racine était une interaction entre le cycle de rafraîchissement des règles de l’agent de sécurité hôte et les règles NAT dynamiques de Docker.
À chaque remplacement de conteneur, Docker programmant le DNAT ; l’agent réconciliait ensuite vers son état désiré et supprimait ce qu’il ne reconnaissait pas.
Comme le proxy était désactivé, il n’y avait pas de chemin de secours en espace utilisateur — seulement des règles NAT. Certaines connexions tombaient pendant les fenêtres où les règles manquaient.

La reprise a été de cesser de traiter iptables comme un jouet partagé. L’équipe est passée à une politique explicite : soit l’agent de sécurité possède l’état du firewall avec
une intégration aware de Docker, soit Docker possède le NAT plus une politique DOCKER-USER contrôlée. Ils ont choisi la dernière pour la simplicité.
Les performances sont restées bonnes. La fiabilité s’est améliorée radicalement. L’optimisation n’était pas fausse ; c’est le modèle de propriété qui était en défaut.

Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise

Un groupe plateforme gérait une flotte d’hôtes Docker derrière des load balancers internes. Ils avaient la pratique stricte : chaque service avait une « sonde de connectivité »
standard exécutée depuis trois endroits — dans le conteneur, depuis l’hôte, et depuis un nœud canari distant dans le même segment réseau.
C’était banal. C’était aussi écrit, automatisé, et appliqué lors des incidents.

Un après-midi, une mise à jour noyau de routine fut suivie d’une vague d’alertes « service inaccessible ». La panique a commencé dans les canaux Slack habituels.
Mais l’on-call a suivi les sondes. Le curl intra-conteneur marchait. Hôte → IP conteneur marchait. Hôte → port publié marchait. Le canari distant échouait.
Cela a réduit le problème en quelques minutes : ce n’était ni Docker, ni l’appli. C’était la joignabilité d’entrée.

Un changement réseau avait modifié quels sous-réseaux pouvaient atteindre les hôtes, et les health checks du load balancer provenaient maintenant d’un sous-réseau
non sur la liste d’autorisation. Le groupe plateforme disposait des captures de paquets prouvant que des SYN n’avaient jamais frappé l’interface de l’hôte.
La correction fut une mise à jour propre de la allowlist périmétrique.

La pratique qui a sauvé la mise n’était pas une optimisation astucieuse. C’était un test discipliné et répétable depuis plusieurs points de vue,
avec des attentes écrites. Cela a évité une spirale de débogage conteneur et a raccourci l’incident.
L’ennuyeux est une fonctionnalité quand vous êtes en astreinte.

Faits intéressants & courte histoire (pour arrêter de deviner)

  • La publication de ports d’origine de Docker sur Linux reposait fortement sur des règles NAT iptables car c’était largement disponible et rapide en space noyau.
  • Le « userland proxy » existait pour gérer des cas limites (comme les connexions hairpin et certains comportements localhost) quand le DNAT pur n’était pas suffisant.
  • La chaîne DOCKER-USER a été introduite pour que les opérateurs puissent imposer une politique avant les règles automatiquement gérées par Docker sans entrer en conflit avec les mises à jour Docker.
  • iptables a deux « mondes » sur le Linux moderne : legacy xtables et backend nftables. Les mélanger peut produire des règles visibles mais non utilisées par le noyau pour votre chemin de trafic.
  • Le « deny routed » par défaut d’UFW est raisonnable pour des hôtes non conteneurisés mais casse souvent le forwarding des conteneurs sauf si vous l’autorisez explicitement.
  • Docker rootless est devenu populaire avec les équipes sécurité, mais il change intentionnellement la façon dont le réseau est implémenté et observé.
  • Docker Desktop sur Mac/Windows implique toujours une frontière VM, donc la publication de ports est un transfert de port à travers la virtualisation, pas seulement une règle NAT locale.
  • Le comportement IPv6 est fréquemment « activé par accident » car les liaisons peuvent afficher [::] même quand l’environnement ne supporte pas réellement la connectivité IPv6 bout-en-bout.

La leçon de l’histoire : le comportement observé est le produit des décisions de plateforme, de la posture de sécurité, et de l’évolution de la pile kernel/firewall.
Traitez votre environnement comme un système réel à couches, pas comme une bulle magique Docker.

FAQ

1) Pourquoi docker ps affiche le port publié s’il ne fonctionne pas ?

Parce que c’est une vue de configuration, pas un test de connectivité. Docker a enregistré la liaison et a probablement tenté de programmer le forwarding,
mais les pare-feu, le routage ou l’application peuvent encore bloquer le trafic réel.

2) Comment savoir si le problème vient de l’appli ou du réseau ?

Utilisez le test en trois sauts : dans le conteneur (curl localhost), hôte vers IP du conteneur, hôte vers port publié. Le premier saut qui échoue indique votre couche.

3) Pourquoi localhost marche mais l’IP LAN de l’hôte ne marche pas ?

Chemins différents. Localhost peut frapper des règles OUTPUT ou du DNAT local ; le trafic externe frappe PREROUTING/FORWARD et est soumis à une politique différente.
Vérifiez aussi si vous vous êtes lié accidentellement à 127.0.0.1.

4) Dois-je ouvrir le port dans le pare-feu du conteneur ?

En général non. La plupart des conteneurs n’exécutent pas de pare-feu. Si vous en avez un, traitez-le comme un hôte réel : autorisez l’entrée vers le port de l’app.
Mais la plupart des problèmes de « port publié inaccessible » sont au niveau hôte/périmètre, pas pare-feu conteneur.

5) Pourquoi ss -lntp n’affiche-t-il parfois pas d’écouteur pour un port publié ?

Parce que la publication basée sur NAT ne nécessite pas un processus écoutant sur le port de l’hôte. Le noyau réécrit et forwarde les paquets.
Si le proxy userland est utilisé, vous verrez docker-proxy.

6) La publication Docker peut-elle échouer à cause d’un mismatch iptables/nftables ?

Oui. Si Docker programme des règles dans un backend qui n’est pas effectivement utilisé pour votre trafic, vous aurez « des règles existent » mais pas de forwarding.
Vérifiez à la fois les vues iptables et nft et standardisez la pile.

7) Qu’est-ce qui change avec Docker rootless ?

Rootless ne peut pas programmer librement les règles NAT système. La publication de ports s’appuie typiquement sur des mécanismes en espace utilisateur.
L’observabilité (inspection iptables) et le comportement (contraintes de liaison, performances) diffèrent. Confirmez le mode d’abord.

8) Comment déboguer si l’hôte est derrière un load balancer ?

Capturez les paquets sur l’interface de l’hôte pendant que le LB probe. Si les SYN n’arrivent jamais, c’est la config LB, les groupes de sécurité, ou le routage.
Si les SYN arrivent mais n’atteignent pas docker0, c’est le pare-feu/NAT hôte.

9) Pourquoi ça marche en IPv4 mais échoue en IPv6 ?

Parce que la connectivité IPv6 nécessite un routage bout-en-bout et des règles pare-feu. Docker afficher [::] n’assure pas que votre réseau la supporte.
Testez explicitement en IPv4/IPv6 et configurez de façon intentionnelle.

10) Dois-je utiliser --network host pour « éviter les problèmes réseau Docker » ?

Seulement si vous comprenez les compromis. Le réseau host supprime la couche NAT mais augmente le risque de collision de ports et réduit l’isolation.
C’est un outil, pas un pansement pour des problèmes inconnus.

Conclusion : étapes suivantes pour éviter les répétitions

Lorsqu’un port Docker est publié mais inaccessible, le système vous indique précisément où c’est cassé — il faut juste l’interroger dans le bon ordre.
Commencez à l’intérieur du conteneur, allez vers l’extérieur, et arrêtez-vous dès que vous trouvez le premier saut échoué. C’est le goulot. Réparez ça, pas votre patience.

Faites ceci ensuite

  1. Codifiez le test trois-sauts (localhost conteneur → IP conteneur depuis l’hôte → port publié sur l’hôte → canari distant) dans vos runbooks.
  2. Standardisez la propriété du pare-feu : soit Docker possède le NAT et vous utilisez DOCKER-USER intentionnellement, soit votre gestionnaire de firewall s’intègre avec Docker. Pas de mystère partagé.
  3. Rendez les liaisons explicites dans Compose (0.0.0.0:PORT:PORT vs 127.0.0.1) pour ne pas livrer du « ça marche sur mon hôte ».
  4. Instrumentez la joignabilité : une simple vérification blackbox depuis un nœud distant détecte la plupart de ces problèmes avant les utilisateurs.
  5. Documentez les modes spéciaux (rootless, IPv6, reverse proxies, load balancers) à côté du service, pas dans la tête de quelqu’un.

Votre but n’est pas de mémoriser la trivia du réseau Docker. Votre but est de réduire le temps jusqu’à la vérité. La checklist ci-dessus le permet — de façon fiable, répétée,
et sans nécessiter un reboot en offrande aux dieux du réseau.

← Précédent
Proxmox Backup Server vs Veeam pour VMware : lequel est meilleur pour des restaurations rapides et des opérations simples
Suivant →
VPN d’entreprise + RDP : Bureau à distance sécurisé sans exposer RDP sur Internet

Laisser un commentaire