Docker Compose + systemd : exécuter des stacks de façon fiable après reboot (sans astuces)

Cet article vous a aidé ?

Tout semble aller bien jusqu’au redémarrage. Puis la stack Docker Compose « simple » devient une scène de crime : les conteneurs démarrent dans le mauvais ordre, les volumes ne sont pas encore montés, les réseaux manquent, et votre base de données est opérationnelle alors que votre application est persuadée que l’univers est encore en panne.

Compose est excellent pour décrire une application. systemd est excellent pour s’assurer que votre machine se comporte comme une machine. Mettez-les ensemble correctement et vous arrêtez d’écrire du spaghetti shell au démarrage qui ne fonctionne que quand personne ne regarde.

Ce que nous résolvons (et ce que nous ne résolvons pas)

Il s’agit d’exécuter des stacks définies par Compose de façon fiable après un redémarrage en utilisant systemd. Fiable signifie :

  • La stack démarre au boot sans intervention manuelle.
  • Elle ne démarre pas trop tôt (avant que les disques, le réseau ou Docker ne soient prêts).
  • Elle s’arrête proprement pour éviter de corrompre des services stateful.
  • Vous pouvez diagnostiquer rapidement les échecs avec les outils déjà présents sur la machine.

Il ne s’agit pas de transformer Compose en Kubernetes. Compose ne deviendra pas un ordonnanceur, un gestionnaire de cluster auto-réparant, ni un orchestrateur multi-nœuds. Si vous avez besoin de ça, vous le savez déjà et vous avez probablement des cicatrices.

Et : nous n’allons pas faire « @reboot sleep 30 && docker compose up -d ». Ce n’est pas de l’ingénierie. C’est un rituel.

Faits et histoire qui comptent vraiment

Un peu de contexte aide, car la moitié des « problèmes Compose + systemd » sont en réalité des « j’ai supposé que le comportement ancien s’appliquait encore ». Voici des faits concrets avec des conséquences opérationnelles :

  1. Compose a commencé sous le nom Fig (2013), un outil en Python. Cet héritage explique pourquoi certain·e·s pensent encore « Compose, c’est une chose Python » et le traitent comme un script plutôt qu’un outil de cycle de vie.
  2. Docker a introduit tôt les politiques de redémarrage (restart: always, unless-stopped). Ces politiques sont appliquées par le démon Docker, pas par systemd, ce qui signifie qu’elles se comportent différemment lors de l’ordre d’arrêt.
  3. systemd est devenu dominant sur les grosses distributions au milieu des années 2010. Avant cela, les scripts d’init étaient « best effort ». Si vous copiez des guides de cette époque, vous héritez de leur aléa.
  4. Compose V2 est un plugin CLI Docker (docker compose), pas l’ancien binaire Python docker-compose. Les unités qui codent en dur l’ancien chemin cassent après mise à jour.
  5. Le nom d’unité Docker varie selon la distro (interaction docker.service vs docker.socket). Un ordre correct exige que vous soyez explicite sur ce dont vous dépendez.
  6. depends_on n’a jamais voulu dire « attendre que ce soit prêt ». C’est un ordre de démarrage, pas une disponibilité. Les healthchecks plus une logique d’attente (ou la logique de retry de l’application) restent importants.
  7. journald n’est pas une fonction de Docker ; c’est une décision de logging côté hôte. Si vous n’intégrez pas les logs au chemin de logging de l’hôte, vous déboguerez les problèmes de boot à l’aveugle.
  8. L’arrêt système est un univers différent de celui du démarrage. Si vous ne gérez pas les timeouts d’arrêt, votre base de données peut recevoir un SIGKILL comme si elle avait volé quelque chose.
  9. Rootless Docker est maintenant de l’opérationnel réel, et cela change l’emplacement des sockets, comment les unités sont installées et qui gère le cycle de vie. Des unités écrites pour Docker rootful échouent silencieusement en rootless.

Une citation à garder sur le mur, parce qu’elle décrit 90 % des échecs au démarrage des conteneurs :

Werner Vogels (idée paraphrasée) : « Tout échoue ; concevez pour que l’échec soit attendu et que la récupération soit automatique. »

Principes pour des stacks résistantes au reboot

1) Choisissez un superviseur : systemd ou les policies de restart Docker

Ne les laissez pas se battre. Vous pouvez utiliser les deux, mais vous devez comprendre la conséquence : systemd supervise la commande Compose, tandis que Docker supervise les conteneurs. Si systemd considère que le service est « terminé » et que Docker redémarre les conteneurs de son côté, vous pouvez vous retrouver avec des signaux de santé trompeurs et des redémarrages déroutants.

Ma préférence pour un hôte unique exécutant un petit nombre de stacks :

  • Utiliser systemd pour démarrer la stack au boot et l’arrêter à l’arrêt.
  • Utiliser les politiques de redémarrage Docker dans Compose pour les redémarrages de conteneurs après que la stack soit en fonctionnement (crashes, échecs transitoires).

