Vous déployez un conteneur. Il semble correct pendant une seconde. Puis il meurt. Puis Docker, utile, le relance… pour qu’il meure à nouveau. Le cycle se poursuit jusqu’à ce que votre supervision ressemble à un moniteur cardiaque dans un mauvais film et que votre téléphone d’astreinte commence à vous supplier.
C’est à ce moment que beaucoup perdent des heures à regarder docker ps et à deviner. Ne le faites pas. Quand un conteneur redémarre sans fin, il y a un journal qui vous dit fiablement ce qui s’est passé : les journaux de la tentative précédente, pas celle en cours qui est encore en train de démarrer.
Le seul journal dont vous avez besoin (et pourquoi ce n’est pas celui que vous regardez)
Quand un conteneur redémarre, il meurt souvent très vite — parfois avant d’émettre quoi que ce soit d’utile, parfois juste après avoir émis la ligne utile puis se termine immédiatement. Si vous taillez les logs pendant la boucle, vous avez tendance à attraper le mauvais moment : la nouvelle instance qui démarre (encore), pas celle qui vient de planter.
Le journal dont vous avez besoin est la tentative de conteneur précédente :
cr0x@server:~$ docker logs --previous myservice
error: cannot open config file /etc/myapp/config.yaml: permission denied
Si vous ne retenez qu’une chose de cet article, retenez ce flag. C’est la différence entre « je pense que c’est le réseau ? » et « c’est une erreur de permission de fichier, corrigez le montage. »
Pourquoi ça marche : le flux de logs de Docker est lié au cycle de vie d’un conteneur. Quand le conteneur est recréé ou redémarré (selon le scénario et le runtime), vous voulez la stdout/stderr du dernier lancement. C’est ce que fournit --previous pour les boucles de redémarrage où le conteneur est relancé sous le même nom.
Et oui, il y a des mises en garde. Si vous utilisez Compose et que les conteneurs sont recréés (nouveaux ID) plutôt que redémarrés, vous devrez peut‑être récupérer les logs par ID de conteneur ou utiliser docker compose logs. Mais le principe reste : arrêtez de regarder la tentative de démarrage actuelle et inspectez le dernier plantage.
Ce que « redémarrer en boucle » signifie réellement dans Docker
Une boucle de redémarrage n’est pas une seule chose. C’est une famille de comportements qui se ressemblent vus de loin : le conteneur s’affiche comme « Restarting (x) … » ou il réapparaît continuellement dans docker ps.
Politiques de redémarrage : les détails que les gens oublient
Les boucles de redémarrage sont généralement pilotées par une politique de redémarrage. Docker supporte :
- no (par défaut) : il s’arrête et reste arrêté.
- on-failure[:max-retries] : redémarre quand le code de sortie est non nul.
- always : redémarre quel que soit le code de sortie (sauf si vous l’arrêtez).
- unless-stopped : redémarre sauf si vous l’avez explicitement arrêté.
En production, vous voulez typiquement unless-stopped pour les services de longue durée. Mais des « bons réglages » deviennent du « bruit » quand le processus plante instantanément. La politique relaie fidèlement un objet cassé. Comme tout employé zélé, elle fait exactement ce que vous avez demandé, pas ce que vous vouliez dire.
Les codes de sortie sont votre premier vrai indice
Docker ne redémarre pas un conteneur parce qu’il s’ennuie. Il le redémarre parce que le processus principal se termine. Ce processus se termine pour l’une des trois grandes raisons :
- L’application a décidé de quitter (erreur de config, échec de migration, dépendance manquante).
- Le système d’exploitation l’a tué (OOM killer, SIGKILL, contraintes cgroup).
- Vous l’avez orchestré (échec d’un healthcheck, watchdog, unit systemd).
Le code de sortie et le flag « OOMKilled » vous indiquent sur quelle branche vous êtes. Vous ne diagnostiquez pas « Docker ». Vous diagnostiquez pourquoi le PID 1 dans ce conteneur ne reste pas en vie.
Une citation à garder en tête pendant le debug : « L’espoir n’est pas une stratégie. »
— Général Gordon R. Sullivan. Ce n’est pas strictement une citation SRE, mais elle s’applique cruellement bien aux boucles de redémarrage.
Plan de diagnostic rapide (premier/deuxième/troisième)
Voici le plan que j’utilise quand un conteneur flappe et que je veux trouver le goulot rapidement, sans transformer l’incident en projet de recherche.
Premier : capturer le dernier échec (ne regardez pas le boot actuel)
- Obtenir les logs précédents :
docker logs --previous(ou ID du conteneur). - Obtenir le code de sortie et la raison :
docker inspectpourState.ExitCode,State.OOMKilled,State.Error.
Si les logs précédents montrent une erreur de configuration évidente, arrêtez‑vous. Corrigez cela. Ne « rajoutez pas de mémoire » pour une faute de frappe YAML.
Second : déterminer s’il s’agit d’un crash, d’un kill ou d’un redémarrage délibéré
- Vérifier dmesg / journal pour les OOM.
- Vérifier l’état du healthcheck (unhealthy peut déclencher des redémarrages même si le processus tourne).
- Vérifier qui le redémarre : politique Docker, systemd, Compose, Swarm.
Troisième : valider l’environnement d’exécution (stockage, réseau, dépendances)
- Montages et permissions : bind mounts, secrets, fichiers de config.
- Ports et DNS : échec de bind, résolution, TLS ?
- Limites de ressources : mémoire, pids, ulimits, espace disque, exhaustion d’inodes.
Blague #1 : Les conteneurs sont comme des plantes d’intérieur — ignorez les bases (eau, lumière, terre) et elles mourront à l’heure dite.
Tâches pratiques : commandes, sorties et décisions (12+)
Voici les tâches qui font réellement avancer. Chacune inclut une commande réaliste, une sortie d’exemple, ce que ça signifie et la décision à prendre ensuite. Exécutez-les sur l’hôte sauf indication contraire.
Task 1: See the restart loop and grab the container ID
cr0x@server:~$ docker ps --no-trunc
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b5a1c0b7fd4b1f7b4b5d5c6a9c8d2d9c7a1c2e3f4a5b6c7d8e9f0a1b2c3d4e5 myapp:1.4.2 "/entrypoint.sh" 2 minutes ago Restarting (1) 5 seconds ago myservice
Signification : Docker indique « Restarting » avec un code de sortie entre parenthèses. Ce nombre est généralement le dernier code de sortie.
Décision : Passez immédiatement aux logs précédents et inspectez l’état.
Task 2: Pull the one log you need
cr0x@server:~$ docker logs --previous myservice
[2026-02-04T10:15:02Z] FATAL: DB_URL is not set
[2026-02-04T10:15:02Z] exiting with code 2
Signification : L’app se termine proprement mais en échouant à cause d’une variable d’environnement manquante.
Décision : Corrigez la configuration au niveau du déploiement (fichier env de Compose, secrets, CI). Ne touchez pas aux réglages du démon Docker.
Task 3: Inspect state, exit code, and OOMKilled
cr0x@server:~$ docker inspect -f 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}} FinishedAt={{.State.FinishedAt}}' myservice
ExitCode=137 OOMKilled=true Error= FinishedAt=2026-02-04T10:15:19.120401234Z
Signification : Le code de sortie 137 et OOMKilled=true indiquent classiquement un kill mémoire (SIGKILL).
Décision : Vérifiez les logs du noyau et les limites mémoire du conteneur. Ce n’est pas un « bug applicatif » tant que l’inverse n’est pas prouvé.
Task 4: Confirm OOM kill in kernel logs
cr0x@server:~$ sudo dmesg -T | tail -n 20
[Sun Feb 4 10:15:19 2026] Memory cgroup out of memory: Killed process 23184 (myapp) total-vm:812340kB, anon-rss:512120kB, file-rss:1200kB, shmem-rss:0kB
[Sun Feb 4 10:15:19 2026] oom_reaper: reaped process 23184 (myapp), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
Signification : Le noyau a tué le processus à cause de la pression mémoire du cgroup.
Décision : Augmentez la mémoire allouée au conteneur, réduisez l’utilisation mémoire ou corrigez une fuite. Assurez-vous aussi que l’hôte a de la marge ; déplacer la limite sans capacité réelle ne résout rien.
Task 5: Check container resource limits applied
cr0x@server:~$ docker inspect -f 'Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}} PidsLimit={{.HostConfig.PidsLimit}}' myservice
Memory=268435456 MemorySwap=268435456 PidsLimit=100
Signification : Limite mémoire de 256 MiB sans swap ; serré pour beaucoup de runtimes. La limite PID peut aussi être problématique pour les apps qui forkent beaucoup.
Décision : Si le service est censé être plus conséquent, augmentez les limites. Si il doit être petit, profilez la mémoire et supprimez les pics (warmup JIT, caches, migrations).
Task 6: Identify if restart policy is forcing the loop
cr0x@server:~$ docker inspect -f 'Name={{.Name}} RestartPolicy={{.HostConfig.RestartPolicy.Name}} MaximumRetryCount={{.HostConfig.RestartPolicy.MaximumRetryCount}}' myservice
Name=/myservice RestartPolicy=always MaximumRetryCount=0
Signification : « always » signifie qu’il redémarrera même si l’app sort avec 0. MaximumRetryCount=0 signifie illimité.
Décision : Pendant le debug, envisagez de passer temporairement à on-failure:5 ou de désactiver les redémarrages pour pouvoir inspecter l’état du conteneur mort sans qu’il ressuscite immédiatement.
Task 7: Stop the loop long enough to inspect safely
cr0x@server:~$ docker update --restart=no myservice
myservice
Signification : Vous avez changé la politique de redémarrage pour cette instance de conteneur.
Décision : Arrêtez‑le ensuite et redémarrez manuellement quand vous êtes prêt. Corrigez aussi la source (fichier Compose, unit systemd) sinon le problème reviendra au prochain déploiement.
Task 8: Inspect container events to see the rhythm and cause
cr0x@server:~$ docker events --since 10m --filter container=myservice
2026-02-04T10:15:18.992345678Z container die b5a1c0b7fd4b (exitCode=137, image=myapp:1.4.2, name=myservice)
2026-02-04T10:15:19.101234567Z container start b5a1c0b7fd4b (image=myapp:1.4.2, name=myservice)
2026-02-04T10:15:24.220987654Z container die b5a1c0b7fd4b (exitCode=137, image=myapp:1.4.2, name=myservice)
Signification : Cadence de redémarrage claire. Code de sortie répété.
Décision : Des codes identiques répétés signifient souvent un échec de démarrage déterministe (config, permissions, bind de port) ou un kill déterministe (OOM au warmup). Concentrez‑vous là‑dessus, pas sur une instabilité réseau aléatoire.
Task 9: Check health status (healthchecks can create “soft restart loops”)
cr0x@server:~$ docker inspect -f 'Health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}} FailingStreak={{if .State.Health}}{{.State.Health.FailingStreak}}{{else}}0{{end}}' myservice
Health=unhealthy FailingStreak=12
Signification : Le processus du conteneur peut tourner, mais le healthcheck échoue à répétition. Certains systèmes (Compose avec services dépendants, watchdogs externes) répondent en redémarrant.
Décision : Inspectez la commande de healthcheck et sa sortie ensuite. Traitez‑la comme du code de production, parce que c’en est.
Task 10: Retrieve healthcheck logs
cr0x@server:~$ docker inspect -f '{{range .State.Health.Log}}{{.End}} {{.ExitCode}} {{.Output}}{{end}}' myservice
2026-02-04T10:16:02.000000000Z 1 curl: (7) Failed to connect to 127.0.0.1 port 8080: Connection refused
2026-02-04T10:16:12.000000000Z 1 curl: (7) Failed to connect to 127.0.0.1 port 8080: Connection refused
Signification : Votre application n’écoute pas sur le port attendu par le healthcheck (ou elle est liée à la mauvaise interface, ou elle n’a pas encore démarré).
Décision : Corrigez l’adresse/port d’écoute de l’application ou le healthcheck. Si le démarrage est lent, ajustez le start_period pour éviter des échecs prématurés.
Task 11: Detect port binding conflicts on the host
cr0x@server:~$ sudo ss -ltnp | grep ':8080 '
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("old-nginx",pid=1187,fd=7))
Signification : Quelque chose d’autre écoute déjà sur ce port hôte.
Décision : Changez le mapping de port publié, ou arrêtez le service en conflit. Si ça marche sur votre laptop mais pas en prod, c’est généralement parce que votre laptop n’avait pas le démon conflit.
Task 12: Validate mounts and permissions (the quiet killer)
cr0x@server:~$ docker inspect -f '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}} (RW={{.RW}}){{"\n"}}{{end}}' myservice
bind /srv/myservice/config.yaml -> /etc/myapp/config.yaml (RW=false)
volume myservice-data -> /var/lib/myapp (RW=true)
Signification : La config est un bind mount en lecture seule. C’est bon. Mais si l’app tente d’y écrire, elle plantera.
Décision : Assurez‑vous que l’app n’écrit que dans des chemins inscriptibles. Si elle doit générer une config, montez un répertoire et écrivez dedans, ou changez le comportement de l’app.
Task 13: Enter a debugging shell (without changing the image)
cr0x@server:~$ docker run --rm -it --network container:myservice --pid container:myservice --entrypoint /bin/sh myapp:1.4.2
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 myapp --config /etc/myapp/config.yaml
/ # netstat -ltn
Active Internet connections (only servers)
tcp 0 0 127.0.0.1:9090 0.0.0.0:* LISTEN
Signification : Vous pouvez observer les processus et ports à l’intérieur des namespaces du conteneur. Ici il écoute sur 9090, pas 8080.
Décision : Corrigez le healthcheck / le mapping de ports. Les namespaces enlèvent le doute.
Task 14: Check filesystem pressure and inode exhaustion
cr0x@server:~$ df -h /var/lib/docker
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p3 100G 98G 2.0G 99% /var/lib/docker
cr0x@server:~$ df -i /var/lib/docker
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/nvme0n1p3 6553600 6551000 2600 100% /var/lib/docker
Signification : Le disque est presque plein et les inodes sont épuisés. Les conteneurs peuvent échouer de façons étranges : impossible d’écrire des fichiers PID, impossible d’extraire des couches, impossible d’appendre des logs.
Décision : Nettoyez images/conteneurs/volumes, agrandissez le stockage, déplacez Docker root. Puis retestez. Si vous ne corrigez pas les inodes, « ajouter 10 Go » ne suffira pas.
Task 15: Check Docker daemon logs (sometimes the daemon is the villain)
cr0x@server:~$ sudo journalctl -u docker --since "10 minutes ago" -n 50
Feb 04 10:15:19 server dockerd[1023]: containerd: time="2026-02-04T10:15:19Z" level=warning msg="failed to shim reaping" id=b5a1c0b7fd4b
Feb 04 10:15:19 server dockerd[1023]: Error response from daemon: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error mounting "/srv/myservice/config.yaml" to rootfs at "/etc/myapp/config.yaml": permission denied: unknown
Signification : Les erreurs OCI/runtime peuvent empêcher le conteneur de démarrer. C’est différent d’un « app qui démarre puis plante ».
Décision : Corrigez les permissions de montage, les profils SELinux/AppArmor, ou l’existence des chemins sur l’hôte. Les ingénieurs applicatifs ne peuvent pas corriger ce qui ne démarre jamais.
Modes d’échec qui provoquent des boucles de redémarrage
La plupart des boucles de redémarrage appartiennent à l’une de ces catégories. Apprenez le pattern, et vous arrêterez de traiter chaque panne comme un flocon de neige unique.
1) L’application se termine parce que la configuration est erronée
Signature : ExitCode est un petit entier non nul (1, 2, 64), les logs montrent « missing env var », « invalid config », « failed to parse ».
Causes typiques : variable d’environnement manquante après rotation des secrets, mauvais chemin de fichier de config, bug de templating, syntaxe JSON/YAML.
Correction : Validez la config au build/déploiement. Ajoutez un mode « configtest » dans l’entrypoint. Échouez rapidement, mais échouez une seule fois (limitez les retries durant le rollout).
2) Comportement du PID 1 et gestion des signaux (le classique « ça marche en local »)
Dans un conteneur, le processus principal est PID 1. PID 1 a des sémantiques spéciales sous Linux : il ignore certains signaux par défaut et est responsable du reaping des zombies. Si vous encapsulez votre app dans un script shell naïf, vous pouvez obtenir des comportements étranges à l’arrêt, des enfants qui ne meurent jamais, ou une « sortie immédiate » parce que le script se termine.
Signature : Le conteneur sort avec 0 rapidement ; les logs montrent que le script est terminé ; aucun processus de longue durée. Ou le conteneur ne s’arrête pas proprement et reçoit SIGKILL, puis redémarre.
Correction : Utilisez exec dans les scripts d’entrypoint. Envisagez un init minimal (comme tini) quand vous lancez des sous-processus.
3) Kills OOM et limites mémoire
Les kills OOM créent les boucles de redémarrage les plus propres et les plus cruelles : tout démarre, alloue beaucoup de mémoire (warmup JVM, import massif Python, étape de build Node, cache en mémoire), puis le noyau le tue. Docker le redémarre. Repeat.
Signature : ExitCode 137, OOMKilled=true, logs du noyau indiquant cgroup OOM.
Correction : Augmentez les limites sur la base de mesures, pas d’espoir ; limitez les caches ; réduisez la concurrence ; évitez les migrations lourdes à chaque démarrage.
4) Healthchecks trop agressifs (ou simplement mauvais)
Les healthchecks sont géniaux jusqu’à ce qu’ils soient écrits comme un test unitaire : fragiles, dépendants du timing, et persuadés que votre service est mort parce qu’un TCP connect a échoué une fois.
Signature : Le service tourne mais devient « unhealthy », l’orchestrateur redémarre ou les services dépendants refusent de démarrer.
Correction : Ajoutez un start_period, ajustez interval/retries, et faites en sorte que la vérification reflète la disponibilité utilisateur (pas la perfection interne).
5) Stockage et systèmes de fichiers : disque plein, mauvaises permissions, montages cassés
Les problèmes de stockage ne crient pas toujours. Parfois ils chuchotent : « read-only file system », « no space left on device », « permission denied ». Puis l’app sort. Puis Docker la redémarre. Souffrance polie et infinie.
Signature : Les logs mentionnent des échecs d’écriture ; les logs du daemon montrent des erreurs de montage ; disque/inodes proches de 100%.
Correction : Corrigez les montages, la propriété (UID/GID), les étiquettes SELinux, et la capacité. Aussi : arrêtez d’écrire vos logs dans le système de fichiers du conteneur comme en 2014.
6) Dépendances en échec : DNS, TLS, bases de données et ordre de démarrage
Les apps supposent souvent que les dépendances sont immédiatement disponibles. Dans les systèmes distribués, cette supposition est adorable et fausse.
Signature : Les logs montrent des connexions refusées/timeout vers la DB ; exit code non nul ; les redémarrages surviennent immédiatement au démarrage.
Correction : Implémentez un backoff et des retries dans l’application. Ou utilisez un pattern d’init container (hors Docker pur) ou un script de démarrage qui attend avec des timeouts. Évitez les boucles « wait‑for‑it » infinies sans deadline.
7) « Optimisations » qui changent le timing et cassent tout
Quand vous compressez le temps de démarrage ou réduisez la taille d’image, vous changez le timing et l’environnement d’exécution. Cela peut révéler des races : le healthcheck s’exécute plus tôt, les dépendances ne sont pas prêtes, les fichiers ne sont pas encore créés.
Signature : Le conteneur flappe après un « cleanup » d’image ou un changement d’image de base ; même code, comportement différent.
Correction : Traitez les changements d’image de base et d’entrypoint comme des changements de production. Testez le cold‑start. Testez avec des limites de ressources réalistes.
Blague #2 : Le conteneur « redémarre pour appliquer des mises à jour », ce qui est exactement ce qu’il dit juste avant d’arrêter de le faire.
Trois mini-récits d’entreprise issus de la production
Mini-récit 1 : L’incident causé par une fausse hypothèse
L’équipe avait une petite API interne exécutée dans Docker Compose sur quelques VM. Configuration simple : conteneur app, conteneur Postgres et un reverse proxy. Ça fonctionnait depuis des mois — jusqu’à un patch OS mineur et un redeploy.
Après le redeploy, le conteneur API est entré dans une boucle de redémarrage serrée. Le premier intervenant a fait ce que beaucoup font sous pression : imputé le problème au « réseau Docker ». Les logs de l’exécution courante étaient pâles : juste des bannières de démarrage. Rien d’évident.
Quelqu’un d’autre a lancé docker logs --previous et a immédiatement vu une ligne indiquant l’échec d’ouverture d’un fichier de certificat. L’hypothèse avait été : « Le certificat est inclus dans l’image. » Il ne l’était pas. C’était un bind mount depuis l’hôte, et le patch OS avait modifié les permissions du répertoire où vivait le certificat.
Le process API tournait en tant qu’utilisateur non root. Il ne pouvait pas lire le certificat, donc il sortait. Docker le redémarrait. Boucle infinie.
La correction fut ennuyeuse : corriger la propriété et les permissions sur le chemin hôte, puis redeployer. La correction durable fut encore plus ennuyeuse : arrêter de supposer que les fichiers hôtes sont stables ; les gérer explicitement (gestion de configuration ou store de secrets) et ajouter une vérification de démarrage qui imprime une erreur claire avant toute autre chose.
Mini-récit 2 : L’optimisation qui s’est retournée contre eux
Une équipe plateforme voulait des déploiements plus rapides et des images plus petites. Ils ont migré plusieurs services d’une image Debian vers une image Alpine plus légère. Les builds étaient plus rapides. Les rapports de scan CVE étaient plus propres. Tout le monde s’est senti « avoir réduit le gaspillage ».
Une semaine plus tard, un service a commencé à flapper après une release routine. Ce n’était pas cohérent sur tous les nœuds. Sur certains nœuds il tournait des heures ; sur d’autres il redémarrait chaque minute. La politique de redémarrage était unless-stopped, donc il continuait d’essayer.
La cause racine était une dépendance native. Le service utilisait une librairie qui se comportait différemment sous musl (Alpine) que sous glibc (Debian). Sous charge, l’usage mémoire grimpait, franchissant la limite cgroup. Le noyau le tuait (exit 137), il redémarrait, et le cycle recommençait. Comme la répartition de la charge variait par nœud, le problème paraissait « aléatoire ».
Ils ont rollbacké l’image de base pour ce service, puis fait le travail difficile : définir des limites mémoire réalistes, construire un test de charge mesurant le RSS en steady‑state et au warmup, et documenter quels services sont sûrs à « minifier ».
La leçon n’était pas « Alpine est mauvais ». La leçon : les optimisations changent la physique. Si vous ne mesurez pas, vous déplacez juste des pannes.
Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la situation
Une équipe liée à la finance exécutait un job batch conteneurisé produisant des fichiers consommés par un autre système. Ce n’était pas glamour. Il tournait une fois par heure, écrivait dans un volume monté, puis s’arrêtait. La politique de redémarrage était on-failure:3, pas always. Ce choix semblait conservateur, peut‑être même timide.
Un matin, le job a commencé à échouer immédiatement. L’échec n’était pas dans les logs applicatifs ; il était dans les logs du démon Docker : un chemin de bind mount n’existait plus sur un des hôtes après une réorganisation du système de fichiers. Le conteneur ne démarrant même pas.
Parce que la politique de redémarrage était bornée, le job s’est arrêté après trois tentatives au lieu de flapper pendant des heures et consommer des ressources. Leur alerting s’est déclenché sur « job n’a pas tourné » plutôt que « CPU du nœud en feu ». L’ingénieur d’astreinte a pu diagnostiquer sans que le système ne change continuellement sous ses yeux.
Ils ont corrigé le chemin de montage et ajouté une vérification pré-déploiement dans la pipeline qui vérifie l’existence des chemins hôtes et leurs permissions. Le job est redevenu ennuyeux, ce qui est l’état correct pour les systèmes liés à la finance.
Erreurs courantes : symptôme → cause racine → correction
Cette section est celle que vous souhaiterez avoir pendant un incident. Les symptômes sont ce que vous voyez. Les causes racines sont ce qui se passe réellement. Les corrections sont le chemin le plus court et sûr vers la stabilité.
1) « Restarting (0) » pour toujours
Symptôme : Le conteneur redémarre, le code de sortie apparaît comme 0.
Cause racine : Politique de redémarrage always avec un processus qui se termine correctement (script terminé ; conteneur job pas destiné à tourner longtemps), ou un superviseur qui s’arrête après avoir spawné un enfant de façon incorrecte.
Correction : Utilisez on-failure pour les jobs one‑shot. Assurez‑vous que le script d’entrypoint utilise exec afin que le vrai service devienne PID 1.
2) Exit code 137 et « OOMKilled=true »
Symptôme : Redémarrages à un point cohérent du démarrage ; parfois marche avec une charge plus faible ; logs tronqués.
Cause racine : Kill mémoire du cgroup (OOM).
Correction : Mesurez la mémoire ; augmentez les limites de façon appropriée ; corrigez les fuites ; ajustez les runtimes (heap JVM, flags mémoire Node). Confirmez avec les logs du noyau.
3) « permission denied » sur les mounts
Symptôme : Logs du daemon montrant des erreurs de montage OCI ; ou logs applicatifs montrant des échecs d’ouverture de fichier.
Cause racine : Permissions du système de fichiers hôte, étiquettes SELinux, profils AppArmor, ou mappage d’utilisateur dans Docker rootless.
Correction : Corrigez la propriété/permissions ; appliquez le contexte SELinux correct ; pour rootless, assurez‑vous que les chemins sont accessibles à l’utilisateur qui exécute dockerd.
4) Healthcheck échoue alors que l’app est en réalité OK
Symptôme : L’app répond sur un port/chemin, mais le healthcheck marque unhealthy ; l’orchestrateur redémarre ou maintient le service hors rotation.
Cause racine : Mauvais port, mauvais chemin, mismatch TLS, ou démarrage plus lent que le délai du healthcheck.
Correction : Corrigez la commande de healthcheck ; ajoutez start_period ; vérifiez que la santé est liée à la disponibilité visible par l’utilisateur.
5) Le conteneur n’atteint jamais les logs applicatifs
Symptôme : docker logs est vide ; le conteneur meurt instantanément ; le daemon montre des erreurs runtime.
Cause racine : Image/entrypoint manquant, erreur de format d’exécutable (mauvaise architecture), échec de montage, binaire manquant, utilisateur invalide.
Correction : Inspectez les logs du daemon ; vérifiez l’architecture de l’image ; testez docker run --entrypoint avec un shell ; validez que les montages existent.
6) Boucle de redémarrage après « durcissement » ou « nettoyage »
Symptôme : Marche en dev ; échoue en prod après activation du root FS en lecture seule, baisse des privilèges, ou suppression de paquets.
Cause racine : L’app écrit sur le root FS, a besoin de certificats CA, de données de fuseau horaire, ou attend /tmp inscriptible.
Correction : Fournissez des volumes inscriptibles pour les chemins nécessaires ; installez les données runtime requises ; documentez les emplacements et permissions requis.
7) Redémarrages aléatoires corrélés à la charge
Symptôme : Conteneur stable la nuit, flappe pendant les pics.
Cause racine : Pics mémoire provoquant OOM, exhaustion de descripteurs de fichiers, exhaustion de threads/processus, ou timeouts upstream provoquant des plantages au démarrage.
Correction : Suivez l’usage des ressources ; définissez des ulimits ; ajoutez du backpressure ; évitez de planter sur des défaillances transitoires de dépendances.
Checklists / plan étape par étape
Checklist A: « Mon conteneur redémarre maintenant » (plan 10 minutes)
- Identifier le conteneur :
docker ps --no-trunc. - Récupérer les logs précédents :
docker logs --previous <name>. - Inspecter la raison de sortie :
docker inspectpour ExitCode et OOMKilled. - Si ExitCode=137 ou OOMKilled=true : vérifier
dmesget les limites mémoire. - Si les logs montrent config/env : comparer les vars env attendues avec celles du conteneur et la config de déploiement.
- Si montage/permissions : vérifier les mounts via
docker inspectet les logs du daemon. - Si healthcheck : inspecter les logs dans
.State.Health; vérifier le port/chemin. - Arrêter la boucle si elle nuit à l’hôte :
docker update --restart=nopuisdocker stop. - Corriger à la source (Compose/systemd/CI) pour que le prochain déploiement ne réintroduise pas la boucle.
- Redémarrer une fois, surveiller events et logs, confirmer la stabilité.
Checklist B: « Rendre les boucles de redémarrage moins pénibles » (contrôles en phase de conception)
- Utiliser des retries bornés quand approprié :
on-failure:5pour les services batch. - Ajouter des vérifications de démarrage claires : valider vars env, fichiers et connectivité avec des erreurs nettes.
- Faire en sorte que les scripts d’entrypoint utilisent
execet sortent non nul en cas d’échec fatal au démarrage. - Définir des healthchecks reflétant la vraie readiness, avec une période de grâce de démarrage.
- Fixer des limites de ressources réalistes et les monitorer ; « illimité » n’est pas une stratégie, c’est une confession.
- Déplacer l’état persistant vers des volumes ; traiter le FS du conteneur comme éphémère.
- Centraliser les logs ; ne pas compter sur « docker logs » comme unique source pendant un incident.
- Documenter les dépendances et le comportement en cas d’échec (que se passe‑t‑il si la DB est down au boot ?).
Faits intéressants et contexte historique
- Fait 1 : La popularité initiale de Docker (vers 2013–2014) était portée par le packaging et la distribution, pas par l’orchestration ; les boucles de redémarrage sont devenues plus visibles quand les gens ont commencé à traiter les conteneurs comme des « pets ».
- Fait 2 : Le standard OCI runtime existe parce que l’écosystème avait besoin d’un comportement cohérent entre outils ; beaucoup de « problèmes Docker » sont en réalité des erreurs du runtime (runc/containerd) exposées par Docker.
- Fait 3 : Le code de sortie 137 indique typiquement SIGKILL (128 + 9). En environnement conteneurisé, cela correspond souvent à un OOM killer, mais peut aussi être un kill externe.
- Fait 4 : Les sémantiques de PID 1 sont antérieures aux conteneurs ; les conteneurs font juste en sorte que plus d’apps deviennent accidentellement PID 1 sans y être conçues.
- Fait 5 : Les healthchecks ont été ajoutés à Docker longtemps après « docker run » ; de nombreuses images sont encore livrées sans eux, et beaucoup d’équipes les ajoutent sans ajuster le timing de démarrage.
- Fait 6 : Les drivers de logs (json-file, journald, syslog, fluentd, etc.) influencent ce que
docker logspeut montrer ; le diagnostic de redémarrage change si les logs ne sont pas stockés localement. - Fait 7 : Les filesystems overlay (overlay2) ont modifié les performances et la sémantique du stockage conteneur comparé aux drivers plus anciens ; certains « échecs aléatoires au démarrage » d’autrefois étaient des cas limites du driver de stockage.
- Fait 8 : Les politiques de redémarrage précèdent les orchestrateurs modernes ; elles sont un mécanisme de fiabilité local, pas une stratégie complète d’ordonnancement. C’est pourquoi elles peuvent amplifier la mauvaise situation sur un seul hôte.
- Fait 9 : Le comportement de Compose qui recrée des conteneurs (nouveaux IDs) vs redémarrage in‑place est une source fréquente de confusion quand on s’attend à ce que
--previousfonctionne toujours par nom.
FAQ
1) Quel est « le seul journal dont j’ai besoin » quand un conteneur redémarre sans fin ?
Les journaux de la tentative d’exécution précédente : docker logs --previous <container>. Ils capturent le plantage que vous avez manqué en regardant le nouveau démarrage.
2) Pourquoi docker logs n’affiche rien d’utile pendant une boucle de redémarrage ?
Parce que vous regardez le mauvais moment du cycle de vie. Le conteneur peut sortir avant de produire de la sortie, ou la ligne utile a été imprimée dans la tentative précédente. Utilisez --previous et inspectez l’état de sortie.
3) Docker recrée-t-il le conteneur ou redémarre-t-il le même ?
La politique de redémarrage Docker redémarre le même conteneur (même ID). Certains outils de plus haut niveau (Compose lors d’updates, Swarm lors de rescheduling) peuvent créer un nouveau conteneur/task, ce qui change la façon de récupérer les logs « précédents ».
4) Que signifie le code de sortie 137 dans Docker ?
Cela signifie couramment que le processus a reçu SIGKILL. Dans les conteneurs, c’est fréquemment le OOM killer du noyau. Confirmez avec docker inspect (OOMKilled) et dmesg.
5) Mon conteneur sort avec le code 0 mais redémarre quand même. Comment ?
La politique always redémarrera même après une sortie réussie. C’est OK pour des daemons, incorrect pour des jobs. Passez à on-failure ou redesign du conteneur pour qu’il reste en cours s’il s’agit d’un service.
6) Un healthcheck défaillant peut-il provoquer des redémarrages ?
Docker lui‑même ne redémarre pas automatiquement sur un statut unhealthy, mais des systèmes externes le font souvent : dépendances Compose, scripts, unités systemd, ou contrôleurs de load balancer. Diagnostiquez la sortie du healthcheck quand même ; elle pointe généralement vers le vrai problème de readiness.
7) Comment arrêter une boucle de redémarrage sans tout supprimer ?
Désactivez temporairement la politique de redémarrage : docker update --restart=no <name>, puis stoppez le conteneur. Corrigez la cause racine, puis réactivez la politique voulue via votre configuration de déploiement.
8) Et si je ne peux pas utiliser docker logs parce que les logs sont envoyés ailleurs ?
Dans ce cas, le « seul journal » est l’équivalent dans votre pipeline de logs, filtré par ID de conteneur et timestamp autour du crash. Cela dit, docker inspect pour les codes de sortie et les logs du daemon restent la vérité locale.
9) Comment déboguer un conteneur qui meurt trop vite pour qu’on puisse y exec ?
Désactivez les redémarrages, lancez l’image avec un entrypoint remplacé (shell), ou exécutez un conteneur de debug dans les mêmes namespaces. L’objectif est d’observer le système de fichiers, les vars env et le réseau depuis le même point de vue que l’app.
10) Est-ce la même chose que Kubernetes CrashLoopBackOff ?
C’est le même mode d’échec de base — processus qui sort et le système réessaye — mais Kubernetes ajoute du backoff, des events, des probes et la gestion de réplicas. Les primitives de diagnostic (logs précédents, codes de sortie, OOM) restent applicables.
Étapes suivantes que vous devriez vraiment faire
Si vous avez un conteneur qui redémarre sans fin, faites ceci dans l’ordre :
- Exécutez
docker logs --previouset lisez‑le sérieusement. - Exécutez
docker inspectpour ExitCode et OOMKilled ; décidez si vous êtes en territoire « crash » ou « kill ». - Vérifiez les logs du démon pour des problèmes OCI/montage si le conteneur ne démarre jamais vraiment.
- Si c’est un OOM : confirmez avec
dmesg, puis corrigez les limites/capacité ou le profil mémoire de l’app. - Corrigez la source de vérité (fichier Compose, unit systemd, config CI), pas le conteneur vivant, sauf si vous effectuez un rustine d’urgence.
Puis faites les améliorations ennuyeuses : retries bornés quand approprié, healthchecks ajustés, comportement PID 1 correct, et une vérification préflight de config qui échoue bruyamment une fois au lieu de silencieusement indéfiniment.