Le conteneur Docker redémarre en boucle : trouvez la cause réelle en 5 minutes

Cet article vous a aidé ?

L’alerte ne dit pas « cause racine inconnue ». Elle dit « service indisponible ». Et quelque part, un conteneur fait cette chose embarrassante : il démarre, meurt, redémarre, meurt — comme s’il négociait avec la réalité.

Les boucles de redémarrage font perdre du temps parce que les gens poursuivent les symptômes : « Docker est instable », « l’image est cassée », « peut-être que le nœud est maudit ». C’est presque toujours quelque chose d’ennuyeux : code de sortie, healthcheck, kill OOM, synchronisation de dépendances, ou une politique de redémarrage qui fait exactement ce que vous lui avez demandé. Voici comment trouver la vraie raison rapidement, sans transformer l’incident en mode de vie.

Playbook de diagnostic rapide (5 minutes)

L’objectif n’est pas « collecter des données ». L’objectif est : identifier le déclencheur du redémarrage et le composant défaillant avant que la boucle n’efface les preuves.
Vous essayez de répondre à trois questions :

  1. Qui le redémarre ? Politique de redémarrage Docker, orchestrateur (Compose, Swarm), systemd, ou vous-même ?
  2. Pourquoi se termine-t-il ? Plantage d’app, mauvaise config, signal, kill OOM, healthcheck échoué, dépendance manquante.
  3. Qu’est-ce qui a changé ? Tag d’image, variable d’environnement, secret, volume, pression mémoire/nucléaire, DNS, pare-feu.

Minute 1 : identifier le conteneur et le moteur de redémarrage

  • Obtenir RestartCount, ExitCode, OOMKilled, politique de redémarrage.
  • Confirmer si Compose/Swarm/systemd est impliqué.

Minute 2 : récupérer la dernière preuve d’échec (avant qu’elle ne disparaisse)

  • Consulter les journaux du dernier démarrage (--since / tail).
  • Inspecter State.Error et les horodatages.

Minute 3 : classifier le mode d’échec

  • Exit code 1/2/126/127 : problèmes d’app/config/exécution.
  • Exit code 137 ou OOMKilled=true : pression mémoire.
  • Healthcheck « unhealthy » : l’app peut encore fonctionner, mais l’orchestrateur la tue.
  • Sorties instantanées : script d’entrée, fichier manquant, utilisateur/permissions incorrectes.

Minute 4 : valider les dépendances et l’environnement d’exécution

  • DNS, réseau, ports, fichiers montés, permissions, secrets.
  • Disponibilité du backend (BD, queue, auth) et timeouts.

Minute 5 : prenez une décision, pas un rapport

Décidez laquelle des actions suivantes vous allez faire : corriger la configuration, ajouter des ressources, revenir à l’image précédente, désactiver un healthcheck défaillant, ou verrouiller les dépendances.
Si vous ne pouvez pas décider après cinq minutes, il vous manque une des informations suivantes : code de sortie, preuve OOM, statut de healthcheck, ou qui effectue le redémarrage.

Une citation à graver dans chaque esprit d’astreinte : « L’espoir n’est pas une stratégie. » — Gene Kranz.

Ce que « redémarre en boucle » signifie vraiment

Une boucle de redémarrage de conteneur n’est pas un bug unique. C’est un contrat entre votre processus, Docker et ce qui supervise Docker.
Le processus du conteneur se termine. Quelque chose le remarque. Quelque chose le redémarre. Ce « quelque chose » peut être Docker lui‑même (politique de redémarrage), Docker Compose, Swarm, Kubernetes (si Docker n’est que le runtime), ou même systemd qui gère un docker run.

Donc le premier anti‑pattern : fixer le nom du conteneur comme s’il vous devait des réponses. Les conteneurs ne redémarrent pas ; ce sont les superviseurs qui redémarrent les conteneurs.
Le meilleur mouvement de debug est d’identifier le superviseur, puis de lire les preuves qu’il laisse.

Drivers de redémarrage typiques

  • Politique de redémarrage Docker : no, on-failure, always, unless-stopped.
  • Compose : restart: dans docker-compose.yml, plus des problèmes d’ordre de dépendance.
  • systemd : unité avec Restart=always qui lance Docker.
  • Automatisation externe : cron, scripts watchdog, jobs CI/CD « s’assurer que c’est en cours d’exécution ».

Deux types de boucles qui paraissent identiques (mais ne le sont pas)

Crash loop : le processus meurt rapidement à cause d’un problème d’app/config/ressource.
Kill loop : le processus tourne, mais un healthcheck échoue ou un superviseur le tue (OOM, watchdog, politique d’orchestrateur).

Votre travail complet est de séparer ces deux cas. Les journaux et les codes de sortie le font en quelques minutes — si vous les récupérez correctement.

