Dépannage des conteneurs Docker : problèmes courants au démarrage et solutions

Diagnostiquez les conteneurs Docker qui se ferment, ne parviennent pas à lier les ports, manquent de fichiers, rencontrent des problèmes de permissions ou sont tués pour cause de mémoire insuffisante.

Dépannage des conteneurs Docker : problèmes courants au démarrage et solutions

Lorsqu'un conteneur Docker ne démarre pas, la solution la plus rapide consiste généralement à résister à l'envie de deviner. Un conteneur n'est qu'un processus avec un système de fichiers, un environnement, des paramètres réseau et des limites qui l'entourent. Si ce processus se termine, Docker enregistre la raison. Votre travail consiste à rassembler les preuves dans le bon ordre.

Je commence généralement par trois questions : Docker a-t-il créé le conteneur, le processus principal a-t-il démarré, et quelque chose d'extérieur au processus l'a-t-il tué ou bloqué ? Ces questions permettent de distinguer un mauvais nom d'image d'une commande cassée, un conflit de port d'un crash d'application, et un problème de permission d'une limite de mémoire.

Commencez par la commande ennuyeuse qui vous dit la vérité :

docker ps -a

Regardez STATUS, PORTS et NAMES. Created signifie que Docker a créé le conteneur mais ne l'a pas réellement fait fonctionner. Exited (1) signifie souvent que l'application a renvoyé une erreur normale. Exited (127) indique généralement une commande manquante. Exited (137) signifie souvent que le processus a été tué de l'extérieur, fréquemment à cause d'une pression mémoire. Ces codes sont des indices, pas des réponses définitives, mais ils vous évitent de déboguer la mauvaise couche.

Ensuite, lisez les logs :

docker logs --tail 100 <conteneur>
docker logs -f <conteneur>

Si le conteneur meurt immédiatement, docker logs est généralement plus utile que de relancer la même commande docker run. Les frameworks d'application impriment souvent la variable d'environnement manquante exacte, l'échec de migration, le fichier de configuration invalide ou l'erreur de liaison avant de se fermer.

Pour l'état de bas niveau, inspectez le conteneur :

docker inspect <conteneur> --format '{{json .State}}'
docker inspect <conteneur> --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{.State.Error}}'

Cette deuxième commande mérite d'être mémorisée. Elle vous indique si Docker a détecté un kill OOM, quel code de sortie a été enregistré et si l'exécution elle-même a eu une erreur.

Si le conteneur se ferme immédiatement

Un conteneur reste actif uniquement tant que son processus principal reste actif. Si la commande se termine, Docker arrête le conteneur. Cela surprend les gens lorsqu'ils exécutent des scripts qui démarrent un démon en arrière-plan puis se terminent.

Par exemple, ce motif se ferme souvent :

CMD service nginx start

La commande service peut démarrer nginx puis se terminer. Docker voit le processus principal se terminer et arrête le conteneur. Le motif adapté aux conteneurs consiste à exécuter le serveur au premier plan :

CMD ["nginx", "-g", "daemon off;"]

La même idée s'applique à Node, Python, Java et aux processus de travail. La commande dans CMD ou ENTRYPOINT doit être le processus de longue durée, pas un lanceur qui met le vrai travail en arrière-plan et se termine.

Si les logs montrent command not found, no such file or directory ou exec format error, testez l'image de manière interactive :

docker run --rm -it --entrypoint sh <image>

Certaines images n'incluent pas bash, en particulier les images Alpine et de style distroless. Utilisez sh en premier sauf si vous savez que bash existe. Une fois à l'intérieur, vérifiez le chemin du fichier, les permissions et l'interpréteur :

ls -l /app
which python || true
head -1 /app/start.sh

Un script peut exister et échouer avec no such file or directory si son shebang pointe vers un interpréteur manquant, comme #!/bin/bash dans une image qui n'a que /bin/sh. Une autre cause courante est les fins de ligne Windows. Si un script shell a été édité sur Windows, le \r invisible peut faire chercher à Linux /bin/sh\r.

