Docker « connection refused » entre services : corrigez les réseaux, pas les symptômes

Cet article vous a aidé ?

« Connection refused » est un message merveilleusement direct. Il ne signifie pas que votre application « est juste un peu lente aujourd’hui ».
Cela veut dire que quelque chose a tenté d’ouvrir une connexion TCP et que l’autre extrémité a répondu « non » immédiatement.
Pas de négociation. Pas d’attente. Juste une porte fermée.

Dans les stacks Docker, cette porte se referme pour des raisons prévisibles : vous appelez la mauvaise adresse, vous êtes sur le mauvais réseau,
vous touchez le mauvais port, le serveur n’écoute pas à l’endroit attendu, ou quelque chose entre les deux filtre le trafic.
Cet article explique comment prouver rapidement laquelle de ces causes est en jeu — et corriger le modèle réseau au lieu de saupoudrer des retries comme une eau bénite.

Ce que signifie réellement « connection refused » (et ce que ça n’est pas)

« Connection refused » est la façon pour TCP de dire : l’IP de destination est joignable, mais rien n’écoute sur ce port
(ou quelque chose a activement rejeté la connexion avec un RST TCP). C’est un mode d’échec très différent de :

  • Timeout : les paquets disparaissent, le routage est cassé, le pare-feu laisse tomber, ou le service est bloqué et ne répond pas.
  • Échec de résolution de nom : vous n’obtenez même pas d’adresse IP pour le nom cible.
  • Connection reset by peer : vous vous êtes connecté, puis l’application vous a fermé en plein vol.

Dans Docker, « refused » signifie souvent que vous vous êtes connecté au mauvais endroit avec succès. Ça semble contradictoire jusqu’à ce que l’on réalise
combien de fois des développeurs visent accidentellement localhost, ou le port publié sur l’hôte depuis l’intérieur du même réseau,
ou une IP de conteneur qui a changé depuis mardi dernier.

Voici la règle que vous pouvez coller sur votre écran : à l’intérieur d’un conteneur, « localhost » désigne le conteneur.
Si vous vous connectez d’un conteneur à un autre, localhost est presque toujours faux à moins que vous n’exécutiez explicitement les deux processus
dans le même conteneur (ce qui est un choix d’architecture en soi).

Un modèle mental du réseau Docker pour intervenir sous pression

Le réseau Docker n’est pas « difficile ». Il est simplement en couches. Les problèmes surviennent quand on devine quelle couche est en cause.
Nous n’allons pas deviner. Nous allons le prouver.

Couche 1 : Processus et socket

Quelque chose doit écouter sur un port. Si votre service écoute sur 127.0.0.1 à l’intérieur de son conteneur, les autres conteneurs ne peuvent pas l’atteindre.
Il doit se lier à 0.0.0.0 (ou à l’adresse de l’interface du conteneur).

Couche 2 : Namespace réseau du conteneur

Chaque conteneur a son propre namespace réseau : ses propres interfaces, routes et loopback. Les conteneurs peuvent être attachés à un ou plusieurs réseaux.
Docker crée une paire veth pour connecter le namespace du conteneur à un bridge (pour les réseaux bridge) ou à un overlay (pour Swarm).

Couche 3 : Réseaux Docker (bridge/overlay/macvlan)

Le réseau « bridge » par défaut n’est pas le même qu’un réseau bridge défini par l’utilisateur. Les réseaux définis par l’utilisateur fournissent une découverte de service basée sur le DNS.
Compose s’appuie là-dessus. Si vous retombez sur le bridge par défaut et commencez à coder des IP en dur, vous écrivez des incidents futurs.

Couche 4 : Découverte de services (DNS Docker)

Sur les réseaux définis par l’utilisateur, Docker exécute un serveur DNS intégré. Les conteneurs le voient souvent comme 127.0.0.11 dans /etc/resolv.conf.
Les noms de service Compose résolvent en IPs de conteneurs sur ce réseau. Si la résolution de noms est erronée, tout en aval devient chaotique.

Couche 5 : Publication de ports et NAT sur l’hôte

ports: dans Compose publie des ports de conteneur vers l’hôte. C’est pour le trafic venant de l’extérieur de Docker (votre laptop, l’hôte, d’autres machines).
À l’intérieur du réseau Docker, les conteneurs devraient normalement communiquer entre eux sur le port du conteneur via le nom de service.