Faits intéressants et petite histoire (pour affiner votre intuition)

  • Les politiques de redémarrage de Docker sont apparues tôt parce que les utilisateurs traitaient les conteneurs comme des daemons légers et avaient besoin d’un comportement d’init sans système d’init à l’intérieur du conteneur.
  • Le code de sortie 137 signifie généralement SIGKILL (128 + 9). Dans les conteneurs, SIGKILL est souvent le OOM killer du noyau ou un kill brutal d’un superviseur.
  • Les healthchecks ont été ajoutés après que des gens ont déployé des services « en cours d’exécution mais morts » — le processus est encore vivant, mais il n’accepte pas le trafic. Sans healthchecks, ces défaillances pourrissent silencieusement.
  • Les journaux Docker ne sont pas « les journaux de l’app » ; ce sont ce que le processus écrit sur stdout/stderr, capturé par un driver de logs. Si votre app écrit dans des fichiers, docker logs peut sembler vide alors que l’app crie dans /var/log à l’intérieur du conteneur.
  • Les systèmes de fichiers overlay ont rendu les conteneurs pratiques en activant le copy‑on‑write, mais ils peuvent amplifier la surcharge IO pour des workloads d’écriture intensive — menant à des timeouts qui ressemblent à des « redémarrages aléatoires ».
  • Les boucles de redémarrage masquent souvent des échecs de dépendance : l’app sort parce qu’elle ne peut pas joindre une base de données, mais la vraie cause est DNS, pare‑feu, mauvais TLS, ou un mot de passe modifié.
  • Compose « depends_on » ne signifie pas « ready » dans le Compose classique ; il ordonne principalement le démarrage, pas la disponibilité. Cette seule incompréhension a brûlé plus d’équipes que des bugs noyau obscurs.
  • Les kills OOM peuvent arriver alors que la mémoire « libre » semble disponible parce que ce qui compte pour le conteneur, ce sont les limites cgroup et la comptabilité mémoire+swap, pas la RAM libre globale de l’hôte.
  • Le comportement du PID 1 compte : signaux, zombies et gestion des sorties peuvent différer si vous lancez un shell en PID 1 versus un wrapper de type init, ce qui change l’apparence des redémarrages.

12+ tâches pratiques : commandes, ce que la sortie signifie, et la décision à prendre

Ce sont les actions que vous pouvez exécuter sous pression. Chaque tâche inclut : une commande, ce que la sortie vous dit, et la décision qu’elle rend possible.
Exécutez-les dans l’ordre jusqu’à trouver la preuve évidente. Et oui, vous pouvez faire la plupart sans « exec » dans un conteneur qui meurt toutes les quatre secondes.

Task 1: Confirm restart loop and get the container ID

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.RunningFor}}'
NAMES        IMAGE                 STATUS                        RUNNING FOR
api          myco/api:1.24.7       Restarting (1) 8 seconds ago   2 minutes
redis        redis:7               Up 3 hours                     3 hours

Signification : Restarting (1) indique que Docker voit le conteneur sortir à répétition et applique une politique de redémarrage.
Le nombre entre parenthèses est le dernier code de sortie (pas toujours ; c’est ce que Docker a observé en dernier).

Décision : Identifiez le nom du service défaillant (api) et passez immédiatement à l’inspection de l’état et des détails de sortie. Ne commencez pas encore par des théories réseau.

Task 2: Inspect restart policy, exit code, OOM, and timestamps

cr0x@server:~$ docker inspect api --format '{{json .State}}'
{"Status":"restarting","Running":true,"Paused":false,"Restarting":true,"OOMKilled":false,"Dead":false,"Pid":24711,"ExitCode":1,"Error":"","StartedAt":"2026-01-02T09:14:44.129885633Z","FinishedAt":"2026-01-02T09:14:51.402183576Z","Health":null}
cr0x@server:~$ docker inspect api --format 'RestartPolicy={{.HostConfig.RestartPolicy.Name}} MaxRetry={{.HostConfig.RestartPolicy.MaximumRetryCount}}'
RestartPolicy=always MaxRetry=0

Signification : ExitCode=1 est une erreur côté application. OOMKilled=false réduit la probabilité d’un kill mémoire (pas parfait, mais signe fort).
La politique de redémarrage always signifie que Docker va réessayer indéfiniment. C’est pratique… jusqu’à ce que ça ne le soit plus.

Décision : Concentrez‑vous sur les erreurs de démarrage/config de l’application et récupérez les journaux du dernier lancement. Si c’était OOMKilled=true ou exit 137, vous basculeriez vers la mémoire.

Task 3: Get the last logs from the previous attempt (not from the beginning of time)

cr0x@server:~$ docker logs --timestamps --tail 200 api
2026-01-02T09:14:49.903214817Z level=error msg="config parse failed" err="missing ENV DATABASE_URL"
2026-01-02T09:14:49.903955113Z level=error msg="fatal: cannot start without database"

Signification : Le conteneur ne « redémarre » pas mystérieusement. Il échoue de façon déterministe : variable d’environnement manquante DATABASE_URL.

Décision : Corriger la configuration et redéployer. Stop. Ne pas ajouter de mémoire. Ne pas « reconstruire l’image ». Ne pas blâmer Docker.

Task 4: Confirm the container env and what it thinks it has

cr0x@server:~$ docker inspect api --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n '1,12p'
APP_ENV=prod
LOG_LEVEL=info
PORT=8080

Signification : L’environnement manque ce que les logs indiquent comme manquant. C’est cohérent. Bien.

Décision : Déterminez d’où doivent venir les variables d’environnement : fichier Compose, --env-file, injection de secrets, ou outil plateforme. Corrigez à la source, pas par un docker exec ponctuel.

Task 5: If it’s Compose, verify the rendered configuration