Si Docker dit que le port est déjà alloué

Les conflits de port se produisent du côté de l'hôte. Dans -p 8080:80, 8080 est le port hôte et 80 est le port du conteneur. Si quelque chose écoute déjà sur le port hôte 8080, Docker ne peut pas le lier.

Vous pouvez voir une erreur comme bind: address already in use ou port is already allocated. Trouvez l'écouteur :

sudo lsof -i :8080
# ou
sudo ss -ltnp 'sport = :8080'

Sur macOS, lsof est généralement le plus simple. Sur les serveurs Linux, ss est souvent disponible par défaut. Sur Windows PowerShell, utilisez :

Get-NetTCPConnection -LocalPort 8080

Choisissez ensuite un port hôte différent ou arrêtez le service qui le possède :

docker run -d -p 8081:80 nginx

Ne modifiez pas le port du conteneur sauf si l'application à l'intérieur du conteneur écoute réellement sur ce nouveau port. Si nginx écoute sur le port 80 à l'intérieur du conteneur, -p 8081:80 est correct. -p 8081:8081 échouera depuis le navigateur si rien à l'intérieur du conteneur n'écoute sur le port 8081.

Si l'application démarre mais ne trouve pas la configuration

De nombreux échecs de démarrage sont dus à des variables d'environnement manquantes. L'image est bonne, la commande est bonne, mais l'application attend DATABASE_URL, REDIS_URL, une clé API ou un fichier de configuration.

Vérifiez ce que Docker a passé :

docker inspect <conteneur> --format '{{range .Config.Env}}{{println .}}{{end}}'

Pour les projets Compose, inspectez la configuration résolue plutôt que de lire uniquement docker-compose.yml :

docker compose config

Cela permet de détecter les erreurs d'indentation, les surprises du fichier .env et les variables qui se sont développées en chaînes vides. Un exemple réel : DATABASE_URL=${DATABASE_URL} semble inoffensif, mais si le shell ou le fichier .env ne le définit pas, votre application peut recevoir une valeur vide et échouer au démarrage.

Soyez prudent avec les secrets dans les logs et l'historique du terminal. Pour un débogage local rapide, passer -e NAME=value est acceptable. Pour les systèmes partagés, utilisez le mécanisme de secret de votre plateforme ou un fichier d'environnement avec des permissions contrôlées.

Si les montages bind ou les volumes provoquent des erreurs de permission

Un conteneur peut échouer au démarrage car il ne peut pas lire un fichier de configuration, écrire un fichier PID, créer un répertoire de cache ou initialiser un répertoire de base de données. Les logs disent généralement permission denied, read-only file system ou operation not permitted.

Inspectez d'abord le montage :

docker inspect <conteneur> --format '{{json .Mounts}}'

Ensuite, vérifiez sous quel utilisateur le conteneur s'exécute :

docker inspect <conteneur> --format 'user={{.Config.User}}'

Si user est vide, l'image peut s'exécuter en tant que root par défaut, mais de nombreuses images de production définissent un utilisateur non root. Un répertoire hôte appartenant à votre UID local peut ne pas être accessible en écriture par l'UID 1000, 1001 ou un utilisateur spécifique au service à l'intérieur du conteneur.

Une séquence de débogage pratique est :

ls -ld ./data
docker run --rm -it -v "$PWD/data:/data" --entrypoint sh <image>
id
ls -ld /data
touch /data/test

Évitez de résoudre tous les problèmes de permission avec chmod 777. Cela peut masquer le problème immédiat tout en en créant un pire. Préférez faire correspondre la propriété ou utiliser des volumes nommés pour les données d'application :

docker volume create app_data
docker run -d -v app_data:/var/lib/app <image>

Les volumes nommés sont particulièrement utiles sur Docker Desktop, où les montages bind traversent une frontière de virtualisation et peuvent se comporter différemment des systèmes de fichiers Linux natifs.

Si le conteneur a été tué pour cause de mémoire

Le code de sortie 137 est un fort indice que le processus a reçu SIGKILL. Dans le travail Docker, cela signifie souvent que le noyau ou Docker Desktop l'a tué parce que la mémoire était épuisée. Confirmez avec inspect :