Cette combinaison garde le cycle de vie de démarrage/arrêt explicite tout en vous donnant de la résilience à l’exécution.

2) Rendez l’ordre réel : disques, réseau, Docker, puis Compose

After=docker.service est nécessaire mais souvent insuffisant. Si votre stack dépend d’un système de fichiers monté (NFS, iSCSI, disque chiffré, import de dataset ZFS), vous devez exprimer cet ordonnancement aussi. Sinon vos conteneurs démarrent avec des répertoires vides et créent un état à un mauvais endroit, ce qui mène aux « pourquoi il utilise SQLite dans /var/lib ? » à 2 h du matin.

3) Ne confondez pas « en cours d’exécution » et « prêt »

systemd peut vous dire qu’un service a démarré. Docker peut vous dire qu’un conteneur s’exécute. Aucun ne peut vous dire que Postgres a fini sa récupération après crash, ou que votre appli a exécuté les migrations, à moins que vous ne le câbliez.

C’est là que les healthchecks, les retries et les timeouts cessent d’être académiques et empêchent les pages inutiles.

4) Gardez les services stateful ennuyeux

Ennuyeux signifie : chemins stables, montages explicites, timeouts d’arrêt explicites et pas de mises à jour surprises au redémarrage. L’approche « cool » est celle qui vous apprend à la dure que les bases de données n’aiment pas les SIGKILL abrupts.

Petite blague #1 : Si votre stack ne démarre que quand vous lui murmurez « juste cette fois », votre serveur a développé une dépendance émotionnelle, pas de l’automatisation.

Concevoir une unité systemd correcte pour Compose

À quoi ressemble « correct »

Un bon fichier d’unité fait quatre choses :

  • S’ordonne après les prérequis (Docker, montages, network-online si nécessaire).
  • Démarre la stack de manière idempotente.
  • Arrête la stack proprement dans un timeout réaliste.
  • Expose les logs et les états d’échec là où vos outils habituels les verront.

Sémantiques d’unité qui comptent en production

Voici les réglages qui décident si vous prenez votre café le matin ou que vous lisez des post-mortems :

  • Type=oneshot + RemainAfterExit=yes : systemd exécute une commande pour monter la stack, puis considère le service « actif » sans garder de processus lié. Cela reflète la réalité : Docker gère les conteneurs, pas le processus Compose.
  • ExecStart/ExecStop : Utilisez docker compose up -d pour démarrer, et docker compose down ou stop pour arrêter. Choisissez selon que vous voulez supprimer réseaux/volumes.
  • TimeoutStartSec/TimeoutStopSec : Donnez suffisamment de temps pour tirer des images (démarrage) et pour que les bases de données flushent (arrêt). Les timeouts par défaut ne sont pas une déclaration morale ; ce ne sont que des valeurs par défaut.
  • WorkingDirectory : Définissez-le. Compose résout les chemins relatifs, les fichiers env et les noms de projet à partir de là. Le laisser implicite est la manière dont vous démarrez accidentellement un projet vide depuis /.
  • EnvironmentFile : Utile pour la configuration par hôte qui n’est pas dans Git. Aussi un moyen propre de garder des secrets hors des fichiers d’unité (mais ce n’est pas un gestionnaire de secrets complet).
  • RequiresMountsFor= : Peu utilisé et excellent. Il fait attendre systemd qu’un chemin soit monté avant de démarrer.
  • After=network-online.target : Uniquement si vous en avez vraiment besoin. Cela peut ralentir le boot si votre configuration réseau est instable. Utilisez-le quand vous dépendez de ressources distantes.

Quelle commande Compose utiliser : up, start, down, stop

Voici le mapping opinionné :

  • Démarrer : docker compose up -d --remove-orphans (supprime les conteneurs oubliés des anciennes configs ; évite les services fantômes).
  • Arrêt (ami des stateful) : docker compose stop (conserve réseaux et conteneurs définis ; redémarrage plus rapide).
  • Arrêt (écran propre) : docker compose down (supprime conteneurs et réseaux ; utilisez quand vous voulez recréer au boot).

Pour la plupart des stacks en production : démarrez avec up -d, arrêtez avec stop. Utilisez down quand vous avez une raison valable (par exemple des patterns d’infrastructure immuable) et que vos volumes sont externes/persistants.

Logging : choisissez journald ou gardez Docker logs, mais soyez délibéré

Pendant les échecs au démarrage, vous voulez un panneau de contrôle unique. Si votre organisation utilise déjà le journal systemd, rendez la sortie de l’unité systemd utile : ajoutez --log-level quand c’est supporté, et assurez-vous que les échecs retournent un code non nul.

Séparément, décidez où stdout/stderr des conteneurs vont. Le driver JSON par défaut de Docker convient jusqu’à ce qu’il ne convienne plus, puis vous découvrez l’utilisation disque de façon amusante. Si vous utilisez journald comme driver de logs Docker, vous obtenez une requête centralisée au niveau hôte avec journalctl. Si vous gardez json-file, configurez la rotation.