cr0x@server:~$ docker compose config | sed -n '/services:/,$p' | sed -n '1,120p'
services:
  api:
    environment:
      APP_ENV: prod
      LOG_LEVEL: info
      PORT: "8080"
    image: myco/api:1.24.7
    restart: always

Signification : La config Compose n’a pas DATABASE_URL. Peut‑être que le fichier env n’a pas été chargé, ou que le nom de la variable a changé.

Décision : Corriger docker-compose.yml ou le chemin du fichier env. Puis redéployer avec un recreate propre pour que l’ancienne config ne traîne pas.

Task 6: Look for healthcheck-driven restarts (it’s sneakier than you think)

cr0x@server:~$ docker inspect api --format '{{json .State.Health}}'
{"Status":"unhealthy","FailingStreak":5,"Log":[{"Start":"2026-01-02T09:20:10.001712312Z","End":"2026-01-02T09:20:10.045221991Z","ExitCode":7,"Output":"curl: (7) Failed to connect to localhost port 8080: Connection refused\n"}]}

Signification : Le processus peut tourner, mais le healthcheck ne parvient pas à joindre le service. Le code de sortie 7 de curl est « failed to connect ».
Certains setups (surtout avec des wrappers Compose ou des superviseurs externes) redémarrent les conteneurs unhealthy.

Décision : Décidez si le healthcheck est faux (mauvais port/interface), trop agressif (intervalle/timeout), ou s’il détecte réellement une app morte. Corrigez le healthcheck ou l’interface du service selon le cas.

Task 7: Determine whether the kernel OOM killer is involved

cr0x@server:~$ docker inspect api --format 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}}'
ExitCode=137 OOMKilled=true Error=
cr0x@server:~$ dmesg -T | tail -n 20
[Thu Jan  2 09:25:13 2026] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=docker.service,mems_allowed=0,oom_memcg=/docker/3b2e...,task_memcg=/docker/3b2e...,task=myco-api,pid=31244,uid=1000
[Thu Jan  2 09:25:13 2026] Killed process 31244 (myco-api) total-vm:2147488kB, anon-rss:612344kB, file-rss:1420kB, shmem-rss:0kB

Signification : Là c’est une autre catégorie de problème. Le conteneur n’a pas « planté », il a été abattu.
OOMKilled=true plus dmesg confirme que le noyau a tué le processus sous pression mémoire du cgroup.

Décision : Augmenter la limite mémoire, réduire l’utilisation mémoire, ou corriger une fuite/régression. Vérifier aussi la contention mémoire au niveau du nœud et les voisins bruyants.

Task 8: Check container memory limits and current usage

cr0x@server:~$ docker inspect api --format 'Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}}'
Memory=536870912 MemorySwap=536870912
cr0x@server:~$ docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.CPUPerc}}'
NAME    MEM USAGE / LIMIT     MEM %     CPU %
api     510MiB / 512MiB       99.6%     140.3%
redis   58MiB / 7.7GiB        0.7%      0.4%

Signification : Une limite de 512MiB avec swap égal à la mémoire est serrée ; vous interdisez quasiment toute marge. Le CPU à 140% suggère un travail intense (plusieurs threads).

Décision : Si cette limite était intentionnelle, ajustez l’app (tailles de heap, caches) et vérifiez le profil mémoire. Si c’était accidentel, augmentez la limite et passez à autre chose.

Task 9: Identify fast-exit issues: wrong entrypoint, missing binary, permissions

cr0x@server:~$ docker inspect api --format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}} User={{json .Config.User}}'
Entrypoint=["/docker-entrypoint.sh"] Cmd=["/app/server"] User="10001"
cr0x@server:~$ docker logs --tail 50 api
/docker-entrypoint.sh: line 8: /app/server: Permission denied

Signification : Le binaire existe mais n’est pas exécutable pour l’utilisateur configuré, ou le système de fichiers est monté noexec, ou la construction de l’image a perdu les bits d’exécution.

Décision : Corriger les permissions de l’image (chmod +x au build), ou exécuter avec un utilisateur qui peut lancer le binaire, ou enlever noexec sur le montage du volume. Ne « corrigez » pas ça par un chmod dans un conteneur en cours d’exécution ; ça ne survivra pas à une reconstruction.

Task 10: Validate mounts and whether a volume hides your shipped files

cr0x@server:~$ docker inspect api --format '{{range .Mounts}}{{println .Destination "->" .Source "type=" .Type}}{{end}}'
/app -> /var/lib/docker/volumes/api_app/_data type= volume
/config -> /etc/myco/api type= bind

Signification : Monter un volume sur /app peut masquer le binaire de l’application livré dans l’image. Si le volume est vide ou obsolète, le conteneur démarre dans un répertoire vide et meurt.

Décision : Ne montez pas par-dessus le chemin de votre application sauf si c’est intentionnel. Déplacez les données réinscriptibles vers /data ou similaire. Si vous voulez le hot‑reload en dev, conservez‑le en dev seulement.

Task 11: Check event stream to see who is killing/restarting it

cr0x@server:~$ docker events --since 10m --filter container=api | tail -n 20
2026-01-02T09:31:10.002345678Z container die 3b2e... (exitCode=137, image=myco/api:1.24.7, name=api)
2026-01-02T09:31:10.120456789Z container start 3b2e... (image=myco/api:1.24.7, name=api)