Si vous vous retrouvez à connecter le conteneur A vers host.docker.internal:5432 pour atteindre le conteneur B, faites une pause et demandez-vous :
« Pourquoi est-ce que je quitte le réseau Docker pour repasser par le NAT ? » Parfois c’est nécessaire. La plupart du temps, non.

Une citation, parce que l’ops a des preuves

Idée paraphrasée de Werner Vogels (fiabilité/architecture) : « Tout échoue ; concevez en supposant l’échec et récupérez automatiquement. »
Cela s’applique ici : arrêtez d’espérer que le réseau se comportera ; concevez pour l’observer et le vérifier.

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

Quand la production brûle, vous n’avez pas besoin d’un doctorat en philosophie. Vous avez besoin d’une séquence qui réduit l’espace de recherche.
Ce playbook est conçu pour « le service A ne peut pas se connecter au service B » avec « connection refused ».

Premier : confirmez que vous visez la bonne cible depuis le conteneur appelant

  • Depuis l’intérieur du conteneur A : résolvez le nom que vous utilisez.
  • Confirmez que l’IP est sur un réseau partagé avec le conteneur B.
  • Tentez une connexion TCP vers le port prévu.

Si la résolution de noms échoue ou pointe vers un endroit inattendu, arrêtez-vous. Corrigez le DNS/l’appartenance au réseau d’abord.

Second : confirmez que le conteneur serveur écoute réellement sur la bonne interface et le bon port

  • À l’intérieur du conteneur B : listez les sockets en écoute.
  • Vérifiez que le service est lié à 0.0.0.0, pas à 127.0.0.1.
  • Vérifiez les logs de l’application pour « démarré » vs « planté et redémarrant ».

Troisième : inspectez la plomberie réseau Docker et le filtrage sur l’hôte

  • Inspectez les attachements réseau et les IPs des conteneurs.
  • Vérifiez les règles iptables/nftables si l’hôte est impliqué (ports publiés, ou trafic traversant des namespaces).
  • Vérifiez l’isolation réseau accidentelle (plusieurs projets Compose, plusieurs réseaux, alias incorrects).

Blague #1 : Si votre correctif est « add sleep 10 », vous n’avez pas résolu le réseau — vous avez juste négocié avec le temps, et le temps vous facturera plus tard.

Tâches pratiques : commandes, sorties attendues et décisions

Ci-dessous des tâches pratiques à exécuter sur un hôte Linux avec Docker et Compose. Chaque tâche inclut une commande, une sortie réaliste,
ce que cette sortie signifie, et la décision à prendre. C’est la partie que vous copiez dans votre canal d’incident.

Task 1: Identify the failing connection details (from logs)

cr0x@server:~$ docker logs --tail=50 api
2026-01-03T09:12:41Z ERROR db: dial tcp 127.0.0.1:5432: connect: connection refused
2026-01-03T09:12:41Z INFO  retrying in 1s

Signification : Le conteneur API tente d’atteindre Postgres sur 127.0.0.1 dans son propre namespace réseau.
À moins que Postgres ne s’exécute dans le même conteneur, c’est incorrect.

Décision : Changez l’hôte de la BD pour le nom de service Compose (par exemple db) et utilisez le port du conteneur (5432).

Task 2: Confirm container state isn’t lying to you

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES     STATUS                        PORTS
api       Up 2 minutes                  0.0.0.0:8080->8080/tcp
db        Restarting (1) 5 seconds ago  5432/tcp

Signification : La base de données redémarre en boucle. « Connection refused » peut être réel (rien à l’écoute), pas un problème de routage.

Décision : Arrêtez d’ajuster le client. Réparez d’abord la boucle de crash du conteneur serveur : inspectez les logs et la config de la BD.

Task 3: Inspect the server logs for bind/listen issues

cr0x@server:~$ docker logs --tail=80 db
2026-01-03 09:12:32.177 UTC [1] LOG:  listening on IPv4 address "127.0.0.1", port 5432
2026-01-03 09:12:32.177 UTC [1] LOG:  listening on IPv6 address "::1", port 5432
2026-01-03 09:12:32.180 UTC [1] LOG:  database system is ready to accept connections

