Politiques de redémarrage Docker : stoppez les boucles de plantage infinies

Cet article vous a aidé ?

Il existe un type d’incident où rien n’est « down » parce que tout est constamment en train de « démarrer ». Vos tableaux de bord montrent des pics CPU, le volume des logs s’envole, et le nom du conteneur est un flou de redémarrages. Vous essayez de faire un docker exec, mais le processus meurt avant que votre invite de shell n’apparaisse. Félicitations : vous avez construit une boucle de plantage infinie.

Les politiques de redémarrage Docker sont conçues pour rendre les services résilients. En production, elles peuvent aussi transformer une petite faute en un incident auto-entretenu : bruyant, coûteux et difficile à déboguer. Voici comment arrêter ça.

Politiques de redémarrage : ce qu’elles font vraiment (pas ce que vous espérez)

Les politiques de redémarrage Docker sont simples sur le papier. En pratique, elles constituent un contrat entre le cycle de vie de votre conteneur et le comportement opinionné du démon. Elles ne rendent pas votre application plus saine. Elles la rendent persistante. Ce sont des propriétés différentes, et les confondre est la manière de créer des boucles de plantage infinies avec « auto-réparation » inscrit dans le post-mortem.

Les quatre politiques que vous utilisez réellement

  • no (par défaut) : Docker ne redémarrera pas le conteneur lorsqu’il se termine. Ce n’est pas « unsafe ». C’est souvent le choix le plus sensé pour les jobs batch et les tâches ponctuelles.
  • on-failure[:max-retries] : Redémarre uniquement si le conteneur se termine avec un code non nul. Optionnellement s’arrête après N tentatives. C’est ce qui se rapproche le plus de « essayer un peu, puis arrêter d’être bizarre ».
  • always : Redémarre quel que soit le code de sortie. Si le démon redémarre, le conteneur revient aussi. C’est la politique qui transforme « arrêt propre » en « résurrection surprise ».
  • unless-stopped : Comme always, sauf qu’un arrêt manuel survit aux redémarrages du démon. C’est « always, mais avec respect pour l’intervention humaine ».

Ce que Docker considère comme un « redémarrage » (et pourquoi c’est important)

Docker redémarre un conteneur quand le processus principal du conteneur se termine. C’est le PID 1 à l’intérieur du conteneur. Si votre PID 1 est un script shell qui fork votre vrai service puis se termine, Docker interprétera cela comme « le service est mort » et le redémarrera fidèlement… pour toujours. La politique de redémarrage n’est pas cassée ; votre stratégie d’init l’est.

Autre point : Docker a un délai/backoff de redémarrage intégré. Ce n’est pas un disjoncteur configurable dans Docker Engine classique. Il empêche une boucle serrée de redémarrages par seconde, mais il n’arrêtera pas une boucle persistante. Il rend juste votre incident plus long et plus déroutant.

Une citation, parce qu’elle reste vraie en 2026 : « L’espoir n’est pas une stratégie. » — Général Gordon R. Sullivan.

Comment choisir une politique en production (version opinionnée)

Si vous exécutez des services de production sur des hôtes uniques (ou de petites flottes) avec Docker Engine ou Compose, traitez les politiques de redémarrage comme une garde-fou de dernier kilomètre, pas comme votre mécanisme principal de fiabilité.

  • Utilisez on-failure:5 par défaut pour la plupart des services longue durée qui devraient rarement crasher. S’il ne peut pas démarrer après 5 essais, quelque chose ne va pas. Arrêtez et alertez.
  • Utilisez unless-stopped quand vous avez une bonne raison (p. ex. sidecars infra simples, dev local, ou un hôte qui doit remonter proprement après un reboot). N’oubliez pas : instrumentez-le.
  • Évitez always pour tout ce qui peut échouer rapidement (mauvaise config, secret manquant, mismatch de schéma, migrations). « Always » est la manière de brûler du CPU sans rien faire d’utile.
  • Utilisez no pour les jobs batch. Si votre job de rapport nocturne échoue, vous voulez probablement qu’il échoue bruyamment, pas qu’il tourne en boucle et envoie 400 emails à la compta.

Première blague courte : Les conteneurs ne se soignent pas eux-mêmes ; ils deviennent juste très doués pour la réincarnation.

Faits et historique qui changent la manière de penser les redémarrages