docker inspect <conteneur> --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}}'

Si OOMKilled est true, vous avez deux tâches : donner au processus assez de mémoire pour démarrer et comprendre pourquoi il en avait besoin d'autant. Augmenter la limite peut être la bonne correction de production pour un service de base de données ou JVM. Pour un petit service web, cela peut révéler une mauvaise configuration par défaut.

Les applications Java sont un exemple classique. Le comportement des anciennes JVM ne s'adaptait pas toujours bien aux limites des conteneurs, et même les JVM modernes ont encore besoin de paramètres -Xmx ou basés sur un pourcentage pour un comportement prévisible. Les services Node peuvent avoir besoin de --max-old-space-size dans des environnements à mémoire limitée. Les bases de données peuvent nécessiter des paramètres de cache explicites.

Pour un test ponctuel :

docker run --memory=1g <image>

Si vous utilisez Docker Desktop, vérifiez également la mémoire attribuée à la VM Docker. Une limite de conteneur ne peut pas aider si la VM elle-même est affamée.

Si l'image ne se télécharge jamais ou si la build n'a jamais produit d'image

Parfois, il n'y a pas de problème de conteneur car il n'y a pas d'image utilisable. Si docker run échoue avant de créer un conteneur, vérifiez l'image séparément :

docker image ls | grep mon-app
docker pull mon-registre/mon-app:tag

Pour les registres privés, confirmez l'authentification :

docker login <registre>

Pour les images locales, assurez-vous que le tag que vous exécutez est le tag que vous avez construit :

docker build -t mon-app:dev .
docker run --rm mon-app:dev

Une erreur locale courante consiste à construire mon-app:dev et à exécuter mon-app:latest, qui peut pointer vers une image plus ancienne ou rien du tout.

Si le réseau est blâmé mais que le service n'écoute pas

Lorsqu'un navigateur ne peut pas atteindre un conteneur, les gens sautent souvent sur le réseau Docker. Prouvez d'abord que l'application écoute à l'intérieur du conteneur.

docker exec -it <conteneur> sh
ss -ltnp || netstat -ltnp

Si l'application est liée à 127.0.0.1 à l'intérieur du conteneur, la publication de port Docker n'aidera pas. L'application doit écouter sur 0.0.0.0 ou l'adresse de l'interface du conteneur. C'est courant avec les serveurs de développement. Par exemple, de nombreux frameworks utilisent localhost par défaut et ont besoin d'un indicateur comme --host 0.0.0.0.

Confirmez ensuite le port publié :

docker port <conteneur>
docker ps --format 'table {{.Names}}	{{.Ports}}'

Vous voulez voir quelque chose comme 0.0.0.0:8080->3000/tcp. S'il n'y a pas de port publié, le service peut fonctionner depuis un autre conteneur sur le même réseau mais pas depuis le navigateur de votre hôte.

Une liste de vérification fiable pour le démarrage

Utilisez cet ordre lorsque vous êtes bloqué :

  1. docker ps -a pour voir si le conteneur existe et comment il s'est fermé.
  2. docker logs --tail 100 <conteneur> pour lire la plainte de l'application elle-même.
  3. docker inspect <conteneur> pour vérifier le code de sortie, l'état OOM, la commande, l'utilisateur, les montages et les ports.
  4. docker run --rm -it --entrypoint sh <image> pour tester l'image manuellement.
  5. Supprimez une variable à la fois : d'abord exécutez sans montages, puis sans réseaux personnalisés, puis avec seulement les variables d'environnement requises.

Cette dernière étape est importante. Une longue commande docker run avec des ports, des volumes, des fichiers d'environnement, un DNS personnalisé, des limites de mémoire et un point d'entrée personnalisé vous donne trop de suspects. Réduisez-la jusqu'à ce que l'image démarre, puis ajoutez les paramètres jusqu'à ce qu'elle se casse. Le paramètre que vous venez d'ajouter est généralement l'endroit où se trouve le vrai problème.