Modèles concrets de fichier d’unité (rootful et rootless)

Modèle A : Docker rootful, unité oneshot, ordonnancement propre

C’est le modèle que je déploie le plus souvent sur un hôte unique. Il est simple, prévisible, et ne prétend pas que Compose est un démon.

cr0x@server:~$ sudo tee /etc/systemd/system/compose@.service > /dev/null <<'EOF'
[Unit]
Description=Docker Compose stack (%i)
Requires=docker.service
After=docker.service
Wants=network-online.target
After=network-online.target

# If your stack uses persistent data on a specific mount, uncomment and set:
# RequiresMountsFor=/srv/%i

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/srv/%i
EnvironmentFile=-/srv/%i/.env

# Pull is optional; use it when you can tolerate boot-time pulls.
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose stop
ExecStopPost=/usr/bin/docker compose rm -f

TimeoutStartSec=300
TimeoutStopSec=180

[Install]
WantedBy=multi-user.target
EOF

Remarques importantes :

  • compose@.service est une unité template. Vous pouvez lancer compose@myapp et elle utilisera /srv/myapp.
  • EnvironmentFile=- la rend optionnelle. Si elle manque, l’unité s’exécute quand même.
  • ExecStopPost rm -f supprime les conteneurs arrêtés pour qu’un futur up n’hérite pas d’un état bizarre. Si vous préférez conserver les conteneurs, supprimez cette ligne.

Modèle B : Docker rootful, « down on stop » pour stacks immuables

Si vous traitez l’hôte comme du bétail (ou au moins un animal lassé), vous pouvez vouloir down pour que chaque boot recrée les conteneurs. Assurez-vous que les volumes sont des volumes nommés réels, pas des volumes anonymes par défaut.

cr0x@server:~$ sudo tee /etc/systemd/system/compose-immutable@.service > /dev/null <<'EOF'
[Unit]
Description=Immutable Docker Compose stack (%i)
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/srv/%i
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down --remove-orphans
TimeoutStartSec=300
TimeoutStopSec=240

[Install]
WantedBy=multi-user.target
EOF

Soyez honnête : si votre base de données utilise un bind mount vers /srv/%i/data et que ce chemin n’est pas monté encore, down ne vous sauvera pas. Elle recréera juste la mauvaise chose plus vite.

Modèle C : Docker rootless + unités systemd utilisateur

Rootless Docker est attractif pour les frontières de sécurité. Il est aussi suffisamment différent pour punir le copier-coller. Le socket et le service vivent dans la session utilisateur, et les unités doivent être installées comme services utilisateur.

cr0x@server:~$ mkdir -p ~/.config/systemd/user
cr0x@server:~$ tee ~/.config/systemd/user/compose@.service > /dev/null <<'EOF'
[Unit]
Description=User Docker Compose stack (%i)
After=default.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=%h/stacks/%i
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose stop
TimeoutStartSec=300
TimeoutStopSec=180

[Install]
WantedBy=default.target
EOF

Et vous devez activer le lingering si vous voulez qu’il démarre au boot sans connexion interactive :

cr0x@server:~$ sudo loginctl enable-linger cr0x

Si vous oubliez le lingering, tout fonctionne dans votre terminal et échoue après reboot. Ce n’est pas un mystère ; c’est un mauvais alignement du cycle de vie.

Tâches pratiques : commandes, sorties et décisions

Ce ne sont pas des « bon à savoir ». Ce sont les choses que vous exécutez réellement quand quelqu’un dit « Ça n’est pas revenu après le reboot ». Chaque tâche inclut la commande, une sortie représentative, ce que cela signifie et la décision que vous en tirez.

Task 1: Confirm Compose V2 vs legacy Compose binary

cr0x@server:~$ docker compose version
Docker Compose version v2.24.6

Signification : Compose est disponible comme plugin CLI Docker.

Décision : Écrivez les fichiers d’unité appelant /usr/bin/docker compose. Ne codez pas en dur docker-compose sauf si vous avez vérifié son existence et sa gestion.

Task 2: Verify Docker daemon is up and not degraded

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 Tue 2026-01-03 09:12:41 UTC; 2min 10s ago
TriggeredBy: ● docker.socket
       Docs: man:docker(1)

Signification : Docker tourne et est socket-activé.

Décision : Si Docker est inactive ou failed, corrigez Docker d’abord. Les unités Compose qui dépendent de Docker échoueront en cascade.

Task 3: Check whether your Compose unit is enabled and which target wants it

cr0x@server:~$ systemctl is-enabled compose@myapp.service
enabled

Signification : Elle doit démarrer au boot quand sa target est atteinte.

Décision : Si elle est disabled, activez-la. Si elle est static, vous avez écrit une unité sans section [Install].