Un peu de contexte rend le comportement de redémarrage de Docker moins arbitraire et plus proche d’un ensemble de compromis qui ont fini par sonner votre pager.

  1. Les politiques de redémarrage Docker précèdent l’adoption grand public de Kubernetes, à une époque où la gestion de conteneurs sur un seul hôte était la norme et « le maintenir en marche » était la demande principale.
  2. Le « problème du PID 1 » est une vieille histoire Unix : signaux, zombies et collecte des processus. Les conteneurs ne l’ont pas créé ; ils ont rendu son ignorance impossible.
  3. La sémantique des codes de sortie est un contrat : Docker les utilise pour on-failure. Si votre appli retourne 0 lors d’une erreur (« tout va bien ! »), vous avez choisi le chaos.
  4. Les boucles de redémarrage existaient bien avant les conteneurs : les unités systemd avec Restart=always peuvent faire le même dommage. Docker a juste facilité ça via une one-liner.
  5. Les healthchecks sont arrivés plus tard que beaucoup ne le pensent. Pendant longtemps, « conteneur up » signifiait « processus existe », pas « le service fonctionne ». Cet héritage façonne encore les pratiques courantes.
  6. Les drivers de logs ont une histoire : le driver par défaut json-file facilitait le remplissage des disques lors des redémarrages. Ce n’est pas théorique ; c’est un récidiviste.
  7. Le comportement OOM-kill est une réalité au niveau du noyau : le conteneur n’a pas « crashé », le noyau l’a tué. Docker rapporte le symptôme ; vous devez encore lire l’autopsie.
  8. Le backoff de Docker n’est pas un disjoncteur complet. Il ralentit la fréquence des redémarrages, mais il ne décide pas de s’arrêter. Cette décision vous revient via la politique et l’automatisation.

Pourquoi les boucles de plantage arrivent : modes de défaillance, pas fautes morales

Une boucle de plantage est généralement l’un de ces cas :

  • Mauvaise configuration ou dépendance manquante : variable d’environnement erronée, fichier absent, DNS défaillant, DB inaccessible, secret non monté.
  • L’appli se termine volontairement : migrations requises, vérification de licence échouée, feature flag invalide, image « run once » utilisée comme service.
  • Pression sur les ressources : OOM kills, throttling CPU provoquant des timeouts, disque plein, épuisement d’inodes, limites de descripteurs de fichiers.
  • Mauvais ordre de démarrage : l’appli démarre avant la DB/queue prête ; sans logique de retry elle se termine immédiatement.
  • Mauvais PID 1 : scripts shell qui se terminent tôt ; pas d’init ; signaux non gérés ; accumulation de zombies puis crash.
  • État corrompu : volumes contenant des upgrades partiels, fichiers de verrou, ou versions de schéma incompatibles avec le binaire.
  • Throttling externe : limitation de taux en amont ; l’application le traite comme fatal et se termine ; les redémarrages amplifient le thundering herd.

Deuxième blague courte : « Nous avons mis restart: always pour la fiabilité » est l’équivalent conteneur de coller du ruban adhésif sur le voyant moteur.

Plan de diagnostic rapide

Lorsque vous êtes en incident et que le conteneur flappe, vous n’avez pas le temps pour la philosophie. Voici l’ordre qui trouve le goulot d’étranglement rapidement.

Première étape : établissez s’il s’agit d’une défaillance applicative ou d’une défaillance plateforme

  1. Vérifiez le compteur de redémarrages et le dernier code de sortie. Si vous voyez des codes 1/2/78 ou similaires, c’est probablement l’app/config. Si vous voyez 137, pensez OOM/kill. Si vous voyez 0 avec des redémarrages, votre politique est always ou le démon a redémarré.
  2. Regardez les 50 dernières lignes de log du run précédent. Vous cherchez une erreur explicite, pas « starting… » répété à l’infini.
  3. Vérifiez le dmesg/journal de l’hôte pour OOM ou erreurs disque. Les conteneurs ne peuvent pas vous dire que le noyau les a tués à moins que vous consultiez le noyau.

Deuxième étape : arrêter l’hémorragie (sans perdre les preuves)

  1. Désactivez temporairement les redémarrages afin d’inspecter l’état et les logs. Ne supprimez pas le conteneur à moins d’avoir déjà capturé ce dont vous avez besoin.
  2. Capturez la config et inspectez les mounts. La plupart des « boucles mystères » sont des « mauvais chemins de fichier » avec quelques étapes en plus.

Troisième étape : décidez si vous réparez l’app, l’hôte ou la politique

  1. Si c’est une config/dépendance, corrigez la configuration, ou implémentez des retries/backoff dans l’app. La politique de redémarrage n’est pas un algorithme de retry.
  2. Si c’est une pression sur les ressources, définissez correctement les limites mémoire, ajustez la journalisation, augmentez le disque ou déplacez les charges. Les politiques de redémarrage ne créent pas de RAM.
  3. Si c’est une mauvaise politique, passez à on-failure:5 ou unless-stopped avec alerting sur les redémarrages.

Tâches pratiques (commandes + sorties + décisions)

Vous vouliez des commandes. Vous obtenez des commandes. Chaque tâche inclut : quoi exécuter, ce que signifie la sortie, et quelle décision prendre ensuite.

Task 1: Identify the flapping container fast

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.RunningFor}}'
NAMES              IMAGE                 STATUS                          RUNNING FOR
api                myco/api:1.42.0        Restarting (1) 6 seconds ago    2 minutes ago
postgres           postgres:16            Up 3 hours (healthy)            3 hours ago
nginx              nginx:1.25             Up 3 hours                      3 hours ago