Signification : Postgres n’écoute que sur le loopback à l’intérieur de son conteneur. Les autres conteneurs recevront « refused ».

Décision : Configurez Postgres pour écouter sur 0.0.0.0 (ou l’interface du conteneur) et ajustez pg_hba.conf en conséquence.
Dans les conteneurs, « sécuriser en se liant à localhost » est du théâtre de sécurité avec des dommages collatéraux.

Task 4: Prove name resolution inside the caller container

cr0x@server:~$ docker exec -it api getent hosts db
172.21.0.3      db

Signification : Le DNS Docker peut résoudre db en une IP. C’est un prérequis pour la découverte de services.

Décision : Si cela échoue, vous êtes sur le mauvais réseau ou utilisez le mauvais nom. Corrigez les réseaux/alias Compose, pas l’application.

Task 5: Test the TCP port from the caller container (fast signal)

cr0x@server:~$ docker exec -it api bash -lc 'nc -vz -w2 db 5432; echo exit=$?'
nc: connect to db (172.21.0.3) port 5432 (tcp) failed: Connection refused
exit=1

Signification : Vous avez atteint l’IP du conteneur, mais le port a refusé. C’est presque toujours « pas d’écoute » ou « écoute sur loopback seulement ».

Décision : Inspectez les sockets en écoute et l’adresse de bind du conteneur serveur (tâche suivante).

Task 6: Check what the server is listening on (inside server container)

cr0x@server:~$ docker exec -it db bash -lc 'ss -lntp | head -n 20'
State  Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0      4096   127.0.0.1:5432      0.0.0.0:*     users:(("postgres",pid=1,fd=6))

Signification : Postgres est lié à 127.0.0.1:5432 seulement. Les conteneurs sur le même réseau ne peuvent pas se connecter.

Décision : Reconfigurez le service pour se lier à 0.0.0.0 (ou à l’IP du conteneur) et redémarrez.

Task 7: Confirm both containers share the same Docker network

cr0x@server:~$ docker inspect -f '{{json .NetworkSettings.Networks}}' api
{"appnet":{"IPAMConfig":null,"Links":null,"Aliases":["api","api-1"],"NetworkID":"c0b3f6f2e2c1","EndpointID":"8d6d...","Gateway":"172.21.0.1","IPAddress":"172.21.0.2","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:15:00:02","DriverOpts":null}}
cr0x@server:~$ docker inspect -f '{{json .NetworkSettings.Networks}}' db
{"appnet":{"IPAMConfig":null,"Links":null,"Aliases":["db","db-1"],"NetworkID":"c0b3f6f2e2c1","EndpointID":"a21c...","Gateway":"172.21.0.1","IPAddress":"172.21.0.3","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:15:00:03","DriverOpts":null}}

Signification : Les deux sont sur appnet avec le même NetworkID. Le routage entre eux devrait donc fonctionner.

Décision : Concentrez-vous sur la configuration d’écoute du serveur et la disponibilité de l’appli, pas sur des bizarreries multi-réseaux.

Task 8: Inspect the network object for surprises (subnet, containers, options)

cr0x@server:~$ docker network inspect appnet --format '{{json .IPAM.Config}} {{json .Containers}}'
[{"Subnet":"172.21.0.0/16","Gateway":"172.21.0.1"}] {"1c2f...":{"Name":"api","IPv4Address":"172.21.0.2/16"},"7aa9...":{"Name":"db","IPv4Address":"172.21.0.3/16"}}

Signification : Subnet et adhésion confirmés. Si la BD n’est pas listée, elle n’est pas sur le réseau que vous pensez.

Décision : Si l’appartenance est incorrecte : corrigez Compose pour attacher les services au même réseau défini par l’utilisateur et redéployez.

Task 9: Detect the “published port confusion” problem

cr0x@server:~$ docker port db
5432/tcp -> 0.0.0.0:15432

Signification : Le conteneur DB expose 5432 en interne, publié comme 15432 sur l’hôte.
Les autres conteneurs devraient quand même utiliser db:5432, pas db:15432 et pas localhost:15432.

Décision : Si la config de l’application pointe vers 15432 depuis l’intérieur de Docker, corrigez-la. Les ports publiés sont pour les clients externes.

Task 10: Validate routing from inside the caller container