Task 4: See if systemd thinks the Compose service is active

cr0x@server:~$ systemctl status compose@myapp.service --no-pager
● compose@myapp.service - Docker Compose stack (myapp)
     Loaded: loaded (/etc/systemd/system/compose@.service; enabled; preset: enabled)
     Active: active (exited) since Tue 2026-01-03 09:13:04 UTC; 1min 40s ago
    Process: 2214 ExecStart=/usr/bin/docker compose up -d --remove-orphans (code=exited, status=0/SUCCESS)

Signification : L’action de démarrage Compose a réussi ; les conteneurs devraient être gérés par Docker.

Décision : Si elle est failed, allez voir les logs du journal pour l’unité et corrigez l’erreur immédiate (env manquant, fichier compose manquant, permissions).

Task 5: Read unit logs for the last boot only

cr0x@server:~$ journalctl -u compose@myapp.service -b --no-pager
Jan 03 09:13:03 server systemd[1]: Starting Docker Compose stack (myapp)...
Jan 03 09:13:04 server docker[2214]: [+] Running 3/3
Jan 03 09:13:04 server docker[2214]:  ✔ Network myapp_default  Created
Jan 03 09:13:04 server docker[2214]:  ✔ Container myapp-db-1  Started
Jan 03 09:13:04 server docker[2214]:  ✔ Container myapp-api-1 Started
Jan 03 09:13:04 server systemd[1]: Started Docker Compose stack (myapp).

Signification : C’est la source de vérité pour savoir si le démarrage au boot a eu lieu.

Décision : Si les logs montrent des fichiers manquants ou des erreurs de montage, corrigez les dépendances/l’ordre. Si les logs montrent un succès mais que l’appli est down, le problème est à l’intérieur des conteneurs ou de leurs dépendances (readiness, réseau, récupération DB).

Task 6: Confirm containers exist and match the Compose project

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES         STATUS                    PORTS
myapp-api-1   Up 1 minute (healthy)     0.0.0.0:8080->8080/tcp
myapp-db-1    Up 1 minute               5432/tcp

Signification : Les conteneurs sont présents ; l’état de santé est visible pour les services qui ont des healthchecks.

Décision : Si les conteneurs manquent, Compose ne s’est pas exécuté ou s’est exécuté dans le mauvais répertoire. Si les conteneurs redémarrent, inspectez les logs et la pression sur les ressources.

Task 7: Inspect why a container is restarting (last exit code, OOM, health)

cr0x@server:~$ docker inspect myapp-api-1 --format '{{.State.Status}} {{.State.ExitCode}} OOM={{.State.OOMKilled}} Health={{if .State.Health}}{{.State.Health.Status}}{{end}}'
running 0 OOM=false Health=unhealthy

Signification : Il tourne mais est unhealthy ; probablement une dépendance non prête ou une mauvaise configuration de l’appli.

Décision : Ne redémarrez pas aveuglément. Vérifiez les logs et la connectivité des dépendances. Si OOM=true, ajustez les limites mémoire ou la capacité de l’hôte.

Task 8: Validate compose file resolution and environment at the exact WorkingDirectory

cr0x@server:~$ cd /srv/myapp
cr0x@server:~$ /usr/bin/docker compose config
name: myapp
services:
  api:
    image: registry.local/myapp-api:1.9.2
    environment:
      DB_HOST: db
  db:
    image: postgres:16

Signification : Compose peut parser et rendre la config finale.

Décision : Si cela échoue, systemd échouera aussi. Corrigez la syntaxe, les variables env manquantes, les fichiers manquants ou le mauvais WorkingDirectory.

Task 9: Confirm your persistent data path is mounted before Compose runs

cr0x@server:~$ findmnt /srv/myapp
TARGET     SOURCE              FSTYPE OPTIONS
/srv/myapp tank/appdata/myapp  zfs    rw,xattr,noacl

Signification : Le répertoire de données de votre stack est un vrai montage (ici, dataset ZFS).

Décision : Si findmnt ne renvoie rien, vous écrivez dans le système de fichiers racine. Ajoutez RequiresMountsFor=/srv/myapp à l’unité et corrigez l’ordre de montage/import.

Task 10: Check boot ordering and dependency graph

cr0x@server:~$ systemctl list-dependencies compose@myapp.service --no-pager
compose@myapp.service
● ├─docker.service
● ├─network-online.target
● └─multi-user.target

Signification : systemd ne démarrera pas l’unité Compose tant que ces unités ne sont pas satisfaites.

Décision : Si votre montage n’est pas listé, vous dépendez du timing. Ajoutez RequiresMountsFor ou des unités de montage explicites.

Task 11: Time where boot is slow: critical chain

cr0x@server:~$ systemd-analyze critical-chain compose@myapp.service
compose@myapp.service +1.820s
└─docker.service +1.301s
  └─network-online.target +1.005s
    └─NetworkManager-wait-online.service +1.002s