Signification : Les événements montrent des cycles die/start explicites et les codes de sortie. Si vous voyez des événements kill avec attribution utilisateur/daemon (parfois visible dans les logs d’audit), c’est votre superviseur externe ou une action opérateur.

Décision : Si les redémarrages sont pilotés par une politique, corrigez la cause de sortie sous‑jacente. Si ce sont des actions manuelles/automatisées, trouvez l’automatisation et empêchez‑la de se battre contre vous.

Task 12: Check systemd if Docker is being supervised from the outside

cr0x@server:~$ systemctl status myco-api.service --no-pager
● myco-api.service - MyCo API container
     Loaded: loaded (/etc/systemd/system/myco-api.service; enabled; vendor preset: enabled)
     Active: activating (auto-restart) (Result: exit-code) since Thu 2026-01-02 09:33:12 UTC; 4s ago
    Process: 32511 ExecStart=/usr/bin/docker run --rm --name api myco/api:1.24.7 (code=exited, status=1/FAILURE)
   Main PID: 32511 (code=exited, status=1/FAILURE)

Signification : systemd redémarre le lanceur de conteneur, pas Docker lui‑même. Votre boucle de redémarrage pourrait ne pas être une politique Docker du tout.

Décision : Corriger le fichier d’unité (environnement, montages, backoff de redémarrage). Décidez aussi si cela devrait être géré par Compose à la place, pour éviter deux superviseurs jouant à la corde.

Task 13: Reproduce without restart to preserve evidence

cr0x@server:~$ docker inspect api --format 'RestartPolicy={{.HostConfig.RestartPolicy.Name}}'
RestartPolicy=always
cr0x@server:~$ docker update --restart=no api
api
cr0x@server:~$ docker start -a api
2026-01-02T09:35:01.110Z level=error msg="fatal: cannot open /config/app.yaml" err="permission denied"

Signification : Désactiver le redémarrage arrête la boucle et vous permet de vous attacher à l’échec. C’est souvent le moyen le plus rapide d’arrêter de perdre des logs.

Décision : Utilisez ceci pour le debug. Puis restaurez la politique de redémarrage souhaitée après la correction. Ne laissez pas les services de production avec restart désactivé sauf si vous aimez les surprises nocturnes.

Task 14: Validate permissions and user mapping on bind mounts

cr0x@server:~$ ls -l /etc/myco/api/app.yaml
-rw------- 1 root root 2180 Jan  2 09:00 /etc/myco/api/app.yaml
cr0x@server:~$ docker inspect api --format 'User={{.Config.User}}'
10001

Signification : Le conteneur s’exécute en UID 10001, mais le fichier monté est lisible seulement par root. C’est un mode d’échec propre et ennuyeux.

Décision : Corriger la propriété/permissions du fichier hôte, ou exécuter le conteneur avec un utilisateur qui peut le lire, ou utiliser l’injection de secrets/config prévue à cet effet.

Task 15: Check DNS/network dependency quickly from a debug container on the same network

cr0x@server:~$ docker network ls
NETWORK ID     NAME              DRIVER    SCOPE
6c0f1b1e2c0a   myco_default      bridge    local
cr0x@server:~$ docker run --rm --network myco_default busybox:1.36 nslookup postgres
Server:    127.0.0.11
Address 1: 127.0.0.11

Name:      postgres
Address 1: 172.19.0.3 postgres.myco_default

Signification : Le DNS à l’intérieur du réseau Docker résout postgres. Si votre app dit « host not found », le problème peut être la config de l’app ou elle est sur un réseau différent.

Décision : Si le DNS échoue ici, corrigez le réseau Docker ou le nom de service. Si le DNS fonctionne, pivotez vers les identifiants, TLS, pare‑feu, ou la temporisation readiness.

Blague courte #1 : Un conteneur en boucle de redémarrage est juste la façon de DevOps d’enseigner la patience, encore et encore, avec conviction.

Codes de sortie à mémoriser

Les codes de sortie sont la chose la plus proche d’un aveu. Docker affiche le code de sortie, mais vous devez encore l’interpréter selon les conventions Unix et les réalités spécifiques aux conteneurs.

Les utiles

  • 0 : sortie propre. Si ça redémarre quand même, quelqu’un a ordonné le redémarrage.
  • 1 : erreur générique. Regardez les journaux ; c’est généralement une configuration ou une exception lancée.
  • 2 : mauvaise utilisation des builtins shell/erreur d’usage CLI ; souvent des flags incorrects ou des erreurs de script d’entrée.
  • 125 : Docker n’a pas pu exécuter le conteneur (erreur du démon, options invalides). Ce n’est pas votre app.
  • 126 : commande invoquée non exécutable (permissions, mauvaise architecture, mount noexec).
  • 127 : commande introuvable (mauvais chemin ENTRYPOINT/CMD, shell manquant, PATH incorrect).
  • 128 + N : le processus est mort d’un signal N. Courants : 137 (SIGKILL=9), 143 (SIGTERM=15).
  • 137 : SIGKILL. Souvent OOM killer, parfois kill forcé par un watchdog.
  • 139 : SIGSEGV. Plantage natif ; peut être une libc défaillante, binaire corrompu, ou corruption mémoire.

Comment les codes de sortie interagissent avec les politiques de redémarrage