cr0x@server:~$ docker exec -it api ip route
default via 172.21.0.1 dev eth0
172.21.0.0/16 dev eth0 proto kernel scope link src 172.21.0.2

Signification : Le conteneur a une route vers le réseau où vit db. Si la route manque, vous avez attaché le mauvais réseau.

Décision : Route manquante = mauvais attachement réseau. Corrigez les réseaux Compose, ne piratez pas /etc/hosts.

Task 11: Validate the caller’s DNS config points at Docker DNS

cr0x@server:~$ docker exec -it api cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0

Signification : Le DNS intégré de Docker est utilisé. Si vous voyez seulement des serveurs DNS d’entreprise, la résolution de noms vers les services peut échouer.

Décision : Si le DNS Docker n’est pas utilisé, vérifiez les paramètres DNS sur le démon ou dans Compose.

Task 12: Prove that the service is reachable when binding is fixed

cr0x@server:~$ docker exec -it db bash -lc 'grep -E "^(listen_addresses|port)" -n /var/lib/postgresql/data/postgresql.conf | head'
60:listen_addresses = '*'
64:port = 5432
cr0x@server:~$ docker restart db
db
cr0x@server:~$ docker exec -it api bash -lc 'nc -vz -w2 db 5432; echo exit=$?'
Connection to db (172.21.0.3) 5432 port [tcp/postgresql] succeeded!
exit=0

Signification : Nous avons converti « refused » en « succeeded » en corrigeant l’écoute.

Décision : Verrouillez le changement de configuration, ajoutez un check de readiness, et retirez tout « retry » client infini.

Task 13: Catch the “depends_on means ready” misconception

cr0x@server:~$ docker compose ps
NAME           IMAGE            COMMAND                  SERVICE   STATUS          PORTS
stack-api-1    api:latest       "/app/api"               api       Up 20 seconds   0.0.0.0:8080->8080/tcp
stack-db-1     postgres:16      "docker-entrypoint..."   db        Up 22 seconds   5432/tcp

Signification : « Up » n’est pas « ready ». Postgres peut encore exécuter des migrations ou rejouer des WAL. Les clients peuvent voir des refus au démarrage.

Décision : Ajoutez un healthcheck à la BD et déclenchez le démarrage de l’API sur la readiness de la BD (ou implémentez un retry robuste côté client avec backoff borné).

Task 14: Verify health status (when you add healthchecks)

cr0x@server:~$ docker inspect -f '{{.State.Health.Status}}' stack-db-1
healthy

Signification : Les healthchecks vous donnent un signal de readiness fiable. Vous pouvez vous en servir pour l’orchestration et l’alerte.

Décision : Si unhealthy : arrêtez de blâmer le réseau et corrigez l’initialisation de la BD, les identifiants, le disque ou la configuration.

Task 15: Spot host firewall/NAT issues for published ports (host involved)

cr0x@server:~$ sudo iptables -S DOCKER | head
-N DOCKER
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.21.0.2:8080

Signification : Docker injecte des règles pour forwarder les ports hôtes vers les IPs de conteneurs. Si ces règles manquent, les ports publiés ne fonctionneront pas.

Décision : Si les règles manquent ou que votre environnement utilise des politiques nftables qui supplantent Docker : alignez la gestion du pare-feu avec Docker,
ou utilisez un mode réseau différent volontairement. Ne « videz pas iptables » en production sauf si vous aimez les audits surprises.

Task 16: Detect accidental multiple Compose projects on separate networks

cr0x@server:~$ docker network ls --format 'table {{.Name}}\t{{.Driver}}\t{{.Scope}}' | grep -E 'stack|appnet'
NAME                DRIVER    SCOPE
stack_default        bridge    local
billing_default      bridge    local

Signification : Deux projets, deux réseaux par défaut. Un conteneur sur stack_default ne peut pas atteindre des services sur billing_default par nom.

Décision : Attachez les deux stacks à un réseau utilisateur partagé (explicitement), ou exécutez-les comme un seul projet Compose si ils sont couplés.

Trois mini-récits d’entreprise issus du terrain

Mini-récit 1 : L’incident causé par une mauvaise hypothèse (« localhost est la base de données »)

Une équipe produit a migré un monolithe en « microservices » sur un trimestre. Ce n’était pas une conversion religieuse ; c’était une ligne budgétaire.
La première étape fut d’exécuter l’API et Postgres dans Docker Compose localement, puis de promouvoir cette configuration dans un environnement dev partagé.