Signification : Votre stack Compose n’est pas la partie lente ; le système attend network-online.

Décision : Si la stack n’a pas vraiment besoin de network-online, supprimez-le. Sinon corrigez le fournisseur network-online (par ex. la configuration du service wait-online).

Task 12: Confirm shutdown behavior and stop timeout

cr0x@server:~$ systemctl show compose@myapp.service -p TimeoutStopUSec -p ExecStop
TimeoutStopUSec=3min
ExecStop={ path=/usr/bin/docker ; argv[]=/usr/bin/docker compose stop ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }

Signification : systemd permettra 3 minutes pour un arrêt propre.

Décision : Si vous exécutez des bases de données, 10 secondes est de la comédie. Augmentez TimeoutStopSec, et ajustez le stop_grace_period dans Compose si nécessaire.

Task 13: Validate Docker logging driver and rotation (to prevent boot-time disk full surprises)

cr0x@server:~$ docker info --format '{{.LoggingDriver}}'
json-file

Signification : Les conteneurs loggent par défaut dans des fichiers JSON.

Décision : Assurez-vous que /etc/docker/daemon.json configure la rotation, ou envisagez journald si cela correspond à votre modèle d’exploitation. Un disque plein au démarrage peut empêcher Docker de démarrer du tout.

Task 14: Detect whether you’re dealing with rootless Docker when you thought you weren’t

cr0x@server:~$ docker context show
default
cr0x@server:~$ systemctl --user status docker --no-pager
Unit docker.service could not be found.

Signification : Probablement Docker rootful (ou service utilisateur non installé). Les setups rootless ont généralement un service docker au niveau utilisateur et un chemin de socket différent.

Décision : Alignez l’emplacement de l’unité (system vs user) avec la façon dont Docker tourne. Des hypothèses décalées causent le « fonctionne en shell, échoue au boot ».

Procédure de diagnostic rapide

Quand une stack ne revient pas après un reboot, vous n’avez pas de temps pour des danses interprétatives. Voici le chemin le plus rapide vers le goulot d’étranglement.

First: prove whether systemd ran the start command

  1. Vérifiez l’état de l’unité : systemctl status compose@X.service
  2. Vérifiez les logs du dernier boot : journalctl -u compose@X.service -b

Si l’unité n’a jamais tourné, vous êtes dans la terre de l’activation/[Install]/targets. Si elle a tourné et échoué, corrigez l’erreur reportée avant de toucher aux conteneurs.

Second: prove whether Docker was ready and stayed alive

  1. systemctl status docker
  2. journalctl -u docker -b pour erreurs du driver de stockage, disque plein, problèmes de permission, crashs du démon.

Si Docker est unhealthy, Compose est hors sujet. Corrigez d’abord le stockage, le disque ou la configuration de Docker.

Third: prove whether prerequisites were available (mounts, network, secrets)

  1. findmnt /srv/X ou chemins pertinents.
  2. systemctl list-dependencies compose@X.service pour voir si des montages sont requis.
  3. Vérifiez la présence et les permissions des fichiers env, des fichiers compose et des répertoires de bind mount.

Fourth: if everything “started,” chase readiness and application-level failures

  1. docker ps et état de santé.
  2. docker logs --tail=200 pour les conteneurs en échec.
  3. docker inspect pour code de sortie, OOMKilled, échecs de health.

Fifth: isolate resource bottlenecks

  1. Pression CPU/mémoire : docker stats --no-stream, free -h.
  2. Pression disque : df -h, docker system df.
  3. Montages lents : systemd-analyze critical-chain et logs des unités de montage.

Petite blague #2 : « Ça a marché après que j’ai rebooté encore » n’est pas une correction ; c’est une machine à sous avec un meilleur branding.

Erreurs courantes : symptôme → cause → correction

Cette section est celle que vous souhaiterez avoir lue avant l’appel d’incident.

1) Symptom: Unit says “active (exited)” but containers are missing

  • Cause racine : Mauvais WorkingDirectory, donc Compose a tourné contre un répertoire vide et a créé un nouveau projet ailleurs (ou n’a rien fait).
  • Correction : Définissez WorkingDirectory=/srv/myapp (ou équivalent). Lancez docker compose config depuis ce répertoire pour valider. Envisagez --project-name si nécessaire, mais habituellement le nom basé sur le répertoire suffit.

2) Symptom: Containers start, then app fails to connect to database right after boot

  • Cause racine : Vous avez compté sur depends_on pour la readiness. Le conteneur DB tourne mais n’accepte pas encore les connexions (récupération après crash, fsck, lecture WAL, déverrouillage du chiffrement).
  • Correction : Ajoutez un healthcheck au conteneur DB et implémentez retry/backoff dans l’application. Si vous devez absolument bloquer le démarrage, utilisez une petite étape d’init/attente dans l’entrypoint du conteneur applicatif, pas dans systemd.