La politique de redémarrage on-failure se déclenche sur les codes de sortie non nuls. Donc elle ne redémarrera pas sur une sortie 0.
La politique always s’en fiche ; elle redémarre quoi qu’il arrive, ce qui peut masquer une app qui sort volontairement après avoir fait son travail.

Si vous exécutez un conteneur de type job (migrations, cron, batch), always est généralement incorrect. Si vous exécutez un service, always convient — jusqu’à ce que vous déployiez quelque chose qui sort instantanément et que vous perdiez le contexte des logs dans la spirale.

Healthchecks : quand « healthy » devient un détecteur de mensonges

Les healthchecks sont utiles. Les mauvais healthchecks sont des générateurs de chaos.
Ils sont aussi fréquemment mal compris : le healthcheck intégré de Docker ne redémarre pas automatiquement les conteneurs. Mais de nombreux superviseurs et patterns de déploiement traitent « unhealthy » comme « kill et restart ».

Comment les healthchecks échouent en production

  • Mauvaise interface/port : l’app se bind sur 0.0.0.0 mais le healthcheck cible localhost à tort — ou l’inverse.
  • Temps de démarrage : le healthcheck commence avant que l’app soit prête, provoquant une série d’échecs et des redémarrages.
  • Couplage de dépendances : le healthcheck appelle des services en aval. Quand l’un d’eux est down, votre conteneur se fait tuer alors qu’il pourrait servir un trafic partiel.
  • Pics de ressources : le healthcheck est trop fréquent ; sur un nœud chargé, il fait basculer le service.
  • Utiliser curl dans des images minimales : commande healthcheck introuvable renvoie exit 127, ce qui semble être « app morte » alors que curl n’est juste pas installé.

Que faire

Les healthchecks doivent tester la capacité de votre service à servir, pas l’univers entier.
Si une BD est down, il est valide qu’une app signale unhealthy — si l’app ne peut pas fonctionner sans elle. Mais ne mettez pas toutes les dépendances externes dans l’endpoint de healthcheck à moins d’être sûr que redémarrer aide.

Ajustez start_period (si disponible), les intervalles et timeouts. Surtout : gardez les healthchecks déterministes et peu coûteux.

Pièges stockage & performance qui ressemblent à des crashes

En tant que personne stockage, je vais le dire clairement : beaucoup d’incidents « le conteneur redémarre » sont en réalité « l’IO a ralenti, des timeouts ont eu lieu, le processus est sorti ».
Docker se fiche de la raison pour laquelle votre processus est sorti. Il voit juste une sortie. Votre app peut quitter sur une migration BD échouée, un timeout de lock, ou « disque plein ».

Disque plein : le classique qui ne meurt jamais

Les conteneurs écrivent des logs, des fichiers temporaires, des fichiers de base de données (parfois par accident), et des diffs de couche. Si la racine Docker se remplit, les conteneurs commencent à échouer de façons charmantes :
échecs de write(), fichiers temporaires corrompus, bases refusant de démarrer, ou votre app plantant parce qu’elle ne peut pas écrire un fichier PID.

cr0x@server:~$ df -h /var/lib/docker
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p4   80G   79G  320M 100% /var/lib/docker

Signification : Vous êtes à court de piste. Attendez‑vous à des échecs aléatoires.

Décision : Libérez de l’espace (prunez images/volumes avec précaution), déplacez Docker root sur un filesystem plus grand, ou arrêtez d’écrire des gros fichiers dans les couches du conteneur. Puis corrigez la rétention/logging pour que ça ne se reproduise pas.

Amplification d’écriture overlay et « ça ne redémarre que sous charge »

Overlay2 convient pour la plupart des usages, mais si votre workload écrit beaucoup dans la couche de filesystem du conteneur (pas les volumes), la performance peut s’effondrer.
Quand la latence monte, les timeouts en cascade : l’app échoue la readiness, les healthchecks échouent, l’orchestrateur tue, des redémarrages arrivent.

Conseils pratiques : les données modifiables vont dans des volumes. Les logs vont sur stdout/stderr (et sont collectés par un driver de logs sensé), pas dans un fichier dans la couche image. Les bases de données ne doivent pas écrire sur overlay à moins d’apprécier la latence fsync à 3 h du matin.

Permissions et décalages UID sur bind mounts

La bonne pratique de sécurité est « exécuter en non‑root ». Super. Puis vous montez des fichiers hôtes appartenant à root et vous vous demandez pourquoi ça redémarre.
Ce n’est pas Docker qui est méchant. C’est Linux qui est Linux.

Horloge et échecs TLS : la dépendance non évidente

Si l’horloge de l’hôte dérive, TLS peut échouer. Les apps qui considèrent « impossible d’établir TLS » comme fatal peuvent sortir immédiatement. Cela ressemble à une boucle de redémarrage avec exit code 1.
Si vous voyez des redémarrages soudains et larges sur des services parlant TLS, vérifiez NTP/chrony et la validité des certificats.

Trois mini-histoires d’entreprise tirées de la vraie vie

1) Incident causé par une fausse hypothèse : « depends_on signifie ready »

Une entreprise de taille moyenne exécutait une stack Compose : API, worker, Postgres, Redis. Ça marchait depuis des mois, jusqu’à un redémarrage d’hôte pendant une maintenance.
Après le reboot, le conteneur API s’est mis à flapper. Redémarrages toutes les secondes. Les ingénieurs ont accusé une « mauvaise image » parce que le déploiement avait eu lieu la veille.