À l’époque du monolithe, la base vivait sur la même VM. Donc la config partout disait DB_HOST=localhost.
Lors de la migration, quelqu’un a mis Postgres dans un conteneur mais a gardé l’ancienne variable, pensant que Docker « mapperait » ça.
Docker a mappé — directement au mauvais endroit.

Le symptôme fut immédiat : les conteneurs API lançaient connect: connection refused. La première réponse fut d’augmenter les retries,
car l’équipe avait récemment souffert de cold starts en Kubernetes. Les retries sont passés de 3 à 30, et les logs sont devenus un roman coûteux.
Ça n’a rien résolu, parce que vous ne pouvez pas rendre eventual-consistent une socket qui n’existe pas.

La percée est venue quand quelqu’un a exécuté getent hosts dans le conteneur et a remarqué que le nom de service db se résolvait correctement.
L’API ne l’utilisait tout simplement pas. Un changement de config plus tard — DB_HOST=db — l’incident était clos.
La leçon n’était pas « utilisez des noms de service ». La leçon était : les hypothèses sont une dette technique avec une date d’échéance.

Mini-récit 2 : L’optimisation qui a échoué (ports publiés pour la « performance »)

Une autre organisation avait un environnement d’intégration basé sur Compose. Un ingénieur senior (compétent, sincèrement) a décidé de « simplifier le réseau »
en faisant communiquer les services via des ports publiés sur l’hôte. La logique semblait propre :
« Tout pointe vers l’IP de l’hôte, on utilise une seule politique de firewall, et ce sera plus simple à déboguer. »

Ça a marché… jusqu’à ce que ça ne marche plus. Sous charge, ils ont commencé à voir des « connection refused » intermittents du service A vers le service B.
Les refus se concentraient pendant les déploiements, mais aussi aléatoirement aux heures de pointe. C’est le genre de comportement qui pousse à blâmer le fournisseur cloud,
le noyau, et parfois l’astrologie.

Le problème réel était l’auto-complexification. Le trafic entrant faisait un détour : conteneur A → NAT hôte → docker-proxy/iptables → conteneur B.
Pendant les redémarrages de conteneurs et le churn d’IP, les fenêtres temporelles s’élargissaient. Certaines connexions tombaient sur un mapping de port pointant brièvement vers nulle part.
De plus, les appels internes étaient maintenant liés à des adresses spécifiques à l’hôte, compliquant la montée en charge horizontale et les basculements.

Ils sont revenus à du trafic direct service-à-service sur le réseau Docker défini par l’utilisateur, en utilisant des noms de services et des ports de conteneurs.
Pour le débogage et l’ingress, ils ont conservé des ports publiés, mais les appels internes sont restés internes. Leur « optimisation » était en fait un détour par davantage d’éléments mobiles.

Mini-récit 3 : La pratique ennuyeuse qui a sauvé la mise (healthchecks et réseaux explicites)

Une équipe plateforme exécutait un stack Compose modeste pour des outils internes : API, queue, Postgres, et un worker. Rien d’exotique.
Ce qui était remarquable, c’est qu’ils le traitaient comme de la prod : réseaux utilisateurs explicites, healthchecks, et noms de services prévisibles.
Pas de defaults magiques. Pas d’« ça marche sur ma machine ».

Un vendredi, l’hôte a redémarré après un patch noyau. Les services sont revenus, mais l’API a commencé à générer des erreurs immédiatement.
L’on-call a vu « connection refused » et s’est préparé à une longue soirée. Puis il a vérifié la santé de la BD : starting.
Postgres rejouait des WAL après un arrêt non propre — normal, mais ça prend du temps.

Grâce aux healthchecks, le conteneur API n’a pas assailli la BD avec une tempête de connexions.
Il a attendu. Les logs sont restés ennuyeux. Les alertes étaient pertinentes. Dix minutes plus tard, tout était healthy et personne n’a rédigé de statut paniqué.

L’ennuyeux est un accomplissement. « Ça redémarre correctement » n’est pas une propriété par défaut ; c’est quelque chose que l’on conçoit.

Erreurs courantes : symptôme → cause racine → correctif