3) Symptom: After reboot, data directory is empty or “reset”

  • Cause racine : Le montage n’était pas prêt ; le conteneur a créé un nouveau répertoire sur le filesystem racine et initialisé des données fraîches. Plus tard le montage apparaît, masquant les mauvaises données.
  • Correction : Ajoutez RequiresMountsFor=/srv/myapp (ou le chemin exact de données) à l’unité. Pour ZFS, assurez-vous que l’import se fait tôt. Pour disques chiffrés, assurez-vous que l’étape de déverrouillage précède Docker/Compose.

4) Symptom: Compose unit fails with “Cannot connect to the Docker daemon” at boot

  • Cause racine : Votre unité s’exécute avant que le socket/démon Docker ne soit prêt, ou Docker est lent à cause de vérifications de stockage.
  • Correction : Assurez-vous de Requires=docker.service et After=docker.service. Si Docker est socket-activé, dépendez quand même du service. Envisagez d’augmenter TimeoutStartSec pour votre unité Compose si le démarrage de Docker est lent.

5) Symptom: Shutdown hangs for a long time, then containers get killed

  • Cause racine : Timeouts d’arrêt trop courts, ou votre unité ne stoppe pas la stack du tout, laissant Docker s’en charger tard dans la séquence d’arrêt.
  • Correction : Ajoutez ExecStop=docker compose stop et un TimeoutStopSec réaliste. Pour les services stateful, définissez stop_grace_period dans Compose et évitez down si vous voulez des redémarrages plus rapides sans recréation.

6) Symptom: Unit works manually, fails at boot with missing env vars

  • Cause racine : Vous avez utilisé des variables de profil de shell ou compté sur un environnement interactif. Les services systemd ne chargent pas vos fichiers RC de shell.
  • Correction : Utilisez EnvironmentFile= dans l’unité, ou intégrez les variables dans Compose env_file. Validez avec systemctl show et docker compose config.

7) Symptom: Boot is slow because network-online waits forever

  • Cause racine : Vous avez ajouté network-online.target par habitude, mais votre stack n’en a pas besoin, ou votre service d’attente réseau est mal configuré.
  • Correction : Retirez la dépendance network-online sauf si vous avez besoin de ressources réseau distantes au démarrage. Si vous en avez besoin, corrigez le service wait-online ou la configuration de votre gestionnaire réseau.

8) Symptom: Rootless Compose stack doesn’t start after reboot

  • Cause racine : Le service utilisateur ne démarre pas au boot car le lingering n’est pas activé, ou l’unité a été installée comme unité système alors que Docker est rootless.
  • Correction : Installez-la comme unité utilisateur et exécutez loginctl enable-linger USER. Confirmez avec systemctl --user is-enabled et vérifiez les logs du journal utilisateur.

Trois mini-histoires du monde corporate

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

Une entreprise de taille moyenne faisait tourner une API orientée client sur une VM unique musclée. Rien de sophistiqué : une stack Compose avec un conteneur API, Redis et Postgres. Ils utilisaient restart: always sur tout et appelaient ça « haute disponibilité ». Ça a fonctionné pendant des mois, ce qui rend confiant sans raison.

Pendant une mise à jour du noyau, la VM a redémarré. Docker est revenu. Les conteneurs sont revenus. L’API est revenue, techniquement. Mais elle a retourné des 500 pendant une dizaine de minutes, puis s’est rétablie d’elle-même. L’astreinte l’a vue, a haussé les épaules et est passée à autre chose. « Transitoire. »

Une semaine plus tard, un autre reboot est survenu—cette fois pendant une période plus chargée. La logique de migration de l’API s’est exécutée au démarrage, a supposé que la base de données était immédiatement joignable, et a échoué lourdement. La politique de redémarrage du conteneur l’a redémarrée fidèlement, encore et encore, martelant les logs et maintenant le service en panne. Postgres allait bien ; il rejouait simplement le WAL sur un disque plus lent après un arrêt non propre.

L’hypothèse erronée était subtile : ils croyaient que « conteneur en cours » impliquait « dépendance prête ». Ils croyaient aussi que la politique de redémarrage Docker suffisait pour gérer l’ordonnancement au démarrage. Les deux croyances sont courantes. Les deux sont fausses d’une manière qui n’apparaît qu’au démarrage ou à la récupération.

La correction a été ennuyeuse et efficace : systemd a démarré la stack après les montages et Docker, des healthchecks ont été ajoutés à Postgres et Redis, et l’API a été modifiée pour retenter la connexion DB avec backoff avant d’exécuter les migrations. Le reboot suivant s’est déroulé sans incident, ce qui est la meilleure des histoires.

Mini-histoire 2 : L’optimisation qui a mal tourné

Une autre société voulait des temps de boot plus rapides. Ils avaient une douzaine de projets Compose sur un même hôte (outils internes, tableaux de bord, petits services). Quelqu’un a remarqué que l’attente sur network-online.target ajoutait des secondes. Ils l’ont donc retiré de toutes les unités et ont déclaré la victoire.