L’hypothèse erronée était subtile et courante : ils croyaient que depends_on signifiait que Postgres était prêt à accepter des connexions. En réalité, Compose démarrait Postgres en premier, mais Postgres avait encore besoin de temps pour la récupération et l’initialisation.
L’API a tenté d’exécuter des migrations au démarrage, n’a pas pu se connecter, et est sortie avec le code 1. Docker l’a redémarrée fidèlement. Encore et encore.

Les journaux étaient là, mais enterrés — parce que la boucle de redémarrage était rapide et que la sortie de logs était mélangée à d’autres lignes. L’équipe a d’abord chassé des problèmes réseau : « Le DNS Docker est-il cassé après reboot ? » Non. Ils ont lancé un conteneur de debug et vérifié DNS et connectivité.

La solution a été ennuyeuse et correcte : ajouter une logique de retry avec backoff exponentiel pour la connexion BD au démarrage, et séparer les migrations en job one‑shot avec sortie d’erreur claire.
Ils ont aussi ajouté un healthcheck à Postgres et fait en sorte que l’API attende la readiness (ou au moins gère « not ready » sans sortir).

L’incident s’est terminé non pas par des héros, mais par une reconnaissance : l’ordre d’orchestration n’est pas la readiness, et la fiabilité vient de démarrages qui tolèrent le monde réel.

2) Optimisation qui s’est retournée contre eux : « on peut réduire les limites mémoire pour l’efficacité »

Une autre organisation a voulu réduire les coûts infra et « serrer l’utilisation des ressources ». Ils ont abaissé les limites mémoire des conteneurs sur plusieurs services.
Ça avait l’air correct en staging, où le trafic était faible et les caches froids. La production n’est pas staging. Jamais.

En un jour, une API clé a commencé à redémarrer de façon intermittente. Pas constamment — juste assez pour rendre la surveillance bruyante et les clients mécontents.
Le code de sortie était 137. Kills OOM. Le service utilisait un runtime managé avec un heap adaptatif, plus un workload JSON subjectif qui provoquait des allocations bursty.

Les ingénieurs ont d’abord essayé d’ajuster le GC du runtime et de limiter le heap. Ça a aidé, mais ils ont manqué un effet de second ordre : une nouvelle « optimisation » dans le même changement augmentait le niveau de compression des réponses pour économiser de la bande passante.
Le CPU est monté, la latence a augmenté, et comme les requêtes s’empilaient, la pression mémoire s’est aggravée. Les kills OOM ont augmenté.

La vraie correction a été d’annuler le changement de compression pour cet endpoint et de restaurer une limite mémoire réaliste avec marge. Puis ils ont profilé les allocations sous charge proche de la prod et appliqué des réductions ciblées.
La leçon : des limites « efficaces » qui provoquent des OOM ne sont pas efficaces ; ce sont des impôts payés en incidents.

3) Pratique ennuyeuse mais correcte qui a sauvé la journée : arrêter la boucle, préserver les preuves

Une société financière avait un service conteneurisé qui s’est mis à redémarrer après une rotation de certificats.
Leur runbook d’astreinte incluait une ligne simple : « Si un conteneur flappe, désactivez le redémarrage et lancez‑le attaché une fois pour capturer l’erreur fatale. »
Ce n’était pas glamour. Ce n’était pas de la sorcellerie. Ça a marché.

Ils ont exécuté docker update --restart=no puis docker start -a. L’app a immédiatement imprimé une erreur TLS claire : elle ne pouvait pas lire la nouvelle clé privée.
La clé avait été déployée avec des permissions restrictives sur un bind mount, lisible seulement par root, alors que le conteneur tournait en UID non‑root.

Sans arrêter la boucle, les journaux auraient été partiels et écrasés par les redémarrages répétés. En arrêtant la boucle, le message d’échec était inratable.
Ils ont corrigé la propriété du fichier, redémarré le service, et sont passés à autre chose.

L’amélioration suivante a été encore plus ennuyeuse : ils ont changé le déploiement des certificats pour utiliser un mécanisme de secrets avec des permissions correctes par défaut, et ajouté une vérification de démarrage qui rapporte une erreur claire avant d’essayer de servir du trafic.

Erreurs courantes : symptôme → cause racine → correction

Cette section est le catalogue « je l’ai déjà vu ». Si vous êtes en astreinte, parcourez les symptômes, choisissez la cause probable, et testez‑la avec une des tâches ci‑dessus.

1) Redémarrages toutes les 2–10 secondes, exit code 1

  • Symptôme : Restarting (1), les logs montrent des erreurs de config ou des env manquantes.
  • Cause racine : variable d’env manquante, flag incorrect, secrets non montés, échec de parsing de config.
  • Correction : corriger l’injection env Compose ; valider avec docker compose config ; redéployer avec recreate.

2) Redémarre avec exit code 127 ou « command not found »

  • Symptôme : journaux : exec: "foo": executable file not found in $PATH.
  • Cause racine : mauvais CMD/ENTRYPOINT, binaire manquant, utilisation d’une image Alpine sans bash alors que l’entrypoint l’utilise.
  • Correction : corriger le Dockerfile entrypoint ; préférer la forme exec ; s’assurer que le shell requis existe ou supprimer la dépendance au shell.