Signification : api est en redémarrage ; Docker affiche le dernier code de sortie entre parenthèses. Il échoue rapidement (tous les quelques secondes).

Décision : Concentrez-vous sur api. Ne touchez pas encore aux dépendances saines.

Task 2: Inspect restart policy and restart count

cr0x@server:~$ docker inspect -f 'Name={{.Name}} Policy={{.HostConfig.RestartPolicy.Name}} Max={{.HostConfig.RestartPolicy.MaximumRetryCount}} Restarts={{.RestartCount}}' api
Name=/api Policy=always Max=0 Restarts=37

Signification : La politique est always ; le nombre maximal de retries est illimité ; 37 redémarrages déjà.

Décision : Vous êtes dans une « boucle infinie par conception ». Prévoyez de désactiver les redémarrages pour déboguer.

Task 3: Get the last exit code and termination reason

cr0x@server:~$ docker inspect -f 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}} FinishedAt={{.State.FinishedAt}}' api
ExitCode=1 OOMKilled=false Error= FinishedAt=2026-01-03T09:14:22.948601234Z

Signification : Code de sortie 1, pas OOM, pas d’erreur runtime. Probablement logique applicative/config/dépendance.

Décision : Lisez les logs du démarrage ayant échoué.

Task 4: Pull logs from the previous run (not just the current flicker)

cr0x@server:~$ docker logs --tail 80 api
2026-01-03T09:14:21Z INFO starting api version=1.42.0
2026-01-03T09:14:22Z ERROR config: missing required env var: DATABASE_URL
2026-01-03T09:14:22Z INFO exiting with code=1

Signification : Le conteneur se comporte correctement : il refuse de démarrer sans configuration.

Décision : Corrigez l’injection d’environnement/secret. Aussi : c’est exactement pourquoi always est un mauvais défaut — cette erreur ne s’auto-résoudra jamais.

Task 5: Confirm the container’s environment (what Docker thinks it is)

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

Signification : Pas de DATABASE_URL. Ce n’est pas un problème DNS à l’exécution ; c’est une configuration manquante au moment de la création du conteneur.

Décision : Mettez à jour Compose, l’unité systemd ou l’outil de déploiement pour fournir la variable (ou montez un fichier et pointez dessus).

Task 6: Stop the restart loop without deleting the container

cr0x@server:~$ docker update --restart=no api
api

Signification : Politique de redémarrage mise à jour. Docker ne le redémarrera pas après le prochain arrêt/exit.

Décision : Arrêtez-le proprement, puis corrigez la configuration, puis démarrez-le une fois avec des paramètres connus bons.

Task 7: Stop it now (so you can inspect state and host pressure)

cr0x@server:~$ docker stop -t 10 api
api

Signification : Le conteneur est arrêté et doit rester arrêté parce que vous avez mis restart=no.

Décision : S’il revient encore, quelque chose d’autre (systemd, watchdog) le respawn.

Task 8: Check whether systemd is restarting Docker itself (daemon restarts can look like container restarts)

cr0x@server:~$ systemctl status docker --no-pager
● docker.service - Docker Application Container Engine
     Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
     Active: active (running) since Sat 2026-01-03 09:00:05 UTC; 20min ago
TriggeredBy: ● docker.socket
       Docs: man:docker(1)
   Main PID: 1123 (dockerd)
      Tasks: 23
     Memory: 312.4M
        CPU: 1min 54.931s

Signification : Le démon Docker est stable pour l’instant.

Décision : Traitez cela comme un problème applicatif/conteneur, pas comme un démon instable.

Task 9: Check for OOM kills at the host level (even when Docker says OOMKilled=false)

cr0x@server:~$ sudo journalctl -k --since "10 min ago" | tail -n 12
Jan 03 09:12:01 server kernel: Memory cgroup out of memory: Killed process 24081 (node) total-vm:1820040kB, anon-rss:612340kB, file-rss:2140kB, shmem-rss:0kB
Jan 03 09:12:01 server kernel: oom_reaper: reaped process 24081 (node), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

Signification : Le noyau a tué un processus dans un cgroup mémoire. Selon le timing et la manière dont le conteneur est mort, les flags d’état de Docker ne racontent pas toujours toute l’histoire.

Décision : Si cela correspond à votre PID de conteneur, vous avez besoin de limites mémoire et/ou d’une correction applicative, pas de plus de redémarrages.

Task 10: Confirm memory limits and whether they’re sane

cr0x@server:~$ docker inspect -f 'MemLimit={{.HostConfig.Memory}} MemSwap={{.HostConfig.MemorySwap}} PidsLimit={{.HostConfig.PidsLimit}}' api
MemLimit=268435456 MemSwap=268435456 PidsLimit=0

