Déboguer « Connection Refused » et les timeouts entre conteneurs Docker
Les erreurs « Connection refused » et les timeouts (délai d’attente dépassé) entre conteneurs Docker sont parmi les problèmes réseau les plus fréquents. Elles ont des causes très différentes : service non démarré, mauvais port, mauvaise interface d’écoute, DNS interne, réseau Docker incorrect, règles iptables, proxy, healthchecks, dépendances mal ordonnées, etc.
Ce tutoriel propose une méthode systématique pour diagnostiquer et corriger ces pannes, avec des commandes réelles et des explications approfondies.
1) Comprendre précisément l’erreur : « refused » vs timeout
Avant de lancer des commandes, il faut distinguer deux familles de symptômes, car elles orientent le diagnostic.
1.1 « Connection refused » (refus immédiat)
Caractéristiques :
- La connexion TCP atteint bien la machine cible (le conteneur / l’IP), mais le port n’a aucun service en écoute (ou le service refuse).
- Le refus est généralement immédiat.
Exemples :
curl: (7) Failed to connect ... Connection refusedtelnet ... Connection refusednc: connect to ... failed: Connection refused
Causes typiques :
- Le processus serveur n’est pas démarré dans le conteneur.
- Le serveur écoute sur un autre port.
- Le serveur écoute seulement sur
127.0.0.1(loopback) au lieu de0.0.0.0. - Le conteneur cible n’est pas celui attendu (mauvais nom DNS / mauvais réseau).
- Le service crash en boucle.
1.2 Timeout (pas de réponse)
Caractéristiques :
- La connexion n’aboutit pas, et expire après un délai.
- Cela indique souvent un problème de routage, de filtrage (iptables, firewall), de réseau (mauvais network), ou un service bloqué.
Exemples :
curl: (28) Connection timed outnc: connect to ... timed outi/o timeoutdans des logs applicatifs
Causes typiques :
- Les conteneurs ne sont pas sur le même réseau Docker.
- Le trafic est bloqué par des règles (iptables, policies).
- Le DNS résout vers une IP inaccessible.
- Le service écoute, mais est saturé/bloqué et ne répond pas.
- Problème d’IPv6 vs IPv4, ou résolution vers une adresse non routable.
2) Rappels essentiels sur le réseau Docker (bridge, DNS, ports)
2.1 Le réseau bridge et les réseaux user-defined
- Le réseau par défaut
bridgeexiste, mais il a des limitations (notamment sur la résolution DNS selon les versions/paramètres). - Les réseaux user-defined (créés par vous) offrent un DNS interne plus fiable : les conteneurs peuvent se joindre par nom.
Créer un réseau :
docker network create appnet
Lancer deux conteneurs dessus :
docker run -d --name api --network appnet nginx:alpine
docker run -it --rm --name client --network appnet alpine:3.19 sh
Dans client, vous pouvez résoudre api :
getent hosts api
2.2 Ports exposés vs ports publiés
EXPOSE 8080(dans un Dockerfile) est informatif.-p 8080:8080publie un port vers l’hôte.- Entre conteneurs sur le même réseau, on n’a pas besoin de
-p. Ils communiquent via l’IP interne et le port interne.
Erreur courante : publier un port vers l’hôte et tenter d’y accéder depuis un autre conteneur via localhost. localhost dans un conteneur = le conteneur lui-même, pas l’hôte, ni un autre conteneur.
3) Checklist rapide (diagnostic en 5 minutes)
Quand un conteneur A n’arrive pas à joindre B :
-
Btourne-t-il ?docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' -
AetBsont-ils sur le même réseau ?docker inspect -f '{{json .NetworkSettings.Networks}}' A | jq docker inspect -f '{{json .NetworkSettings.Networks}}' B | jq -
Le nom DNS
Brésout-il depuisA?
DansA:getent hosts B -
Le port est-il ouvert (service en écoute) dans
B?
DansB:ss -lntp -
Tester la connectivité TCP depuis
AversB:
DansA:nc -vz B 8080
Selon le résultat (refused vs timeout), on creuse.
4) Mettre en place un labo reproductible (pour s’entraîner)
On va créer deux services : une API (Python) et un client.
4.1 Créer un réseau
docker network create debugnet
4.2 Lancer un serveur HTTP simple (Python)
docker run -d --name server --network debugnet -w /srv python:3.12-alpine \
sh -c "python -m http.server 8000 --bind 0.0.0.0"
4.3 Lancer un client
docker run -it --rm --name client --network debugnet alpine:3.19 sh
Dans client :
apk add --no-cache curl bind-tools netcat-openbsd iproute2
curl -v http://server:8000/
Vous devriez obtenir une réponse. Maintenant, nous allons provoquer des pannes et les diagnostiquer.
5) Diagnostiquer « Connection refused »
5.1 Vérifier que le service écoute réellement dans le conteneur cible
Dans server :
docker exec -it server sh
Puis :
ss -lntp
Vous devriez voir quelque chose comme :
LISTEN 0 5 0.0.0.0:8000 ... python
Si vous ne voyez rien sur le port attendu, c’est souvent :
- commande de démarrage incorrecte
- application qui a crash
- application qui écoute sur un autre port
Vérifier les logs :
docker logs --tail=200 server
5.2 Erreur classique : écoute sur 127.0.0.1 au lieu de 0.0.0.0
Modifions le serveur pour écouter uniquement sur loopback :
docker rm -f server
docker run -d --name server --network debugnet -w /srv python:3.12-alpine \
sh -c "python -m http.server 8000 --bind 127.0.0.1"
Depuis client :
curl -v http://server:8000/
Résultat typique : Connection refused.
Pourquoi ?
Dans le conteneur server, 127.0.0.1 signifie « moi-même ». Les connexions venant d’un autre conteneur arrivent sur l’interface réseau du conteneur (ex. eth0), pas sur sa loopback. Le port n’est donc pas accessible.
Correction : écouter sur 0.0.0.0 (toutes les interfaces) :
docker rm -f server
docker run -d --name server --network debugnet -w /srv python:3.12-alpine \
sh -c "python -m http.server 8000 --bind 0.0.0.0"
5.3 Mauvais port : confusion entre port interne et port publié
Supposons un service interne sur 8000. Si vous publiez vers l’hôte en -p 8080:8000, alors :
- depuis l’hôte :
localhost:8080 - depuis un autre conteneur : toujours
server:8000(pas8080), sauf si vous passez par l’hôte (ce qui est rarement souhaitable).
Exemple :
docker rm -f server
docker run -d --name server --network debugnet -p 8080:8000 -w /srv python:3.12-alpine \
sh -c "python -m http.server 8000 --bind 0.0.0.0"
Depuis client, ceci marche :
curl -v http://server:8000/
Mais ceci échoue souvent (refused ou timeout selon le cas) :
curl -v http://server:8080/
Car 8080 n’est pas ouvert dans le conteneur. Le mapping -p est un mécanisme côté hôte.
5.4 Le conteneur cible n’est pas sur le même réseau / mauvais DNS
« Connection refused » peut aussi arriver si le nom server résout vers une IP inattendue (par exemple un autre conteneur, ou un ancien service).
Vérifier la résolution DNS depuis client :
getent hosts server
nslookup server
Vérifier les réseaux :
docker network inspect debugnet | jq '.[0].Containers | keys'
Si server n’apparaît pas, il n’est pas sur le réseau. Connecter un conteneur à un réseau :
docker network connect debugnet server
6) Diagnostiquer les timeouts (le cas « ça pend »)
Un timeout signifie souvent que les paquets n’arrivent pas à destination, ou que la réponse ne revient pas.
6.1 Vérifier d’abord : sont-ils sur le même réseau ?
Depuis l’hôte :
docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' client
docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' server
Si ce n’est pas le même réseau, Docker ne route pas automatiquement entre réseaux bridge distincts.
Solution : mettre les deux services sur le même réseau user-defined (recommandé), ou connecter l’un à l’autre réseau.
6.2 Tester la connectivité IP brute (ping) — avec prudence
Beaucoup d’images n’ont pas ping (ICMP). On peut l’installer, mais ce n’est pas toujours souhaitable en prod. Pour debug :
Dans client :
apk add --no-cache iputils
ping -c 2 server
Si le ping ne marche pas, ce n’est pas forcément bloquant (ICMP peut être filtré). Préférez un test TCP.
6.3 Tester le TCP avec netcat
Dans client :
nc -vz -w 2 server 8000
- Si
succeeded: le port est joignable. - Si
Connection refused: on revient au chapitre précédent. - Si
timed out: problème réseau/filtrage/service bloqué.
6.4 Vérifier les routes et interfaces dans les conteneurs
Dans client :
ip addr
ip route
Dans server :
docker exec -it server sh -c "ip addr && ip route"
Sur un réseau bridge Docker classique, vous verrez une interface eth0 avec une IP privée (souvent 172.x.x.x) et une route par défaut via la gateway du bridge.
Si server résout vers une IP, vérifiez qu’elle correspond :
getent hosts server
6.5 Inspecter le réseau Docker et ses paramètres
docker network inspect debugnet | jq '.[0] | {Name,Driver,IPAM,Options}'
Vous pouvez y voir le subnet, la gateway, etc.
6.6 iptables / nftables : quand l’hôte filtre
Sur Linux, Docker manipule iptables/nftables pour NAT et isolation. Des règles personnalisées ou un firewall (UFW, firewalld) peuvent casser des flux.
Vérifier rapidement (selon votre système) :
sudo iptables -S
sudo iptables -L -n -v
Si votre système utilise nftables :
sudo nft list ruleset
Points d’attention :
- Chaînes
DOCKER,DOCKER-USER - Policies
DROPinattendues - Règles UFW qui bloquent le forwarding
Pour UFW, un symptôme courant : les conteneurs ne peuvent pas se joindre entre eux ou sortir. Vérifier :
sudo ufw status verbose
La résolution dépend de votre politique de sécurité, mais une piste est d’autoriser le forwarding sur l’interface docker (ex. docker0) ou d’ajuster les règles dans DOCKER-USER.
6.7 Cas particulier : vous tentez d’atteindre l’hôte via localhost
Depuis un conteneur, localhost = le conteneur. Pour joindre l’hôte :
- Sur Docker Desktop (Mac/Windows) :
host.docker.internal - Sur Linux : selon versions,
host.docker.internalpeut exister, sinon utiliser la gateway du réseau (souvent172.17.0.1) ou ajouter--add-host.
Trouver la gateway depuis un conteneur :
ip route | awk '/default/ {print $3}'
Puis tester :
curl -v http://$(ip route | awk '/default/ {print $3}'):8080/
7) Docker Compose : dépendances, réseaux et healthchecks
En pratique, beaucoup de « timeouts » viennent d’un ordre de démarrage : le client démarre avant la base de données / API.
7.1 depends_on ne garantit pas la disponibilité
Dans Compose, depends_on garantit l’ordre de démarrage des conteneurs, pas que le service est prêt à accepter des connexions.
Symptôme : l’application tente de se connecter au démarrage, échoue (refused), puis crash ou boucle.
Solution : ajouter un healthcheck au service dépendant et attendre l’état healthy, ou implémenter des retries côté application.
7.2 Exemple : Postgres + application
Lancer un Postgres :
docker run -d --name pg --network debugnet \
-e POSTGRES_PASSWORD=secret postgres:16-alpine
Tester depuis un conteneur client :
docker run -it --rm --network debugnet postgres:16-alpine \
sh -c "pg_isready -h pg -U postgres"
Si votre application démarre trop tôt, pg_isready peut échouer au début. En production, on met des retries.
7.3 Vérifier l’état de santé d’un conteneur
docker inspect -f '{{json .State.Health}}' pg | jq
Si vous voyez Status: unhealthy, votre service n’est pas prêt, et les timeouts/refused sont attendus.
8) Debug “dans” le réseau : conteneur toolbox
Il est souvent utile d’avoir un conteneur “boîte à outils” avec curl, dig, tcpdump, etc.
8.1 Lancer un conteneur netshoot (très pratique)
Image couramment utilisée pour debug réseau.
docker run -it --rm --network debugnet nicolaka/netshoot bash
Tests :
dig server
curl -v http://server:8000/
nc -vz server 8000
8.2 Capturer le trafic avec tcpdump
Dans netshoot :
tcpdump -i eth0 -nn host server and tcp port 8000
Puis, dans un autre terminal, lancez :
docker exec -it client sh -c "curl -v http://server:8000/"
Interprétation rapide :
- Vous voyez des
SYNpartir mais pas deSYN-ACKrevenir : problème de routage/filtrage ou service absent. - Vous voyez
RST(reset) : souvent « connection refused » (port fermé). - Vous voyez handshake OK puis rien : application bloquée / problème HTTP au-dessus du TCP.
9) Problèmes DNS internes Docker
9.1 Quand le nom ne résout pas
Dans un conteneur :
cat /etc/resolv.conf
Sur un réseau user-defined, Docker fournit un DNS interne (souvent 127.0.0.11). Vérifiez :
grep -n . /etc/resolv.conf
Si getent hosts service ne renvoie rien :
- le conteneur cible n’est pas sur le même réseau
- le nom est incorrect
- vous utilisez le réseau
hostou des paramètres DNS custom
9.2 Vérifier les alias et noms Compose
Avec Compose, le nom DNS est souvent le nom du service (et parfois le nom du conteneur). Inspectez :
docker ps --format 'table {{.Names}}\t{{.Image}}'
docker network inspect <votre_reseau> | jq '.[0].Containers[].Name'
10) Cas avancés : IPv6, proxy, MTU, et services qui “écoutent” mais ne répondent pas
10.1 IPv6 vs IPv4
Un client peut résoudre un nom en IPv6 et tenter de s’y connecter, alors que le service n’écoute qu’en IPv4 (ou l’inverse).
Dans le conteneur client :
getent ahosts server
Forcer IPv4 avec curl :
curl -4 -v http://server:8000/
Forcer IPv6 :
curl -6 -v http://server:8000/
10.2 Variables proxy (HTTP_PROXY) qui détournent le trafic
Si HTTP_PROXY/HTTPS_PROXY est défini dans un conteneur, curl et parfois votre application peuvent envoyer la requête vers un proxy externe, ce qui ressemble à un timeout “mystère”.
Vérifier :
env | grep -i proxy
Tester sans proxy :
curl -v --noproxy '*' http://server:8000/
Ou définir NO_PROXY :
export NO_PROXY=server,localhost,127.0.0.1,.local
10.3 MTU (surtout en VPN / cloud)
Des timeouts intermittents (ou uniquement sur gros payloads) peuvent venir d’un MTU incorrect. Symptômes : handshake TCP OK, puis transfert qui bloque.
Voir MTU :
ip link show eth0
Côté hôte, inspecter l’interface docker :
ip link show docker0
Ajuster le MTU est un sujet à part (daemon.json, réseau, overlay), mais gardez-le en tête si les pannes sont “bizarres” et dépendantes de la taille.
10.4 Service “en écoute” mais application non prête
Un serveur peut ouvrir le port très tôt, mais ne pas être prêt (migrations DB, warmup). Résultat : connexion acceptée mais requêtes qui expirent.
Approche :
- vérifier logs applicatifs
- ajouter endpoint
/health - configurer healthcheck Docker
- ajouter timeouts/retries côté client
11) Méthode pas-à-pas : de zéro à la cause racine
Voici une procédure robuste, applicable à la plupart des stacks.
Étape A — Vérifier l’état et les redémarrages
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
docker inspect -f '{{.Name}} RestartCount={{.RestartCount}} OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}' server
Si RestartCount augmente, le service crash : le réseau n’est peut-être pas le problème.
Étape B — Vérifier les logs
docker logs --tail=200 server
Cherchez :
- “listening on …”
- “bind: address already in use”
- erreurs de config (port, host)
- erreurs de connexion DB
Étape C — Vérifier l’écoute du port dans le conteneur cible
docker exec -it server sh -c "ss -lntp || netstat -lntp"
Si le port n’apparaît pas, corriger l’app (bind, port, commande).
Étape D — Vérifier la résolution DNS depuis le conteneur source
docker exec -it client sh -c "getent hosts server && getent ahosts server"
Si ça ne résout pas : réseau, nom, DNS.
Étape E — Tester TCP depuis la source
docker exec -it client sh -c "nc -vz -w 2 server 8000"
- refused : port fermé / bind loopback / mauvais conteneur
- timeout : réseau/iptables/service bloqué
Étape F — Inspecter les réseaux
docker network ls
docker network inspect debugnet | jq '.[0].Containers'
Vérifier que les deux conteneurs sont présents.
Étape G — Capturer le trafic si nécessaire
docker run -it --rm --network debugnet nicolaka/netshoot bash
tcpdump -i eth0 -nn host server and tcp port 8000
12) Corrections typiques (solutions concrètes)
12.1 Mettre tous les services sur un réseau user-defined
docker network create appnet
docker run -d --name db --network appnet postgres:16-alpine
docker run -d --name api --network appnet myapi:latest
docker run -d --name web --network appnet myweb:latest
12.2 Corriger le bind d’écoute
Dans beaucoup de frameworks :
- Node.js : écouter sur
0.0.0.0 - Flask :
app.run(host="0.0.0.0", port=...) - Django :
runserver 0.0.0.0:8000 - Go :
http.ListenAndServe(":8080", ...)(par défaut sur toutes interfaces)
Exemples rapides :
Flask :
app.run(host="0.0.0.0", port=5000)
Django :
python manage.py runserver 0.0.0.0:8000
12.3 Utiliser le bon host côté client : le nom du service
Depuis un conteneur, ciblez http://api:port (nom DNS Docker), pas localhost.
12.4 Ajouter retries et timeouts côté client
Même avec un réseau parfait, un service peut mettre 5–30 secondes à être prêt.
Exemple bash simple :
for i in $(seq 1 30); do
nc -z api 8080 && echo "API OK" && exit 0
echo "Attente API... ($i)"
sleep 1
done
echo "API indisponible"
exit 1
12.5 Vérifier et corriger les variables proxy
unset HTTP_PROXY HTTPS_PROXY ALL_PROXY
export NO_PROXY="api,db,localhost,127.0.0.1"
13) Pièges fréquents (et comment les reconnaître)
-
localhostutilisé au lieu du nom du service- Symptôme : ça marche depuis l’hôte, pas depuis un conteneur.
-
Service écoute sur 127.0.0.1
- Symptôme : refused depuis l’extérieur, OK dans le conteneur même.
-
Mauvais port interne
- Symptôme : refused sur
:8080alors que l’app écoute:8000.
- Symptôme : refused sur
-
Conteneurs sur réseaux différents
- Symptôme : timeout, DNS parfois absent.
-
Firewall / iptables
- Symptôme : timeout “dur”, pas de SYN-ACK, ou trafic asymétrique.
-
Proxy
- Symptôme : le client tente de joindre un proxy au lieu du service interne.
-
Démarrage trop rapide du client
- Symptôme : erreurs au boot, puis tout marche si on redémarre.
14) Conclusion : une approche “scientifique” du debug
Pour résoudre efficacement « Connection refused » et les timeouts entre conteneurs Docker :
- Identifiez le type d’erreur (refused vs timeout).
- Vérifiez service en écoute (
ss -lntp) et logs. - Vérifiez DNS (
getent hosts) et réseau partagé (docker network inspect). - Testez TCP (
nc -vz) plutôt que de supposer. - Utilisez un conteneur toolbox (
netshoot) et, si nécessaire,tcpdump. - Corrigez les causes structurelles : réseau user-defined, bind
0.0.0.0, bons ports, healthchecks, retries.
Si vous me donnez :
- la sortie de
docker ps, - la commande/compose utilisé,
- et un
curl -vounc -vzdepuis le conteneur source, je peux vous guider vers la cause exacte en quelques itérations.