Docker n'a jamais redémarré ma base : le prix de restart: no
Un conteneur est mort la nuit et rien ne l'a relancé, parce que chaque service était en restart: no. Les healthchecks que j'avais ne pouvaient pas aider, et la raison surprend la plupart des gens.
Quelque chose est mort au milieu de la nuit. Je l’ai appris le matin, par un utilisateur, pas par une machine.
Le conteneur s’était arrêté. Il est resté arrêté. Docker l’avait regardé mourir sans rien faire, précisément parce que je lui avais dit de ne rien faire. La ligne responsable faisait deux caractères, répétée sur chaque service de mon fichier compose.
Voici la partie 5 d’une série sur le durcissement d’un SaaS construit en solo en production. Le setup et la liste complète des points faibles sont dans l’article pilier. Celui-ci parle de la différence entre un conteneur qui plante et un système qui se rétablit.
La ligne, sur les sept services
restart: no
Chaque service l’avait : nginx, l’API, Postgres, Redis, MinIO, le build du dashboard, l’utilitaire workspace. Sept sur sept.
restart: no est la valeur par défaut de Docker, et elle veut dire exactement ce qu’elle dit. Si le processus s’arrête, pour quelque raison que ce soit, le conteneur reste mort. Pas de superviseur, pas de retry, pas de backoff. Ça veut aussi dire quelque chose de pire auquel je n’avais pas réfléchi : si le VPS redémarre, rien ne remonte. La machine boote, le démon Docker démarre, et chaque conteneur reste là, arrêté, à attendre que je me connecte en SSH pour lancer make up. Un redémarrage d’hôte après une mise à jour du noyau aurait mis tout le produit hors ligne jusqu’à ce que je le remarque à la main.
Sur un laptop, restart: no est le défaut sain : vous ne voulez pas que l’expérience d’hier se relance toute seule au redémarrage. Sur un serveur de production, c’est la décision de n’avoir aucune reprise automatique.
Ce qui surprend : j’avais des healthchecks
Voilà ce qui rend l’histoire intéressante. Ce n’est pas que je n’avais aucune supervision au niveau conteneur. Deux services avaient de vrais healthchecks, bien écrits :
db:
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U ${DB_USER} -d ${DB_NAME}']
interval: 10s
timeout: 5s
retries: 5
api:
healthcheck:
test: ['CMD', 'bun', '-e', "fetch('http://localhost:8000/health')..."]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Et l’API attend même la base correctement, ce qui est le bon pattern :
api:
depends_on:
db:
condition: service_healthy # pas seulement "démarré"
Alors pourquoi rien de tout ça n’a aidé ? À cause d’un fait qui piège presque tous ceux qui viennent de Kubernetes :
En Docker Compose pur, un healthcheck qui échoue ne redémarre rien. Il bascule le statut du conteneur en unhealthy et s’arrête là. Pas de sémantique de liveness probe, pas de remplacement automatique. Contrairement à une liveness probe Kubernetes ou à Docker Swarm, Compose n’a aucun acteur intégré qui réagit à unhealthy.
Donc un healthcheck sans politique de redémarrage produit un conteneur magnifiquement, précisément étiqueté comme cassé, que personne ne redémarre et que personne ne regarde.
Deux mécanismes, deux modes de panne différents
Vu comme ça, la conception devient évidente. Ce ne sont pas des alternatives, ils couvrent des pannes différentes :
| Panne | Ce qui l’attrape |
|---|---|
| Le processus plante ou s’arrête | Politique de redémarrage (restart:) |
| Le processus est vivant mais coincé, bloqué, ou ne sert plus | Healthcheck plus quelque chose qui agit dessus |
Il me manquait la seconde moitié de la deuxième ligne, et toute la première ligne. C’est comme ça qu’un crash à 3h du matin devient une panne matinale.
Choisir une politique de redémarrage
Il y a quatre valeurs, et une seule est généralement la bonne pour un service de production :
| Politique | Redémarre au crash | Survit au reboot de l’hôte | Respecte un arrêt manuel |
|---|---|---|---|
no | non | non | sans objet |
on-failure[:N] | seulement si code de sortie non nul | non | oui |
always | oui | oui | non, il revient après un arrêt délibéré |
unless-stopped | oui | oui | oui |
unless-stopped est celle que vous voulez pour les services de longue durée. Elle redémarre le conteneur au crash, le fait revenir après un reboot de l’hôte, et vous laisse quand même arrêter délibérément un service avec docker compose stop sans que Docker vous contredise au prochain démarrage du démon. always a l’air similaire mais ressuscitera un conteneur que vous aviez arrêté exprès, ce qui rend fou pendant un incident.
db:
restart: unless-stopped
api:
restart: unless-stopped
nginx:
restart: unless-stopped
Notez que ça vaut pour les services de longue durée seulement. Les conteneurs one-shot (une étape de build, un lanceur de migrations, l’utilitaire workspace qui ne fait qu’un install) doivent rester en no, parce que relancer un job déjà terminé n’est pas de la reprise, c’est une boucle.
Faire en sorte que “unhealthy” serve à quelque chose
Les politiques de redémarrage gèrent les crashs. Elles ne font rien pour le cas plus vicieux : le processus tourne encore, donc Docker est content, mais il ne sert plus rien. C’est ce que les healthchecks détectent, et en Compose il faut ajouter l’acteur vous-même. Deux options honnêtes :
- Un petit sidecar autoheal : un conteneur avec accès au socket Docker qui surveille le statut
unhealthyet redémarre ces conteneurs. C’est quelques lignes dans le fichier compose et ça comble le trou sans introduire d’orchestrateur. - Une supervision externe qui vous alerte, pour qu’un humain décide. Plus lent, mais ça attrape aussi les pannes qu’un redémarrage ne corrige pas (un disque plein, une dépendance morte, un certificat expiré).
La seconde n’est pas optionnelle même si vous ajoutez la première, parce que “redémarre-le” n’est pas toujours la bonne réponse, et un service qui boucle en redémarrages toute la nuit est une panne à lui tout seul. Obtenir cette visibilité est le sujet du prochain article de cette série.
Tant que vous y êtes, comblez aussi les petits trous. Dans mon fichier, seuls la base et l’API avaient des healthchecks : Redis, MinIO et nginx n’en avaient aucun. Et nginx utilisait la forme courte de depends_on, qui attend seulement que les autres conteneurs aient démarré, pas qu’ils soient sains :
# avant : nginx démarre dès que l'api existe, prête ou non
nginx:
depends_on: [storage, api]
# après : nginx attend qu'ils puissent vraiment servir
nginx:
depends_on:
api:
condition: service_healthy
storage:
condition: service_healthy
Ce que ça ne corrige pas
Les politiques de redémarrage vous gardent debout à travers les crashs et les reboots. Elles ne donnent pas des déploiements sans coupure. Mes déploiements lancent toujours docker compose up -d --force-recreate, qui arrête le conteneur et en démarre un nouveau, donc chaque déploiement est quelques secondes où les requêtes tombent dans le vide. C’est un autre problème avec un autre correctif (construire l’image ailleurs, puis permuter), et il appartient au pipeline de déploiement, aux côtés du jour où mon build a rempli le disque.
Être honnête sur la frontière compte : unless-stopped transforme “à terre jusqu’à mon réveil” en “à terre quelques secondes”. C’est un gain énorme, et ce n’est pas de la haute disponibilité. Sur un seul VPS, il y a toujours exactement un exemplaire de chaque chose.
À retenir
restart: no est le bon défaut sur un laptop et la décision de n’avoir aucune reprise sur un serveur. Si votre compose de production l’a, un crash à 3h du matin est une panne jusqu’à ce que vous le remarquiez, et un reboot d’hôte est une panne totale jusqu’à ce que vous vous connectiez en SSH.
Mettez unless-stopped sur chaque service de longue durée. Donnez un healthcheck à chaque service, pas seulement à la base. Utilisez condition: service_healthy dans depends_on pour que les choses démarrent dans un ordre qui fonctionne vraiment. Et rappelez-vous le point qui piège : en Compose, un healthcheck ne fait qu’étiqueter le problème. Quelque chose d’autre doit agir dessus, que ce soit un sidecar autoheal ou une alerte qui vous atteint.
La vue d’ensemble de la série suit le reste.