Signification : Limite mémoire de 256 MiB sans marge de swap. Ça peut aller pour un petit service Go ; c’est un piège pour une app Node avec de grands heaps.

Décision : Soit augmentez la limite, soit configurez le heap runtime ; puis passez à on-failure:5 pour qu’une régression ne devienne pas un déni de service contre votre propre hôte.

Task 11: Check disk pressure and log growth (restart loops love filling disks)

cr0x@server:~$ df -h /var/lib/docker
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p3  200G  189G  1.2G  100% /var/lib/docker

Signification : Le chemin de stockage de Docker est plein. Cela peut provoquer des défaillances secondaires étranges (échec de pulls d’images, échecs d’écriture, risque de corruption des métadonnées).

Décision : Arrêtez les conteneurs flappants, faites un prune en sécurité, et limitez les logs. Ne continuez pas à redémarrer sur un disque plein.

Task 12: Identify which containers are producing huge JSON logs

cr0x@server:~$ sudo du -h /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -h | tail -n 5
2.1G /var/lib/docker/containers/8f2c.../8f2c...-json.log
3.8G /var/lib/docker/containers/31ab.../31ab...-json.log
5.4G /var/lib/docker/containers/aa90.../aa90...-json.log
6.0G /var/lib/docker/containers/3c11.../3c11...-json.log
7.2G /var/lib/docker/containers/1d77.../1d77...-json.log

Signification : Certains conteneurs écrivent des logs de plusieurs gigaoctets. Les boucles de plantage multiplient cela rapidement car chaque démarrage logge les mêmes bannières et stack traces.

Décision : Activez la rotation des logs dans la configuration du démon et corrigez l’app bruyante. Entre-temps, libérez de l’espace avec précaution.

Task 13: Verify the container’s last start attempt timestamps

cr0x@server:~$ docker inspect -f 'StartedAt={{.State.StartedAt}} FinishedAt={{.State.FinishedAt}}' api
StartedAt=2026-01-03T09:14:21.115312345Z FinishedAt=2026-01-03T09:14:22.948601234Z

Signification : Il vit ~1.8 secondes. Ce n’est pas une erreur « transitoire » ; c’est une défaillance de démarrage déterministe.

Décision : Arrêtez d’utiliser always. Corrigez la config, démarrez une fois, puis réactivez une politique de redémarrage bornée.

Task 14: Get the exact command/entrypoint Docker is running

cr0x@server:~$ docker inspect -f 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' api
Entrypoint=["/bin/sh","-c"] Cmd=["/app/start.sh"]

Signification : PID 1 est /bin/sh -c, qui exécute un script. C’est une source classique de problèmes de gestion des signaux et de redémarrages causés par des scripts qui se terminent tôt.

Décision : Inspectez le script. Préférez les entrypoints en forme exec et un init si nécessaire.

Task 15: Reproduce the failure interactively (without the restart policy)

cr0x@server:~$ docker run --rm -it --entrypoint /bin/sh myco/api:1.42.0 -lc '/app/start.sh; echo exit=$?'
config: missing required env var: DATABASE_URL
exit=1

Signification : Vous avez reproduit le problème hors du conteneur flappant. C’est un progrès : c’est déterministe.

Décision : Corrigez l’injection d’environnement, pas Docker.

Task 16: Apply a sane policy after fixing config

cr0x@server:~$ docker update --restart=on-failure:5 api
api

Signification : S’il échoue de manière répétée, il s’arrête après cinq échecs.

Décision : Associez cela à de l’alerting sur le compteur de redémarrages afin que « arrêté après cinq » devienne une page et non un downtime silencieux.

Task 17: Validate healthcheck behavior (healthchecks don’t restart containers by themselves)

cr0x@server:~$ docker inspect -f 'Health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' api
Health=unhealthy

Signification : Docker le marque comme unhealthy, mais il ne redémarrera pas automatiquement uniquement parce qu’il est unhealthy (comportement classique de Docker Engine).

Décision : Si vous avez besoin que « unhealthy » déclenche un redémarrage, vous avez besoin d’un contrôleur externe (ou d’un orchestrateur différent), ou implémentez une auto-terminaison applicative sur état de santé irrécupérable (avec précaution).

Task 18: Watch restart events in real time

cr0x@server:~$ docker events --since 5m --filter container=api
2026-01-03T09:10:21.115Z container start 8b4f... (name=api)
2026-01-03T09:10:22.948Z container die   8b4f... (exitCode=1, name=api)
2026-01-03T09:10:24.013Z container start 8b4f... (name=api)

Signification : Vous voyez la cadence de la boucle et les codes de sortie. Utile quand les logs sont bruyants ou ont été tournés.

Décision : Si les redémarrages corrèlent avec des événements hôtes (redémarrage du démon, problème réseau), élargissez la portée. Si la cadence est stable, c’est applicatif/config.

Healthchecks, disponibilité des dépendances et le mythe « le redémarrage corrige tout »