3) Redémarre avec exit code 126 ou « permission denied »

  • Symptôme : le binaire existe mais n’est pas exécutable, ou le script d’entrée n’est pas exécutable.
  • Cause racine : mauvais mode de fichier, mount noexec, ownership erroné en mode non‑root.
  • Correction : mettre le bit exécutable au build ; ajuster les options de montage ; aligner UID/GID ou permissions.

4) Exit code 137, sporadique sous charge

  • Symptôme : OOMKilled=true dans inspect ; dmesg montre oom‑kill.
  • Cause racine : limite mémoire conteneur trop basse ; fuite mémoire ; pic de charge ; trop peu de swap.
  • Correction : augmenter la limite, ajuster heap/caches, étudier le profil mémoire ; réduire la concurrence ; ajouter du backpressure.

5) « Up » mais bascule constamment healthy/unhealthy puis redémarre

  • Symptôme : échec de healthcheck en streak ; redémarrages si l’orchestrateur réagit à unhealthy.
  • Cause racine : healthcheck agressif, mauvais endpoint, dépendances testées, app écoute une autre interface.
  • Correction : rendre le healthcheck bon marché et correct ; ajuster intervalle/timeout/start_period ; s’assurer que l’app écoute comme prévu.

6) Fonctionne sur un hôte, flappe sur un autre

  • Symptôme : même image, comportement différent.
  • Cause racine : différences de noyau/cgroup, disque plein, options de montage différentes, config DNS, mismatch d’architecture CPU.
  • Correction : comparer docker info, espace disque hôte, options de montage ; vérifier l’arch de l’image ; standardiser le runtime.

7) Le conteneur sort « avec succès » (code 0) mais redémarre sans fin

  • Symptôme : code de sortie 0 ; politique de redémarrage always.
  • Cause racine : vous exécutez un job (migrations, init, CLI) avec une politique de service.
  • Correction : utiliser restart: "no" ou on-failure ; séparer le job du service longue durée.

8) Après un « petit changement », tout se met à flapper

  • Symptôme : plusieurs conteneurs redémarrent autour du même moment.
  • Cause racine : dépendance partagée : panne DNS, rotation de certs, dérive d’horloge, throttling du registre, disque plein, pression mémoire hôte.
  • Correction : vérifier signaux hôte (disque, dmesg, synchro temps) ; valider permissions secrets/certs ; rollback du changement partagé.

Blague courte #2 : Si votre healthcheck dépend de cinq autres services, ce n’est pas un healthcheck — c’est un travail de groupe.

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

Checklist A: Arrêter l’hémorragie (sûr en production)

  1. Confirmer l’étendue : est‑ce un conteneur ou plusieurs ? Si plusieurs, suspecter un problème au niveau hôte (disque, mémoire, DNS, heure).
  2. Capturer les preuves : saisir docker inspect State, les 200 dernières lignes de logs, et docker events pour 10 minutes.
  3. Stabiliser : si la boucle est trop rapide, désactivez temporairement le redémarrage (docker update --restart=no) pour préserver les logs et réduire le churn.
  4. Choisir l’action : rollback du tag d’image, corriger env/secrets, augmenter la mémoire, ou corriger le healthcheck.
  5. Communiquer clairement : « Exit code 137, OOM kill confirmé dans dmesg. Augmentation de la limite et rollback du changement mémoire. » Pas « Docker est bizarre. »

Checklist B: Trouver le déclencheur de façon propre et reproductible

  1. Identifier la politique de redémarrage et le superviseur (Docker vs Compose vs systemd).
  2. Lire le code de sortie et le flag OOM.
  3. Lire les logs du dernier essai (--tail, avec horodatages).
  4. Vérifier les logs de santé si healthchecks configurés.
  5. Vérifier les montages et permissions (surtout en non‑root).
  6. Vérifier la connectivité des dépendances depuis le même réseau Docker.
  7. Vérifier l’espace disque hôte et les logs OOM hôte.
  8. Relancer le conteneur attaché une fois avec restart désactivé pour reproduire proprement.

Checklist C: Prévenir la récidive (la partie que les gens sautent)

  1. Rendre le démarrage résilient : retry des dépendances avec backoff ; ne pas sortir instantanément sur des failures transitoires.
  2. Séparer les jobs one‑shot : migrations et changements de schéma doivent être des jobs explicites, pas cachés dans le boot du service principal.
  3. Dimensionner correctement les healthchecks : peu coûteux, déterministes, pas dépendants du monde entier.
  4. Fixer des limites sensées : marge mémoire, contraintes CPU adaptées au workload, et éviter des limites choisies par optimisme.
  5. Logger sur stdout/stderr : garder les logs des conteneurs accessibles et centralisables.
  6. Documenter les invariants : env vars requises, montages nécessaires, permissions attendues, comportement de sortie attendu.

Ce qu’il faut éviter quand vous déboguez une boucle de redémarrage

  • Ne reconstruisez pas les images en boucle jusqu’à ce que vous puissiez citer le code de sortie et la dernière ligne fatale des logs.
  • Ne faites pas d’abord un exec dans le conteneur ; il peut mourir avant que vous n’appreniez quoi que ce soit. Commencez par inspect/logs/events.
  • Ne mettez pas « restart: always » partout comme sparadrap. Ça cache les conteneurs job et peut amplifier les tempêtes d’échec.
  • Ne blâmez pas Docker avant d’avoir vérifié disque plein et OOM. Docker rapporte surtout ce que Linux a fait.