Voici les schémas récurrents derrière « connection refused » dans des systèmes conteneurisés. Chacun inclut une action corrective spécifique.
Si votre équipe répète l’un d’eux, transformez-le en règle de lint ou en élément de checklist de revue.

1) « L’API ne peut pas atteindre la BD, mais la BD tourne » → DB liée au loopback → binder sur 0.0.0.0

  • Symptôme : connect: connection refused depuis d’autres conteneurs ; ss montre 127.0.0.1:5432.
  • Cause racine : Le service n’écoute que sur le loopback à l’intérieur du conteneur.
  • Fix : Configurez l’adresse de bind/écoute sur 0.0.0.0 (ou l’IP du conteneur), plus des règles d’auth appropriées (ex. pg_hba.conf pour Postgres).

2) « Marche sur l’hôte, échoue dans le conteneur » → utilisation de localhost → utilisez le nom de service

  • Symptôme : La config de l’appli utilise localhost ou 127.0.0.1 pour un autre service.
  • Cause racine : Malentendu sur les namespaces réseau.
  • Fix : Utilisez le nom de service Compose (ex. redis, db) et le port du conteneur.

3) « Connection refused seulement au démarrage » → la readiness n’est pas garantie → ajoutez healthchecks/backoff

  • Symptôme : Quelques secondes/minutes après le déploiement : refus ; plus tard : OK.
  • Cause racine : Le client démarre avant que le serveur n’écoute (ou avant qu’il soit prêt à accepter des connexions).
  • Fix : Healthchecks et gating des dépendances ; ou retry client avec backoff exponentiel borné et jitter.

4) « Je peux pinguer, mais TCP est refusé » → le réseau est OK, le port ne l’est pas → arrêtez de déboguer la couche L3

  • Symptôme : IP joignable, ARP/routage OK, mais nc échoue avec refused.
  • Cause racine : Service arrêté, mauvais port, mauvaise adresse de bind, ou crash applicatif.
  • Fix : Vérifiez ss -lntp et les logs du service ; vérifiez le mapping de ports et la config.

5) « Le nom de service ne se résout pas » → mauvais réseau/projet → attachez au même réseau utilisateur

  • Symptôme : getent hosts db échoue dans un conteneur.
  • Cause racine : Les conteneurs sont sur des réseaux différents ou dans des projets Compose différents sans réseau partagé.
  • Fix : Déclarez un réseau explicite partagé dans Compose et attachez-y les deux services.

6) « Connexion au port publié depuis un autre conteneur » → détour NAT → utilisez le port du conteneur sur le réseau

  • Symptôme : Un conteneur appelle host:15432 ou service:15432 parce que 15432 est publié.
  • Cause racine : Confusion entre ports d’ingress et ports internes.
  • Fix : Pour le trafic interne : db:5432. Publiez les ports seulement pour les clients externes.

7) « Refus intermittents après redéploiement » → hypothèses d’IP obsolètes → arrêtez d’utiliser les IP de conteneur

  • Symptôme : IP codée en dur fonctionne jusqu’au redémarrage, puis refus/timeout.
  • Cause racine : Les IP des conteneurs changent ; la config applicative ne suit pas.
  • Fix : Utilisez la découverte par nom, pas les adresses IP de conteneurs.

8) « Port publié mort depuis l’extérieur » → conflit firewall/nftables → alignez le filtrage hôte avec Docker

  • Symptôme : Port hôte mappé, conteneur à l’écoute, mais les clients externes reçoivent un refus.
  • Cause racine : Les règles de pare-feu de l’hôte supplantent le DNAT/forwarding de Docker, ou les règles Docker n’ont pas été installées proprement.
  • Fix : Corrigez la politique de pare-feu pour permettre le forwarding ; assurez l’intégration des chaînes Docker ; évitez de gérer iptables avec deux systèmes concurrents.

Blague #2 : Le réseau Docker n’est pas hanté ; il en a juste l’air quand vous sautez l’étape où vous vérifiez dans quel univers vos paquets sont.

Listes de contrôle / plan étape par étape