La plupart des boucles de plantage sont des problèmes de disponibilité des dépendances déguisés en « bizarrerie Docker ». Votre appli démarre, tente la base de données une fois, échoue, se termine. Docker redémarre. Répétez jusqu’à la mort thermique de l’univers ou jusqu’à ce que la DB revienne et que vous ayez de la chance.

Faites ceci dans l’application : retry avec backoff, et distinguez fatal vs transitoire

Si la base de données est down pendant 30 secondes pour maintenance, sortir immédiatement n’est pas « propre ». C’est fragile. Implémentez des retries de connexion avec backoff exponentiel et un budget temps maximum. Si l’erreur est fatale (mauvais mot de passe, hôte erroné), sortez une fois et laissez des retries bornés on-failure attraper un problème d’ordre de déploiement transitoire.

Faites ceci dans Docker : utilisez les healthchecks pour l’observabilité et le gating, pas pour une guérison magique

Les healthchecks sont utiles car ils vous donnent un signal lisible par machine : healthy/unhealthy. Dans Docker classique, ils ne redémarrent pas automatiquement le conteneur, mais ils :

  • vous aident à voir « le processus tourne mais le service est mort »,
  • s’intègrent avec les conditions depends_on de Compose (dans les implémentations Compose plus récentes),
  • donnent à votre monitoring externe quelque chose de meilleur que « le conteneur existe ».

Vérifications de dépendance : évitez les scripts « wait-for-it » qui n’en finissent jamais

Il y a deux styles d’échec :

  • Boucles fail-fast : l’app sort immédiatement, Docker redémarre. Bruyant, mais évident.
  • Starts qui restent bloqués : l’entrypoint attend indéfiniment qu’une dépendance soit prête. Docker pense que c’est « Up » mais ce n’est pas servi. Silencieux, mais mortel.

Préférez des attentes bornées avec timeouts explicites. Si la dépendance n’apparaît pas, sortez en non-zéro et laissez on-failure tenter quelques fois, puis s’arrêter.

Docker Compose, Swarm, et pourquoi votre politique peut ne pas être appliquée

Le comportement des politiques de redémarrage dépend de la manière dont vous déployez.

Compose : restart: est facile à définir et facile à oublier

Compose rend trivial de saupoudrer restart: always dans un fichier. Les équipes le font parce que cela « réduit les tickets ». Cela réduit aussi l’apprentissage, jusqu’au jour où cela transforme une simple mauvaise configuration en une tempête de logs à l’échelle de la flotte.

De plus : les différences de version Compose comptent. Certains champs sous deploy: sont ignorés à moins d’utiliser Swarm. Les gens copient/colent des configs et supposent qu’elles sont actives. Elles ne le sont pas.

Swarm services : le comportement de redémarrage est un modèle différent

Swarm a sa propre boucle de réconciliation. Les politiques de redémarrage y font partie de l’ordonnancement des services, pas seulement du comportement du démon local. Si vous êtes sur Swarm, utilisez les conditions et délais de redémarrage au niveau service. Si vous n’êtes pas sur Swarm, ne faites pas semblant en utilisant les clés deploy: dans Compose en pensant qu’elles fonctionneront.

systemd qui encapsule Docker : les boucles de redémarrage doubles existent

Un pattern courant : une unité systemd exécute docker run et systemd a Restart=always. Le conteneur Docker a --restart=always. Quand quelque chose tourne mal, les deux couches « aident ». Maintenant vous avez une boucle de redémarrage qui survit aux redémarrages du démon et résiste à vos tentatives d’arrêter le conteneur parce que systemd le recrée immédiatement.

Si vous devez utiliser systemd, laissez systemd gérer le comportement de redémarrage et mettez la politique du conteneur Docker à no. Ou vice versa. Désignez une seule entité responsable.

Observabilité et garde-fous : limiter votre propre chaos

Une politique de redémarrage sans alerting n’est qu’une défaillance silencieuse avec des étapes en plus. Votre objectif n’est pas « redémarrer pour toujours ». Votre objectif est « récupérer rapidement des fautes transitoires et signaler les fautes persistantes ». Cela signifie des garde-fous.

Garde-fou 1 : alerter sur les redémarrages et sur le taux de redémarrages

Le nombre de redémarrages seul n’est pas suffisant. Un conteneur qui redémarre une fois par jour peut être acceptable. Un conteneur qui redémarre 50 fois en 5 minutes est un incident. Suivez à la fois le total absolu et le taux. Si vous n’avez pas de pipeline métrique, vous pouvez toujours le faire avec un cron + docker inspect et un fichier d’état simple. Ce n’est pas glamour, mais ce n’est pas non plus expliquer à la direction pourquoi votre facture de logging a doublé du jour au lendemain.

Garde-fou 2 : rotation des logs au niveau du démon

Si vous utilisez json-file (beaucoup le font), activez la rotation. Boucles de crash + logs non bornés + petits disques = générateur d’incidents prévisible.