FAQ

1) Pourquoi docker ps affiche « Restarting (1) » ?

Cela signifie que le conteneur sort et que Docker applique une politique de redémarrage. Le nombre est le dernier code de sortie observé.
Confirmez avec docker inspect et lisez .State.ExitCode ainsi que les horodatages.

2) Comment savoir si c’est Docker qui le redémarre ou autre chose ?

Vérifiez la politique de redémarrage dans docker inspect (.HostConfig.RestartPolicy), puis cherchez des superviseurs externes :
systemctl status pour les unités, et docker events pour les motifs start/stop. Compose ajoute aussi son propre comportement de cycle de vie.

3) Quelle est la façon la plus rapide d’attraper le vrai message d’erreur ?

Désactivez temporairement le redémarrage (docker update --restart=no) et lancez attaché une fois (docker start -a).
Ça arrête le tumulte et affiche clairement la ligne fatale.

4) Exit code 137 : est-ce toujours OOM ?

Non. Cela signifie SIGKILL. L’OOM est la cause la plus courante dans les conteneurs, mais un superviseur peut aussi SIGKILL un processus.
Confirmez avec docker inspect (OOMKilled=true) et les logs hôte (dmesg).

5) Pourquoi docker logs est vide alors que l’app échoue ?

Parce que Docker ne capture que stdout/stderr. Si votre app logge dans des fichiers à l’intérieur du conteneur, docker logs peut être muet.
Reconfigurez l’app pour logger sur stdout/stderr ou inspectez les fichiers (idéalement via un volume, pas la couche du conteneur).

6) Le conteneur est « unhealthy » mais le processus tourne. Pourquoi redémarrer ?

Le statut de santé Docker ne redémarre pas intrinsèquement le conteneur, mais beaucoup de patterns de déploiement le font : superviseurs externes, scripts, ou orchestrateurs interprètent unhealthy comme « remplacer ».
Corrigez l’endpoint healthcheck, le timing, ou la sensibilité, ou ajustez le comportement du superviseur.

7) Pourquoi ça marche quand je le lance manuellement mais pas sous Compose ?

Compose change les réseaux, l’injection d’environnement, les montages de volumes, et parfois le répertoire de travail. Comparez :
docker compose config vs docker inspect pour le conteneur en cours d’exécution. Les différences de montages et env vars sont les coupables habituels.

8) Comment déboguer un conteneur qui sort trop vite pour y faire un exec ?

Utilisez d’abord docker logs et docker inspect. Si besoin, désactivez le restart et lancez attaché une fois.
Vous pouvez aussi remplacer temporairement l’entrypoint pour obtenir un shell et inspecter le système de fichiers, mais considérez cela comme une expérience contrôlée, pas la solution.

9) Les problèmes disque peuvent-ils vraiment causer des boucles de redémarrage ?

Absolument. Disque plein, IO lente, ou problèmes de permissions sur des volumes peuvent empêcher les apps de passer leurs checks de démarrage, planter, ou timeout.
Vérifiez df -h pour la racine Docker, inspectez les montages, et cherchez « no space left on device » dans les logs.

10) Sur quoi dois‑je alerter pour détecter ça tôt ?

Alertez sur l’augmentation des compteurs de redémarrage, les basculements d’état de santé, les événements OOM kill, l’utilisation disque de la racine Docker, et les taux élevés de sorties de conteneurs.
Les redémarrages ne sont pas intrinsèquement mauvais ; les redémarrages inattendus le sont. Établissez une baseline.

Conclusion : étapes suivantes pour éviter la récidive

Une boucle de redémarrage de conteneur semble chaotique, mais elle est généralement déterministe. Arrêtez de deviner.
En cinq minutes, vous pouvez savoir : qui le redémarre, quel code de sortie il renvoie, s’il a été tué par OOM, et quelle était la dernière ligne fatale des logs.
Après ça, la correction est typiquement banale : corriger env/secrets, ajuster permissions, modifier le comportement du healthcheck, ou donner assez de mémoire au processus pour qu’il survive.

Faites ceci ensuite :

  1. Standardisez un runbook : inspect → logs → events → signaux hôte (disque/OOM) → reproduire attaché une fois.
  2. Rendez le démarrage tolérant : retries avec backoff, timeouts, et messages fatals clairs.
  3. Séparez jobs et services ; ne lancez pas les migrations comme effet secondaire du boot du service à moins d’en avoir l’intention.
  4. Auditez les politiques de redémarrage : utilisez always pour les services, on-failure pour les jobs de plantage seulement, et no pour les one‑shots.
  5. Déplacez les données réinscriptibles vers des volumes et gardez les logs accessibles sur stdout/stderr.

Vous n’avez pas besoin d’héroïsme. Vous avez besoin de preuves, vite. Puis de la discipline pour changer ce qui a réellement cassé.

© 2026. Écrits pratiques pour les équipes qui livrent et exploitent leurs systèmes.

← Précédent
Mots de passe perdus et chiffrement : quand les erreurs deviennent permanentes
Suivant →
Politiques de quarantaine et dossier spam : ne perdez plus de messages importants

Laisser un commentaire