Étape par étape : de « refused » à la cause racine en 10 minutes

  1. Identifiez la cible exacte depuis le log client : hôte, port, protocole. Si c’est localhost, supposez que c’est faux jusqu’à preuve du contraire.
  2. Vérifiez l’état/health du conteneur serveur : le serveur redémarre-t-il ou est-il unhealthy ?
  3. Depuis le conteneur client : résolvez le nom du serveur et capturez l’IP.
  4. Depuis le conteneur client : tentez une connexion TCP avec nc vers le nom et le port du serveur.
  5. Depuis le conteneur serveur : confirmez qu’un listener existe avec ss -lntp.
  6. Confirmez l’adresse de bind : si c’est 127.0.0.1, corrigez en 0.0.0.0 (et renforcez l’auth correctement).
  7. Confirmez l’appartenance réseau : les deux conteneurs sont sur le même réseau utilisateur ; inspectez l’objet réseau.
  8. Éliminez la confusion des ports : les appels internes utilisent le port du conteneur ; les appels externes utilisent le port publié.
  9. Inspectez ensuite les règles firewall/NAT de l’hôte, si l’hôte fait partie du chemin.
  10. Après correction : ajoutez des healthchecks/readiness, supprimez les sleeps cultures et documentez le contrat réseau.

Checklist de déploiement : prévenir les « refused » avant qu’ils n’apparaissent

  • Utilisez un réseau défini par l’utilisateur ; ne comptez pas sur le bridge par défaut.
  • Utilisez les noms de service pour le trafic inter-services ; ne codez jamais les IP des conteneurs en dur.
  • Lie les services réseau à 0.0.0.0 à l’intérieur des conteneurs sauf raison spécifique contraire.
  • Publiez les ports seulement pour l’ingress ; ne faites pas transiter le trafic interne par l’hôte.
  • Ajoutez des healthchecks pour les services stateful (BD, cache, queue) ; utilisez le statut pour l’orchestration.
  • Implémentez des retries bornés avec backoff et jitter côté client ; considérez cela comme de la résilience, pas un pansement.
  • Gardez la politique de pare-feu cohérente avec Docker ; évitez les gestionnaires de règles concurrents.
  • Nommez explicitement les réseaux Compose quand plusieurs projets doivent communiquer.

Faits intéressants et contexte historique (utiles, pas triviaux)

  • Fait 1 : Les premières installations Docker reposaient beaucoup sur des bridges Linux et iptables NAT ; les ports publiés sont encore implémentés via des règles DNAT sur de nombreux systèmes.
  • Fait 2 : Le réseau bridge par défaut se comportait historiquement différemment des bridges définis par l’utilisateur, surtout autour de la découverte DNS/automatique.
  • Fait 3 : Le DNS embarqué de Docker apparaît souvent comme 127.0.0.11 à l’intérieur des conteneurs sur les réseaux définis par l’utilisateur — un détail diagnostic précieux.
  • Fait 4 : « Connection refused » est typiquement un RST TCP immédiat, ce qui signifie que le chemin réseau jusqu’à l’IP a fonctionné ; c’est le endpoint qui a rejeté le port.
  • Fait 5 : Les noms de service Compose sont devenus un mécanisme de découverte de services de facto pour dev/test bien avant que beaucoup d’équipes n’adoptent des systèmes « réels » de discovery.
  • Fait 6 : Les IP de conteneurs sont volontairement éphémères ; l’adressage stable s’obtient par le nommage et la discovery, pas en pinnant des IPs.
  • Fait 7 : Certaines distributions sont passées d’iptables à nftables ; les outils de pare-feu mismatching peuvent produire des échecs réseau Docker confus si les chaînes ne sont pas intégrées correctement.
  • Fait 8 : depends_on dans Compose n’a jamais garanti la readiness ; c’est un ordre de démarrage. Traitez la « readiness » comme une propriété applicative.
  • Fait 9 : Se binder sur 127.0.0.1 à l’intérieur d’un conteneur est un classique piège car cela bloque silencieusement tout le trafic externe tout en semblant « sécurisé ».

FAQ

1) Pourquoi ai-je « connection refused » au lieu d’un timeout ?

Refused signifie généralement que vous avez atteint l’IP de destination et que le kernel a répondu par un reset parce que rien n’écoute sur ce port
(ou qu’un pare-feu a activement rejeté). Les timeouts concernent plutôt les paquets abandonnés et les chemins cassés.

2) Si les deux services sont dans Compose, pourquoi ne peuvent-ils pas communiquer automatiquement ?

