← Retour aux tutoriels

Déboguer « Connection Refused » et les timeouts entre conteneurs Docker

dockerdocker-composereseaudebuggingconnection-refusedtimeoutdnsports

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 :

Exemples :

Causes typiques :

1.2 Timeout (pas de réponse)

Caractéristiques :

Exemples :

Causes typiques :


2) Rappels essentiels sur le réseau Docker (bridge, DNS, ports)

2.1 Le réseau bridge et les réseaux user-defined

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

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 :

  1. B tourne-t-il ?

    docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
  2. A et B sont-ils sur le même réseau ?

    docker inspect -f '{{json .NetworkSettings.Networks}}' A | jq
    docker inspect -f '{{json .NetworkSettings.Networks}}' B | jq
  3. Le nom DNS B résout-il depuis A ?
    Dans A :

    getent hosts B
  4. Le port est-il ouvert (service en écoute) dans B ?
    Dans B :

    ss -lntp
  5. Tester la connectivité TCP depuis A vers B :
    Dans A :

    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 :

Si vous ne voyez rien sur le port attendu, c’est souvent :

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 :

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

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 :

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 :

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 :


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 :

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 :


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 :

É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"

É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 :

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)

  1. localhost utilisé au lieu du nom du service

    • Symptôme : ça marche depuis l’hôte, pas depuis un conteneur.
  2. Service écoute sur 127.0.0.1

    • Symptôme : refused depuis l’extérieur, OK dans le conteneur même.
  3. Mauvais port interne

    • Symptôme : refused sur :8080 alors que l’app écoute :8000.
  4. Conteneurs sur réseaux différents

    • Symptôme : timeout, DNS parfois absent.
  5. Firewall / iptables

    • Symptôme : timeout “dur”, pas de SYN-ACK, ou trafic asymétrique.
  6. Proxy

    • Symptôme : le client tente de joindre un proxy au lieu du service interne.
  7. 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 :

Si vous me donnez :