Le boot est devenu plus rapide. Puis les pannes étranges ont commencé : un conteneur de métriques ne pouvait pas résoudre DNS lors de son démarrage initial et a mis en cache l’échec. Un service de vérification de licence a essayé de joindre un endpoint externe une seule fois, a échoué, et s’est désactivé jusqu’à un redémarrage manuel. Un reverse proxy a démarré sans pouvoir résoudre les upstreams et a servi des pages d’erreur par défaut.

En postmortem, l’équipe a découvert quelque chose d’inconfortable : ces services n’avaient jamais été robustes face à l’absence transitoire de réseau. L’attente network-online masquait la fragilité des applications. La supprimer a rendu la fragilité visible, et l’« optimisation » a transformé la rapidité de boot en instabilité des services.

La solution finale a été nuancée. Ils ont réintroduit network-online uniquement pour les stacks qui en avaient vraiment besoin, et ils ont corrigé les pires coupables pour qu’ils retentent les opérations réseau correctement. Le temps de boot s’est légèrement amélioré, la fiabilité a beaucoup progressé, et l’équipe a appris que gagner des secondes sur le boot coûte cher quand ça tourne mal.

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

Une entreprise du domaine financier faisait tourner une petite stack Compose pour un pipeline de reporting : scheduler, worker et une base de données. L’hôte utilisait un stockage chiffré et un dataset dédié pour les données DB. Leur unité systemd avait une ligne en plus par rapport aux autres : RequiresMountsFor=/srv/reporting.

Un matin, après un reboot non surveillé, l’étape de déverrouillage du chiffrement a pris plus de temps que d’habitude parce qu’un service dépendant a retenté la récupération de clés. Le système était « up » mais le dataset n’était pas monté quand Docker a démarré. Sans ordonnancement, le conteneur DB aurait initialisé une base fraîche dans un répertoire non monté du filesystem racine.

Mais l’unité n’a pas démarré. systemd a attendu. Docker était prêt, le réseau était OK, mais le montage n’était pas là, donc la service Compose est resté en file d’attente. Une fois le dataset monté, la stack a démarré normalement. Pas de répertoires en double, pas de base de données fraîche fantôme, pas de drame de restauration.

Quand ils ont audité les logs de boot plus tard, l’unique artefact était un démarrage retardé. Ce retard était une fonctionnalité : il a empêché une divergence silencieuse des données. Le meilleur travail de fiabilité ressemble souvent à « rien ne s’est passé », ce qui est l’esthétique acceptable en production.

Listes de vérification / plan étape par étape

Plan étape par étape : migrer proprement une stack Compose vers systemd

  1. Normalisez l’emplacement de la stack : placez chaque projet sous un répertoire stable (exemple : /srv/myapp). Décidez si vous voulez des unités template (compose@.service) ou des unités par stack.
  2. Rendez l’état explicite : assurez-vous que les bases de données utilisent des volumes nommés ou des bind mounts vers un chemin que vous contrôlez. Évitez les volumes anonymes pour ce qui compte.
  3. Validez la config Compose de manière déterministe : exécutez docker compose config depuis le répertoire prévu. Corrigez les warnings et les variables manquantes.
  4. Écrivez l’unité :
    • WorkingDirectory= défini sur le répertoire de la stack.
    • Requires=docker.service, After=docker.service.
    • RequiresMountsFor= pour tout chemin de données persistantes sur des montages dédiés.
    • ExecStart=docker compose up -d --remove-orphans
    • ExecStop=docker compose stop (ou down si vous le souhaitez vraiment).
    • Des timeouts réalistes.
  5. Rechargez systemd : systemctl daemon-reload.
  6. Activez l’unité : systemctl enable --now compose@myapp.service.
  7. Testez le comportement au reboot sans redémarrer immédiatement : arrêtez Docker, démarrez Docker, assurez-vous que l’unité se comporte. Ensuite faites un vrai reboot en fenêtre de maintenance et surveillez les logs du journal.
  8. Instrumentez la readiness : ajoutez des healthchecks pour les dépendances critiques ; assurez-vous que les applications retentent en cas d’échec. Ne faites pas faire des retries applicatifs par systemd.
  9. Définissez la politique de logs : choisissez le driver de logs et la rotation. Vérifiez que l’utilisation disque ne va pas exploser.
  10. Documentez le contrat d’exploitation : ce que signifient « start », « stop » et « upgrade » pour cette stack, incluant les attentes de sécurité des données.