Ils le peuvent, mais seulement s’ils partagent un réseau et que vous utilisez des noms de service. Les problèmes surviennent quand les services atterrissent sur des réseaux différents,
dans des projets Compose différents, ou que le client est configuré pour utiliser localhost ou un port publié sur l’hôte.

3) Dois-je utiliser les adresses IP des conteneurs pour la performance ?

Non. Les IP des conteneurs changent. La résolution de noms n’est pas votre goulot d’étranglement ; votre prochaine panne l’est.
Utilisez les noms de service et laissez Docker DNS gérer le mappage.

4) Quelle est la différence entre expose et ports dans Compose ?

expose documente les ports internes et peut influencer des comportements liés au linking, mais il ne publie pas vers l’hôte.
ports publie des ports sur l’interface hôte (généralement via NAT). Le trafic conteneur-à-conteneur n’a pas besoin de ports publiés.

5) depends_on suffit-il à empêcher les échecs de connexion au démarrage ?

Non. Il démarre les conteneurs dans un ordre ; il ne garantit pas que la dépendance est prête à accepter des connexions.
Utilisez des healthchecks et/ou un retry client avec un backoff raisonnable.

6) Pourquoi le serveur écoute-t-il sur 127.0.0.1 à l’intérieur du conteneur ?

Beaucoup de services par défaut écoutent le loopback pour « sécurité ». Dans les conteneurs, cela bloque souvent le seul trafic que vous voulez réellement : les autres conteneurs.
Liez à 0.0.0.0 et sécurisez par authentification/ACL plutôt que de vous cacher derrière le loopback.

7) Les pare-feu peuvent-ils provoquer « connection refused » dans Docker ?

Oui. Des règles de reject peuvent générer des RST/réponses ICMP qui ressemblent à un refus. Plus couramment, les pare-feu provoquent des timeouts en laissant tomber des paquets.
Si votre chemin implique des ports publiés ou du routage inter-hôtes, validez l’intégration du pare-feu hôte avec Docker.

8) Les conteneurs doivent-ils s’appeler via le port publié de l’hôte ?

Généralement non. Cela ajoute du NAT, des modes de défaillance supplémentaires et du couplage à l’hôte. Utilisez le réseau service-à-service via le réseau Docker partagé.
Publiez des ports pour les clients hors Docker.

9) Pourquoi ça marche localement mais échoue en CI ou sur un serveur partagé ?

Les setups locaux ont souvent moins de réseaux, moins de projets Compose et moins de règles de pare-feu. En CI/environnements partagés,
les noms de réseaux peuvent se chevaucher, les services démarrent dans un ordre différent, et les baselines de firewall peuvent varier. Rendez les réseaux explicites et ajoutez des healthchecks.

10) Et si le DNS se résout correctement mais que j’ai toujours des refus ?

Alors le DNS n’est pas le problème. « Refused » pointe vers des sockets d’écoute, des adresses de bind, des ports incorrects ou des crashes du processus serveur.
Exécutez ss -lntp dans le conteneur serveur et vérifiez que le port écoute sur 0.0.0.0.

Conclusion : étapes pratiques suivantes

« Connection refused » n’est pas un mystère ; c’est un diagnostic qui demande à être bouclé. Votre travail est d’arrêter de le traiter comme un événement météo.
Prouvez la cible, prouvez la résolution de noms, prouvez l’appartenance réseau, prouvez l’écoute, et ensuite seulement discutez des pare-feu.

Étapes suivantes qui paient :

  • Auditez toutes les configs inter-services pour localhost, IP hôtes et ports publiés utilisés en interne. Remplacez par des noms de service et des ports de conteneurs.
  • Ajoutez des healthchecks pour les services stateful et faites en sorte que vos clients gèrent le démarrage avec un backoff borné.
  • Rendez les réseaux Compose explicites, surtout quand plusieurs projets doivent communiquer.
  • Standardisez un runbook court : getent, nc, ss, docker network inspect. Entraînez-le jusqu’à ce que ça devienne réflexe.
← Précédent
Ubuntu 24.04 : Déconnexions aléatoires — diagnostiquer les pertes NIC et les offloads sans superstition
Suivant →
Timeouts aléatoires Debian/Ubuntu : tracer le chemin réseau avec mtr/tcpdump et corriger la cause (Cas n°64)

Laisser un commentaire