Garde-fou 3 : retries bornés au niveau politique

on-failure:5 n’est pas parfait, mais il crée un état clair : « ceci est en panne de façon persistante ». Cet état est actionnable. « Ça redémarre pour toujours » ne l’est pas.

Garde-fou 4 : limites de ressources conformes à la réalité

Une mémoire non bornée permet à un conteneur de prendre l’hôte entier. Des limites mémoire trop serrées le font redémarrer pour toujours. Les deux sont mauvais. Définissez des limites raisonnables et surveillez l’utilisation réelle. Traitez les limites comme des outils SLO, pas comme une punition.

Trois mini-récits en entreprise (anonymisés, plausibles, techniquement exacts)

Mini-récit 1 : L’incident causé par une mauvaise hypothèse

Dans une entreprise de taille moyenne, une équipe a migré une API legacy des VM vers Docker sur une paire d’hôtes costauds. Ils étaient fiers : moins de pièces à gérer, déploiements plus simples, environnements cohérents. Ils ont ajouté --restart=always « pour que le service reste en marche ». Pas d’orchestrateur, pas de superviseur externe. Juste Docker Engine et de la confiance.

Pendant une rotation de secrets de routine, le mot de passe DB a changé. Le nouveau secret a bien été placé dans le store, mais le job de déploiement qui reconstruisait les conteneurs a échoué à mi-chemin, laissant un hôte avec l’ancienne variable d’environnement et la nouvelle image. L’API démarrait, échouait l’authentification, et sortait avec le code 1. Docker l’a redémarrée. Encore. Et encore.

Les logs étaient pleins d’échecs d’authentification, écrits au format json-file sur un disque partagé. En une heure, le disque contenant /var/lib/docker était presque plein. D’autres conteneurs ont alors commencé à échouer à écrire leur état. Le monitoring a commencé à flancher car son propre conteneur ne pouvait plus écrire sur disque. L’on-call voyait « tout redémarre » et a d’abord suspecté un problème noyau.

L’hypothèse erronée était subtile : ils pensaient qu’une politique de redémarrage était une fonctionnalité de fiabilité. Ce n’est pas le cas. C’est une fonctionnalité de persistance. Une défaillance persistante reste une défaillance ; elle devient juste plus bruyante avec le temps.

La correction était ennuyeuse : passer les services à on-failure:5, rotation des logs, et—surtout—traiter les secrets manquants/invalides comme un incident de déploiement digne d’être paginé avec une procédure de rollback claire.

Mini-récit 2 : L’optimisation qui s’est retournée contre eux

Une autre organisation faisait tourner une flotte d’hôtes Docker pour des outils internes. Quelqu’un a remarqué que les redémarrages de services pendant les déploiements étaient lents parce que les images étaient volumineuses et que les scripts de démarrage faisaient des checks « utiles ». Ils ont optimisé : réduit l’image, supprimé plusieurs checks, et remplacé l’entrypoint par un wrapper shell léger qui configurait des variables puis lançait le service. Les déploys sont devenus plus rapides. Tout le monde a applaudi.

Quelques semaines plus tard, une dépendance en amont a commencé à renvoyer des erreurs TLS intermittentes à cause d’un problème de chaîne de certificats. L’application faisait auparavant des retries pendant une minute avant de sortir ; un des « checks utiles » supprimés comprenait une boucle d’attente réseau. Maintenant elle échouait vite et sortait immédiatement. Comme le service avait restart: always, la flotte a martelé la dépendance défaillante, créant une boucle de rétroaction. L’amont les a rate-limité, ce qui a rendu les échecs plus fréquents, ce qui a augmenté les redémarrages, ce qui a augmenté les triggers de rate limit. Une jolie économie circulaire de douleur.

Ça a empiré : le wrapper shell était PID 1 et ne forwardait pas correctement les signaux. Lors de la mitigation, les opérateurs ont essayé d’arrêter les conteneurs, mais les arrêts étaient incohérents et parfois bloquaient, laissant des ports occupés. Cela a fait échouer les redémarrages suivants différemment (« address already in use »), ajoutant de la confusion et prolongeant l’incident.

L’optimisation n’était pas intrinsèquement mauvaise—des images plus petites sont bonnes—mais les changements ont retiré la logique de résilience de l’application et l’ont remplacée par « Docker va le redémarrer ». Docker a fait exactement cela, et le comportement résultant était techniquement correct et opérationnellement désastreux.

La correction finale : restaurer une logique de retry avec jitter, utiliser des entrypoints en forme exec (et un init si nécessaire), et changer la politique vers des retries bornés. Ils ont aussi implémenté des budgets de panne en amont côté client pour éviter d’écraser les dépendances partielles.

Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise

Une équipe fintech exécutait un service lié aux paiements avec Docker Compose sur une petite cluster d’hôtes. Rien d’exotique. Ce qu’ils avaient, c’était de la discipline : chaque service utilisait on-failure:3 sauf exception écrite, et chaque conteneur avait un healthcheck. Les compteurs de redémarrages étaient exposés en métriques et paginaient sur un seuil de taux.

Un matin, une nouvelle build est arrivée en production avec un bug subtil de parsing de config. Le service sortait avec le code 78 (erreur de config) juste après avoir loggé une ligne claire. Les trois premiers redémarrages ont eu lieu rapidement, puis le conteneur s’est arrêté. L’on-call a reçu une page : « service arrêté après retries ». Les logs étaient courts et lisibles parce que la rotation était définie globalement. L’hôte est resté sain parce que la boucle s’est arrêtée par politique, pas par hasard.

Ils ont rollbacké en quelques minutes. Pas d’effondrement disque, pas de « pourquoi la CPU est saturée », pas d’effets voisins bruyants sur des services non concernés. Le postmortem était presque ennuyeux, ce qui est le compliment le plus élevé que vous puissiez faire à des opérations.

La pratique qui les a sauvés n’était pas outillée exotiquement. C’était deux défauts : retries bornés, et alerting quand le seuil est atteint. Le conteneur ne s’est pas « auto-réparé ». Le système s’est auto-reporté.

Erreurs courantes : symptômes → cause racine → correction

1) Symptom: Container restarts forever with the same log line

Cause racine : restart: always (ou unless-stopped) + échec déterministe au démarrage (env var manquante, fichier absent, flag erroné).

Correction : Passez à on-failure:5, corrigez l’injection de config, et faites en sorte que votre appli affiche une ligne d’erreur claire avant de sortir.

2) Symptom: Restarts show exit code 137

Cause racine : OOM kill ou terminaison forcée. Souvent une limite mémoire trop serrée ou une fuite mémoire.

Correction : Confirmez les logs OOM du noyau, augmentez la limite mémoire ou ajustez le heap runtime, et ajoutez du monitoring mémoire. Les retries bornés évitent le thrash de l’hôte.

3) Symptom: docker stop works, but container comes back

Cause racine : Politique always (ou un autre superviseur le recrée : systemd, cron, agent CI).

Correction : docker update --restart=no et vérifiez les superviseurs externes. Faites en sorte qu’une seule couche soit responsable des redémarrages.

4) Symptom: Container is “Up” but service is dead

Cause racine : Pas de healthcheck et le processus est vivant mais bloqué (deadlock, attente d’une dépendance). La politique de redémarrage n’aide pas car rien ne se termine.

Correction : Ajoutez un healthcheck et de l’alerting externe ; envisagez un watchdog qui redémarre sur état unhealthy persistant (avec précaution), ou corrigez la cause du deadlock.

5) Symptom: After host reboot, containers you “stopped” are running again

Cause racine : restart: always ignore les arrêts manuels après redémarrage du démon ; unless-stopped les respecte.

Correction : Utilisez unless-stopped lorsque les arrêts manuels doivent persister après un redémarrage du démon, ou déplacez le contrôle vers un orchestrateur de plus haut niveau.

6) Symptom: Disk fills up during an incident

Cause racine : Les boucles de crash amplifient la journalisation ; le driver par défaut json-file sans rotation est non borné.

Correction : Configurez la rotation des logs du démon, réduisez le spam de startup, et borne les redémarrages pour que la défaillance ne génère pas des logs infinis.

7) Symptom: “depends_on” didn’t prevent the crash loop

Cause racine : L’ordre de démarrage n’est pas disponibilité. Le conteneur de dépendance peut être « up » mais pas prêt à accepter des connexions.

Correction : Ajoutez des checks de readiness et de la logique de retry ; utilisez des healthchecks et des gates de readiness lorsque supportés.

8) Symptom: Graceful shutdown doesn’t happen; data corruption risk

Cause racine : PID 1 est un wrapper shell qui ne forwarde pas les signaux ; l’app ne gère pas SIGTERM ; timeout d’arrêt trop court.

Correction : Utilisez l’entrypoint en forme exec, ajoutez un init (p. ex. --init), gérez les signaux, et définissez des timeouts d’arrêt raisonnables.

Listes de contrôle / plan pas à pas