Checklist opérationnelle : avant de qualifier de « fiable »

  • L’unité démarre avec succès sur un cold boot (pas seulement un warm reboot).
  • L’unité attend les montages requis ; aucun répertoire de données créé sur le mauvais filesystem.
  • Les conteneurs de bases de données ont des périodes de grâce d’arrêt réalistes ; l’arrêt ne leur envoie pas systématiquement un SIGKILL.
  • Les logs des échecs de démarrage sont visibles dans journalctl -u et les logs des conteneurs sont conservés/rotatés.
  • Retirer un service de Compose ne le laisse pas tourner pour toujours (utilisez --remove-orphans).
  • Scénarios de désastre testés : Docker ne démarre pas, disque plein, montage manquant, réseau absent. Le système échoue de façon bruyante, pas silencieuse.

FAQ

1) Should I use Docker restart policies if I have systemd units?

Oui, mais pour des couches différentes. systemd doit gérer « démarrer la stack au boot » et « l’arrêter à l’arrêt ». Les politiques de redémarrage Docker gèrent les crashes de conteneurs à l’exécution. Gardez-les alignés et évitez les illusions de double-supervision.

2) Should the systemd service be Type=simple and run “docker compose up” without -d?

Généralement non. Lancer sans -d lie la santé du service à un processus client long. Ça peut fonctionner, mais c’est fragile : logs, comportement TTY et crashs du client peuvent confondre systemd. Type=oneshot + up -d est plus propre pour la plupart des hôtes.

3) Is “depends_on” enough to control startup order?

Ça contrôle l’ordre de démarrage, pas la readiness. Si vous avez besoin de readiness, utilisez des healthchecks et de la logique de retry. Traitez la readiness comme une responsabilité applicative, pas comme de la magie d’orchestration.

4) Should ExecStop use “docker compose down” or “stop”?

stop est plus sûr pour les stacks stateful et plus rapide pour redémarrer. down convient quand vous voulez recréer les conteneurs et que vous êtes sûr que les données persistantes sont sur des volumes nommés/bind mounts qui ne vont pas disparaître.

5) How do I ensure my stack doesn’t start before my ZFS datasets or encrypted volumes?

Utilisez RequiresMountsFor= pointant vers le chemin dont votre stack a besoin (par exemple, /srv/myapp ou /var/lib/myapp). Cela force systemd à attendre le montage. Assurez-vous aussi que le montage lui-même est configuré pour apparaître avant le multi-user.

6) Why does the unit say “active (exited)”—isn’t that wrong?

C’est correct pour une unité oneshot avec RemainAfterExit=yes. Le rôle de l’unité est d’exécuter la commande de démarrage. Docker maintient ensuite les conteneurs. Si vous voulez que systemd suive un processus, adoptez un autre pattern.

7) How do I run multiple stacks cleanly?

Utilisez une unité template (compose@.service) et une convention de répertoire comme /srv/<stack>. Chaque stack est activée indépendamment : systemctl enable --now compose@stackname. Cela évite une unité monolithique qui tente de tout faire et échoue de façon ambiguë.

8) What’s the clean way to handle secrets?

Au minimum, gardez les secrets hors des fichiers d’unité et hors de Git. Utilisez EnvironmentFile= avec des permissions appropriées, ou les secrets de Compose basés sur des fichiers. Si vous avez déjà un gestionnaire de secrets, intégrez-le à l’exécution des conteneurs. Ne faites pas comme si systemd était un coffre à secrets.

9) What about Podman Compose or quadlets?

Podman a une intégration systemd de première classe via quadlets, et c’est un choix valide. Mais cet article traite spécifiquement de Docker Compose. Si vous êtes sur Podman, appuyez-vous sur ses patterns natifs plutôt que d’émuler Docker.

10) How do I keep boot from pulling images and stalling forever?

Ne tirez pas d’images au boot sauf si vous le voulez vraiment. Pré-pull les images via un job de mise à jour séparé ou un workflow de maintenance. Si vous devez tirer au boot, augmentez TimeoutStartSec et acceptez le compromis.

Conclusion : les prochaines étapes appropriées

Si vous voulez que les stacks Compose se comportent après un reboot, cessez de traiter le boot comme une superstition et commencez à le traiter comme de la gestion de dépendances. systemd excelle pour l’ordonnancement et le cycle de vie. Compose excelle pour déclarer la stack. Ensemble, ils sont ennuyeux dans le meilleur sens.

Prochaines étapes qui rapportent immédiatement :

  • Écrivez une unité template avec WorkingDirectory, After/Requires et RequiresMountsFor.
  • Activez-la par stack et vérifiez avec journalctl -b après un reboot contrôlé.
  • Ajoutez des healthchecks et des retries là où « en cours » n’est pas égal à « prêt ».
  • Définissez des timeouts d’arrêt qui respectent vos services stateful.

Votre futur vous recevra encore parfois des pages. Mais ce sera pour de vrais problèmes, pas parce que vos conteneurs ont battu vos disques à la ligne de départ.

← Précédent
Gestion des images pour sites rapides : ratio d’aspect, styles de lazy loading, placeholders floutés
Suivant →
Vaporware en production : comment les produits annoncés brisent les systèmes réels

Laisser un commentaire