Plan pas à pas : comment corriger les politiques de redémarrage sans casser la production

  1. Inventaire des politiques actuelles. Listez les conteneurs et leurs politiques de redémarrage. Signalez tout ce qui a always sans justification claire.
  2. Classifiez les services. Jobs batch, services sans état, services avec état, agents infra. Chacun obtient un défaut.
  3. Choisissez une politique par défaut : généralement on-failure:5 pour les services, no pour les jobs, unless-stopped pour un petit ensemble d’agents « doivent revenir après reboot ».
  4. Ajoutez de l’alerting sur le taux de redémarrages. Si vous ne pouvez pas, au moins créez un rapport quotidien et un seuil de paging pour « arrêté après N retries ».
  5. Activez la rotation des logs. Au niveau du démon. Ne comptez pas sur chaque équipe applicative pour bien le faire.
  6. Revoyez les entrypoints. Les wrappers shell méritent une attention particulière. Ajoutez --init quand utile.
  7. Testez les modes de défaillance. Coupez la DB (figurativement). Retirez un secret. Assurez-vous que le système échoue bruyamment et de façon prévisible.
  8. Déployez les changements progressivement. Un hôte ou un groupe de services à la fois. Surveillez les « dépendances cachées » sur les redémarrages infinis (oui, ça arrive).
  9. Documentez les exceptions. Si un service a vraiment besoin de always, expliquez pourquoi et quel alert le détecte en cas de boucle de plantage.

Checklist : quoi capturer pendant un incident de boucle de plantage

  • Politique de redémarrage et compteur de redémarrages (sortie docker inspect).
  • Dernier code de sortie et flag OOMKilled.
  • Les 100 dernières lignes de log (avant rotation ou prune).
  • Logs noyau de l’hôte pour OOM / erreurs disque / réseau.
  • Utilisation disque pour /var/lib/docker et points de montage des volumes.
  • Toute configuration de superviseur externe (unités systemd, cron, runners CI).

FAQ

1) Should I use restart: always in production?

Rarement. Ne l’utilisez que si vous comprenez les modes de défaillance et avez de l’alerting sur le taux de redémarrage. Par défaut, préférez on-failure:5 pour les services.

2) What’s the practical difference between always and unless-stopped?

unless-stopped respecte un arrêt manuel à travers les redémarrages du démon. always ramène le conteneur après un redémarrage du démon même si vous l’aviez arrêté précédemment.

3) Does Docker restart a container when it becomes unhealthy?

Pas par défaut dans Docker Engine classique. Les healthchecks marquent l’état ; ils ne déclenchent pas automatiquement des redémarrages. Vous avez besoin d’un contrôleur externe pour ce comportement.

4) If my app exits 0 on error, will on-failure restart it?

Non. on-failure redémarre uniquement sur codes de sortie non nuls. Corrigez les codes de sortie de votre appli ; ils font partie du contrat opérationnel.

5) Why can’t I docker exec into a flapping container?

Parce qu’il ne tourne pas assez longtemps. Désactivez les redémarrages (docker update --restart=no), arrêtez-le, puis lancez l’image interactivement avec un shell pour reproduire le problème.

6) What exit codes should I watch for?

Codes courants : 1 échec générique (lire les logs), 137 souvent tué/OOM, 143 SIGTERM, 0 sortie réussie (mais si ça redémarre, vous avez probablement mis always ou redémarré le démon).

7) Can restart policies hide outages?

Oui. Elles peuvent convertir « service down » en « service flappant », qui semble vivant pour un monitoring superficiel. Alertez sur les redémarrages et la santé au niveau service, pas seulement sur l’existence du conteneur.

8) Should I set a maximum retry count?

Oui, pour la plupart des services. Cela crée un état final stable pour des échecs persistants et empêche une consommation infinie de ressources. Associez-le à de l’alerting pour que « arrêté après retries » soit actionnable.

9) What’s the best way to prevent restart loops from filling disks?

Limitez les redémarrages, activez la rotation des logs au niveau du démon, et réduisez le logging bruyant au démarrage. Surveillez explicitement l’utilisation de /var/lib/docker.

10) Isn’t Kubernetes better at this?

Kubernetes vous offre des contrôleurs et primitives plus puissants, mais il peut aussi créer des boucles de crash si vous mal configurez probes et backoff. Le principe reste : les redémarrages ne corrigent pas une défaillance déterministe.

Conclusion : prochaines étapes que vous pouvez déployer cette semaine

Les politiques de redémarrage sont un scalpel, pas du ruban adhésif. Utilisez-les pour récupérer des fautes transitoires, pas pour maintenir un binaire cassé en rotation pendant que vos logs mangent le disque.

Prochaines étapes pratiques :

  1. Auditez tous les conteneurs pour restart: always et justifiez chacun.
  2. Changez le défaut vers on-failure:5 pour les services et no pour les jobs.
  3. Activez la rotation des logs au niveau du démon si vous utilisez json-file.
  4. Ajoutez de l’alerting sur le taux de redémarrages et sur « arrêté après retries ».
  5. Corrigez le PID 1 et la gestion des signaux dans les images qui utilisent des entrypoints shell ; utilisez la forme exec et un init quand approprié.

Alors la prochaine fois que quelque chose échouera à 2h du matin, cela échouera comme un adulte : une fois, clairement, et avec suffisamment de preuves pour le corriger.

← Précédent
DNS WireGuard ne fonctionne pas via le VPN : configuration DNS correcte sur Linux et Windows
Suivant →
La pénurie mondiale de puces : quand de minuscules composants ont paralysé des industries entières

Laisser un commentaire