Cycle de vie et diagnostic d'un conteneur
Pourquoi
Un conteneur en production finit toujours par mal se comporter. Il redémarre en boucle, il consomme trop, il ne répond plus, ou il met dix secondes à s'arrêter à chaque déploiement, ce qui rallonge les mises à jour et coupe des requêtes en plein vol. Dans tous ces cas, on a besoin des mêmes réflexes : lire ce que le conteneur a dit, regarder ce qu'il fait, interroger sa configuration réelle, entrer dedans si nécessaire, et l'arrêter proprement.
Cette leçon construit ces réflexes sur un cas réaliste : la base PostgreSQL de notre application, et un petit service Python qui doit s'arrêter sans perdre de travail. Elle s'attarde sur un sujet que la plupart des introductions ignorent et qui cause pourtant des incidents dans presque toutes les équipes : le PID 1 et les signaux.
Les concepts
Où vont les sorties d'un conteneur
Un conteneur n'écrit pas ses journaux dans un fichier de /var/log : son processus principal écrit sur sa sortie standard et sa sortie d'erreur, et Docker les capture par l'intermédiaire du shim (leçon 3). Le pilote de journalisation (leçon 4) les range ensuite sur le disque de l'hôte. docker logs relit ce que le pilote a stocké. Conséquence : une application qui écrit ses journaux dans un fichier à l'intérieur du conteneur les rend invisibles pour docker logs et les perd à la suppression du conteneur. Les images officielles redirigent d'ailleurs leurs fichiers de journaux vers les sorties standard (l'image nginx fait de /var/log/nginx/access.log un lien vers /dev/stdout).
Arrêter, c'est envoyer un signal
docker stop ne tue pas un conteneur. Il envoie au processus principal le signal SIGTERM, qui signifie « demande d'arrêt : terminez proprement », attend un délai de grâce (10 secondes par défaut), puis, si le processus est toujours là, envoie SIGKILL, qui le tue sans discussion. docker kill envoie directement SIGKILL (ou le signal de votre choix avec -s).
Un arrêt propre laisse à l'application le temps de finir les requêtes en cours, de vider ses tampons, de fermer ses connexions à la base. Un SIGKILL coupe tout : transactions interrompues, fichiers à moitié écrits, clients qui reçoivent une erreur.
Le PID 1 n'est pas un processus comme les autres
Sous Linux, quand un processus reçoit un signal pour lequel il n'a installé aucun gestionnaire, le noyau applique l'action par défaut : pour SIGTERM, c'est la terminaison. C'est pourquoi un kill tout simple arrête la plupart des programmes, même ceux qui n'ont rien prévu.
Il y a une exception : le processus init, le PID 1 d'un namespace de PID. Pour le protéger d'une terminaison accidentelle, le noyau ignore les signaux qu'il ne gère pas explicitement. Or, dans un conteneur, le PID 1 est votre application. Si elle n'installe pas de gestionnaire pour SIGTERM, le signal envoyé par docker stop est purement et simplement ignoré, Docker attend dix secondes, puis frappe avec SIGKILL.
Le PID 1 a une seconde responsabilité : il adopte les orphelins. Quand un processus meurt avant ses enfants, ceux-ci sont rattachés au PID 1, qui doit récupérer leur statut de fin avec wait(). Sinon, ils restent à l'état de zombies dans la table des processus. Une application qui lance des sous-processus (un script qui appelle des commandes, un serveur qui lance des workers) et n'a pas été écrite pour être un init peut ainsi accumuler des zombies.
Deux solutions existent : écrire l'application pour qu'elle gère SIGTERM (indispensable pour un arrêt propre de toute façon), et/ou intercaler un init minimal comme tini, que Docker fournit avec l'option --init (leçon 3).
Les vérifications de santé
Qu'un conteneur soit « Up » signifie seulement que son processus existe. Il peut être bloqué, saturé, ou encore en train de démarrer. Une vérification de santé (healthcheck) est une commande que Docker exécute périodiquement dans le conteneur ; son code de retour (0 = sain, 1 = malade) fait passer le conteneur par trois états : starting, healthy, unhealthy. Compose (leçon 11) s'en sert pour ordonner les démarrages ; les orchestrateurs ont leurs propres sondes, sur le même principe.
En pratique
Une base PostgreSQL à observer
Lançons la base de notre fil conducteur, avec une vérification de santé :
$ docker run -d --name pg -e POSTGRES_PASSWORD=essai \
--health-cmd 'pg_isready -U postgres' \
--health-interval 2s --health-timeout 2s --health-retries 3 \
--health-start-period 30s \
postgres:18
Les options de santé se lisent ainsi : exécuter pg_isready -U postgres toutes les 2 secondes ; considérer qu'un essai a échoué s'il dure plus de 2 secondes ; déclarer le conteneur unhealthy après 3 échecs consécutifs ; mais pendant les 30 premières secondes (start period), ne pas compter les échecs, le temps que la base s'initialise.
$ docker ps --filter name=pg --format '{{.Names}} {{.Status}}'
pg Up 3 seconds (health: starting)
$ docker ps --filter name=pg --format '{{.Names}} {{.Status}}'
pg Up 7 seconds (healthy)
docker logs : lire ce que le conteneur a dit
$ docker logs --tail 3 pg
2026-10-01 10:01:40.182 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2026-10-01 10:01:40.188 UTC [74] LOG: database system was shut down at 2026-10-01 10:01:40 UTC
2026-10-01 10:01:40.194 UTC [1] LOG: database system is ready to accept connections
Les options utiles au quotidien :
--tail N: seulement les N dernières lignes. Sans elle,docker logsrelit tout depuis la création du conteneur, ce qui peut prendre longtemps.-f(--follow) : suivre en continu, commetail -f.--sinceet--until: borner dans le temps, en durée relative (10m,2h) ou en date absolue (2026-10-01T09:00:00).-t: préfixer chaque ligne par l'horodatage de réception par Docker, utile quand l'application n'horodate pas elle-même :
$ docker logs -t --tail 2 pg
2026-10-01T10:01:40.189160492Z 2026-10-01 10:01:40.188 UTC [74] LOG: database system was shut down at 2026-10-01 10:01:40 UTC
2026-10-01T10:01:40.194442307Z 2026-10-01 10:01:40.194 UTC [1] LOG: database system is ready to accept connections
La commande diagnostique d'un incident en cours est souvent docker logs -f --since 5m <nom>.
docker exec : agir dans un conteneur en cours
docker exec lance un nouveau processus dans les namespaces d'un conteneur existant (avec l'appel système setns() vu à la leçon 2) :
$ docker exec pg psql -U postgres -c "CREATE TABLE notes(id serial, texte text);"
CREATE TABLE
Ce processus n'est pas le PID 1 et ne fait pas vivre le conteneur : s'il se termine, le conteneur continue ; si le conteneur s'arrête, il est tué. Il s'exécute sous l'utilisateur configuré pour le conteneur (le USER de l'image, ou celui passé avec -u à docker run), que l'on change avec -u :
$ docker exec pg id
uid=0(root) gid=0(root) groups=0(root)
$ docker exec -u postgres pg id
uid=999(postgres) gid=999(postgres) groups=999(postgres),101(ssl-cert)
Les options -i et -t ont le même sens qu'avec docker run : docker exec -it pg bash ouvre un shell interactif, et la même erreur que pour docker run apparaît si l'on demande un terminal sans en avoir un.
Tip
Un shell dans un conteneur sert à comprendre, jamais à réparer. Tout ce que vous modifiez à la main disparaîtra à la prochaine recréation du conteneur. Une fois la cause trouvée, corrigez l'image ou la configuration.
docker top et docker stats : ce qu'il fait, ce qu'il consomme
Beaucoup d'images minimales n'ont pas ps. Plutôt que de l'installer, demandez à Docker, qui lit la table des processus de l'hôte :
$ docker top pg -o pid,user,cmd
PID USER CMD
1699381 dnsmasq postgres
1699666 dnsmasq postgres: io worker 0
1699667 dnsmasq postgres: io worker 1
1699668 dnsmasq postgres: io worker 2
1699669 dnsmasq postgres: checkpointer
...
Arrêtons-nous sur la colonne USER. PostgreSQL tourne en tant qu'utilisateur postgres, UID 999, dans le conteneur. docker top affiche les processus vus de l'hôte, et sur l'hôte de test, l'UID 999 correspond à l'utilisateur système dnsmasq. Ce n'est pas un bogue : c'est la démonstration directe que, sans namespace user, un UID dans le conteneur est le même UID sur l'hôte (leçon 2). Les noms ne sont que des étiquettes lues dans le /etc/passwd de chaque côté.
docker stats affiche la consommation en direct ; --no-stream en prend un instantané :
$ docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.PIDs}}' pg
NAME CPU % MEM USAGE / LIMIT PIDS
pg 6.70% 43.03MiB / 30.71GiB 9
La limite affichée (30,71 Gio) est la mémoire totale de l'hôte : aucune limite n'a été fixée avec --memory. Ces chiffres viennent des fichiers du cgroup du conteneur, vus à la leçon 2.
docker inspect : la vérité sur la configuration
docker inspect renvoie, en JSON, toute la configuration et l'état d'un conteneur : plusieurs centaines de lignes. On en extrait une valeur avec un gabarit Go (--format, ou -f) :
$ docker inspect pg --format '{{.State.Status}} pid={{.State.Pid}} redémarrages={{.RestartCount}} santé={{.State.Health.Status}}'
running pid=1699381 redémarrages=0 santé=healthy
Ou avec jq, plus confortable pour explorer :
$ docker inspect pg | jq '.[0].Config.Env'
[
"POSTGRES_PASSWORD=essai",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/postgresql/18/bin",
"GOSU_VERSION=1.19",
"LANG=en_US.utf8",
"PG_MAJOR=18",
"PG_VERSION=18.6-1.pgdg13+2",
"PGDATA=/var/lib/postgresql/18/docker"
]
Remarquez la première ligne : le mot de passe passé avec -e est lisible en clair par quiconque peut interroger le démon. Nous y reviendrons dans la partie Sécurité.
Attention aux gabarits trouvés en ligne. D'innombrables tutoriels donnent ce gabarit pour obtenir l'adresse IP d'un conteneur :
$ docker inspect pg --format '{{.NetworkSettings.IPAddress}}'
template parsing error: template: :1:18: executing "" at <.NetworkSettings.IPAddress>: map has no entry for key "IPAddress"
Ce champ, déprécié depuis 2015, a été retiré de l'API (version 1.52) avec Docker 29. Un conteneur peut être branché sur plusieurs réseaux, chacun avec son adresse : il faut parcourir la table Networks :
$ docker inspect pg --format '{{range $nom, $r := .NetworkSettings.Networks}}{{$nom}} {{$r.IPAddress}}{{end}}'
bridge 172.17.0.3
docker diff et docker cp : le système de fichiers
docker diff liste ce qui a changé dans la couche inscriptible du conteneur par rapport à son image (A ajouté, C modifié, D supprimé) :
$ docker diff pg
C /run
C /run/postgresql
A /run/postgresql/.s.PGSQL.5432.lock
A /run/postgresql/.s.PGSQL.5432
Quatre lignes seulement, alors que PostgreSQL vient d'initialiser une base complète et que nous avons créé une table ! Où sont passées les données ? L'image déclare un volume :
$ docker image inspect postgres:18 --format '{{json .Config.Volumes}}'
{"/var/lib/postgresql":{}}
$ docker inspect pg --format '{{range .Mounts}}{{.Type}} {{.Name}} -> {{.Destination}}{{end}}'
volume cea24e7cdfb0a87834990e72f029f14e79353d0addec11f255ee6976adbea6be -> /var/lib/postgresql
Docker a créé automatiquement un volume anonyme et l'a monté sur /var/lib/postgresql. Les données n'y sont donc pas dans la couche du conteneur. Toute la leçon 9 est consacrée à ce mécanisme et à ses pièges.
docker cp copie des fichiers entre l'hôte et un conteneur, même arrêté, sans avoir besoin de cat ni de tar dans l'image :
$ docker cp pg:/var/lib/postgresql/18/docker/postgresql.conf ./postgresql.conf
$ ls -l postgresql.conf
-rw------- 1 vous vous 32657 oct. 1 12:01 postgresql.conf
C'est l'outil idéal pour récupérer un fichier de configuration généré ou un rapport de plantage.
L'arrêt propre, et ses pièges
Commençons par le cas le plus simple : sleep en PID 1.
$ docker run -d --name dormeur alpine:3.22 sleep 300
$ /usr/bin/time -f "docker stop : %e s" docker stop dormeur
dormeur
docker stop : 10.23 s
$ docker inspect dormeur --format 'ExitCode={{.State.ExitCode}}'
ExitCode=137
Dix secondes, et un code 137 (SIGKILL). sleep n'installe aucun gestionnaire de signaux ; en PID 1, SIGTERM est ignoré. On peut le vérifier dans /proc : le masque SigCgt (signaux interceptés) est vide.
$ docker run -d --name dormeur3 alpine:3.22 sleep 300
$ docker exec dormeur3 grep -E 'SigCgt|SigIgn' /proc/1/status
SigIgn: 0000000000000000
SigCgt: 0000000000000000
Avec --init, tini devient le PID 1, relaie SIGTERM à sleep, qui n'est plus PID 1 et subit donc l'action par défaut :
$ docker run -d --init --name dormeur2 alpine:3.22 sleep 300
$ /usr/bin/time -f "docker stop : %e s" docker stop dormeur2
dormeur2
docker stop : 0.17 s
$ docker inspect dormeur2 --format 'ExitCode={{.State.ExitCode}}'
ExitCode=143
Instantané, avec le code 143 (128 + 15, SIGTERM).
Passons à une vraie application, qui doit finir son travail avant de s'arrêter. Ce petit service Python installe un gestionnaire pour SIGTERM :
import signal, sys, time
def arreter(signum, frame):
print(f"signal {signal.Signals(signum).name} reçu : je termine les requêtes en cours", flush=True)
time.sleep(1) # simule la fin du travail en cours
print("arrêt propre", flush=True)
sys.exit(0)
signal.signal(signal.SIGTERM, arreter)
print("service démarré", flush=True)
while True:
time.sleep(1)Placé dans signaux/service.py, lançons-le en montant le répertoire (l'option -v est expliquée à la leçon 9) :
$ docker run -d --name svc -v $PWD/signaux:/app:ro python:3.14-slim python /app/service.py
$ /usr/bin/time -f "docker stop : %e s" docker stop svc
svc
docker stop : 1.19 s
$ docker logs svc
service démarré
signal SIGTERM reçu : je termine les requêtes en cours
arrêt propre
$ docker inspect svc --format 'ExitCode={{.State.ExitCode}}'
ExitCode=0
C'est le comportement idéal : le signal est reçu, le travail se termine, le code de sortie est 0.
Maintenant, le piège. On veut afficher un message avant de lancer le service, et l'on écrit naturellement :
$ docker run -d --name svc2 -v $PWD/signaux:/app:ro python:3.14-slim \
sh -c 'echo démarrage; python /app/service.py'
$ docker exec svc2 ps
OCI runtime exec failed: exec failed: unable to start container process: exec: "ps": executable file not found in $PATH
$ docker top svc2 -o pid,ppid,cmd
PID PPID CMD
1697870 1697846 sh -c echo démarrage; python /app/service.py
1697886 1697870 python /app/service.py
$ /usr/bin/time -f "docker stop : %e s" docker stop svc2
svc2
docker stop : 10.25 s
$ docker logs svc2
démarrage
service démarré
$ docker inspect svc2 --format 'ExitCode={{.State.ExitCode}}'
ExitCode=137
(Au passage, ps n'existe pas dans l'image python:3.14-slim : docker top le remplace.) Le PID 1 est désormais sh, et Python n'est que son enfant. docker stop envoie SIGTERM à sh, qui, en PID 1 et sans gestionnaire, l'ignore ; Python ne reçoit jamais rien ; dix secondes plus tard, SIGKILL tue tout le monde. Le message « arrêt propre » n'apparaît jamais : le service a été tué au milieu de son travail.
La correction tient en un mot, exec, qui remplace le shell par Python :
$ docker run -d --name svc3 -v $PWD/signaux:/app:ro python:3.14-slim \
sh -c 'echo démarrage; exec python /app/service.py'
$ docker top svc3 -o pid,ppid,cmd
PID PPID CMD
1698603 1698580 python /app/service.py
$ /usr/bin/time -f "docker stop : %e s" docker stop svc3
svc3
docker stop : 1.18 s
$ docker logs svc3
démarrage
service démarré
signal SIGTERM reçu : je termine les requêtes en cours
arrêt propre
Note
Le comportement dépend du shell. Le sh de l'image Python est dash (Debian), qui ne remplace jamais le shell par la dernière commande. Le sh de BusyBox (Alpine) le fait automatiquement quand la commande est unique : avec alpine:3.22 sh -c 'sleep 300', docker top ne montre que sleep. Ne comptez pas sur ce comportement : écrivez exec.
Le délai de grâce se règle avec --stop-timeout au lancement, ou -t sur docker stop. Mais l'allonger ne corrige rien si le signal n'arrive jamais : il ne fait que retarder le SIGKILL.
Déboguer un conteneur sans shell
Les images les plus sûres (distroless, ou construites FROM scratch) ne contiennent que l'application : pas de shell, pas de ls, pas de ps. Comment les déboguer ? Prenons l'image pause de Kubernetes, qui ne contient qu'un binaire :
$ docker run -d --name sans-shell registry.k8s.io/pause:3.10
$ docker exec sans-shell sh
OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH
La solution découle de la leçon 2 : lancer un second conteneur, avec des outils, qui partage les namespaces du premier :
$ docker run --rm --pid container:sans-shell --network container:sans-shell busybox:1.37 \
sh -c 'ps; ip addr | grep inet'
PID USER TIME COMMAND
1 65535 0:00 /pause
7 root 0:00 sh -c ps; ip addr | grep inet
13 root 0:00 ps
inet 127.0.0.1/8 scope host lo
inet6 ::1/128 scope host
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
--pid container:sans-shell place le conteneur de débogage dans le namespace de PID de la cible : on voit /pause en PID 1, avec son UID 65535. --network container:sans-shell partage sa pile réseau : on voit son adresse IP, on pourrait tester ses ports avec nc ou wget. Pour explorer aussi son système de fichiers, on passe par /proc/1/root, ce qui demande la capability SYS_PTRACE puisque la cible tourne sous un autre UID :
$ docker run --rm --pid container:sans-shell --cap-add SYS_PTRACE busybox:1.37 \
sh -c 'ls /proc/1/root; ls -l /proc/1/root/pause'
dev
etc
pause
proc
sys
-rwxr-xr-x 1 root root 735760 May 23 2024 /proc/1/root/pause
C'est exactement le principe des conteneurs éphémères de Kubernetes (kubectl debug). Docker Desktop propose une commande docker debug qui automatise ce montage ; la technique manuelle ci-dessus fonctionne partout, avec Docker Engine seul.
Sous le capot
docker logs ne parle pas au conteneur : il lit les fichiers du pilote de journalisation sur l'hôte. C'est pourquoi il fonctionne sur un conteneur arrêté. Avec un pilote qui envoie les journaux vers un service distant, docker logs fonctionne quand même grâce au « double envoi » (dual logging), actif par défaut, qui garde une copie locale en cache ; il échoue seulement si ce cache est désactivé.
docker exec demande à containerd de créer un processus supplémentaire dans la tâche existante ; runc rejoint les namespaces du conteneur avec setns(), s'inscrit dans son cgroup, applique les mêmes restrictions (capabilities, seccomp, AppArmor), puis exécute la commande. Le processus créé a le même environnement que le conteneur, plus les variables passées avec -e.
Le signal d'arrêt n'est pas forcément SIGTERM : une image peut déclarer un StopSignal (nginx utilise SIGQUIT pour un arrêt gracieux), et docker run --stop-signal le remplace. docker stop le lit dans la configuration du conteneur.
Les vérifications de santé sont exécutées par dockerd, avec le même mécanisme que docker exec. Elles consomment donc des ressources dans le conteneur, à chaque intervalle : une vérification qui lance un interpréteur lourd toutes les deux secondes se voit dans docker stats. Docker conserve les cinq derniers résultats dans .State.Health.Log, sortie comprise, ce qui en fait un outil de diagnostic :
$ docker inspect pg --format '{{json .State.Health}}' | python3 -m json.tool
{
"Status": "healthy",
"FailingStreak": 0,
"Log": [
{
"Start": "2026-10-01T12:01:43.760817576+02:00",
"End": "2026-10-01T12:01:43.878849293+02:00",
"ExitCode": 0,
"Output": "/var/run/postgresql:5432 - accepting connections\n"
},
...
Pièges courants
Le conteneur met dix secondes à s'arrêter. C'est presque toujours le PID 1 qui ignore SIGTERM : un shell intercalé (sh -c sans exec), un script d'entrée sans exec "$@" final, ou une application qui n'installe pas de gestionnaire. Diagnostic : docker top pour voir qui est PID 1, puis grep SigCgt /proc/1/status dans le conteneur. Code de sortie 137 après un docker stop = le délai de grâce a expiré.
La vérification de santé ment. pg_isready sans -h interroge PostgreSQL par son socket Unix local. Or, au premier démarrage, le script d'entrée de l'image lance un serveur temporaire qui n'écoute que sur ce socket, le temps d'exécuter les scripts d'initialisation :
$ docker exec pg grep -n "listen_addresses" /usr/local/bin/docker-entrypoint.sh
297: set -- "$@" -c listen_addresses='' -p "${PGPORT:-5432}"
Une vérification sur le socket peut donc répondre « sain » pendant l'initialisation, alors que les autres conteneurs ne peuvent pas encore se connecter par le réseau. pg_isready -U postgres -h 127.0.0.1 teste ce que les clients utiliseront vraiment. Règle générale : une vérification de santé doit tester le chemin réel des clients.
Un gabarit inspect qui ne marche plus. {{.NetworkSettings.IPAddress}} échoue depuis Docker 29. Adaptez vos scripts avec {{range .NetworkSettings.Networks}}. Mieux : ne dépendez pas de l'adresse IP d'un conteneur, qui change à chaque recréation ; utilisez les noms DNS des réseaux définis par l'utilisateur (leçon 10).
Des journaux vides. L'application écrit dans un fichier au lieu de la sortie standard, ou Python met sa sortie en tampon quand elle ne va pas vers un terminal (d'où le flush=True du service ci-dessus ; la variable PYTHONUNBUFFERED=1 a le même effet pour tout le programme).
docker exec sur un conteneur qui redémarre en boucle. Impossible : il ne reste pas assez longtemps. Utilisez docker logs pour lire la dernière tentative, docker events pour la séquence, puis docker run --rm -it --entrypoint sh <image> pour explorer l'image elle-même.
Sécurité
docker inspectrévèle les secrets passés en variables d'environnement, etdocker execdonne un shell root dans n'importe quel conteneur. Ce sont deux raisons supplémentaires de réserver l'accès au démon aux seuls administrateurs.docker execen root par défaut. Dans un conteneur qui tourne avec un utilisateur non privilégié, Le serveur PostgreSQL tourne sous l'utilisateurpostgres, maisdocker exec pg ...s'exécute en root, comme nous l'avons vu :docker executilise l'utilisateur configuré pour le conteneur, et l'image PostgreSQL n'en configure pas (c'est son script d'entrée qui abandonne ses privilèges). Précisez-upour garder le moindre privilège, y compris en diagnostic.- Le conteneur de débogage partage tout.
--pid container:et--network container:donnent au conteneur de débogage la vue et l'accès réseau de la cible, etSYS_PTRACEle droit de lire sa mémoire. Supprimez-le dès le diagnostic fini (--rm), et n'utilisez que des images d'outillage de confiance. - Un arrêt brutal est un risque d'intégrité. Un
SIGKILLsur une base de données ou un traitement de fichiers peut laisser des données incohérentes. Garantir un arrêt propre est aussi une mesure de protection des données.
En production
- Tous les orchestrateurs arrêtent par
SIGTERMpuisSIGKILL. Kubernetes envoieSIGTERM, attendterminationGracePeriodSeconds(30 s par défaut), puisSIGKILL. Les mises à jour progressives (rolling updates) arrêtent des conteneurs en permanence : une application qui ignoreSIGTERMcoupe des requêtes à chaque déploiement. Testez l'arrêt propre dès le développement, avecdocker stopet un chronomètre. - Écrivez les scripts d'entrée correctement. Ils doivent finir par
exec "$@"ouexec mon-programme. C'est un point de contrôle de revue de code systématique pour tout Dockerfile. - Centralisez les journaux.
docker logsest un outil de diagnostic local ; en production, un agent (Fluent Bit, Vector, Grafana Alloy) collecte les journaux de tous les conteneurs vers une plateforme centrale. Le cours Journaux centralisés avec Loki en traite. - Les vérifications de santé de Docker ne sont pas reprises par Kubernetes, qui ignore l'instruction
HEALTHCHECKdes images et utilise ses propres sondes (livenessProbe,readinessProbe,startupProbe). La logique, en revanche, est la même : tester le chemin réel, laisser un délai au démarrage, ne pas surcharger.
Exercices
1. Enquête sur un arrêt lent. Lancez docker run -d --name lent nginx:1.29, puis mesurez docker stop lent. Est-ce rapide ? Lisez le StopSignal de l'image avec docker image inspect. Recommencez avec docker run -d --name lent2 --stop-signal SIGKILL nginx:1.29 : quel est le risque de ce réglage ?
Solution
L'arrêt de nginx est rapide : son image déclare StopSignal à SIGQUIT, que nginx traite comme un arrêt gracieux (il termine les requêtes en cours). Avec --stop-signal SIGKILL, l'arrêt reste rapide mais devient brutal : les connexions en cours sont coupées net. On ne change le signal d'arrêt que pour adopter celui que l'application documente comme gracieux.
2. Trouver le PID 1. Pour chacune de ces commandes, prévoyez qui sera le PID 1 et si docker stop sera rapide, puis vérifiez avec docker top et un chronomètre : (a) docker run -d python:3.14-slim python -m http.server ; (b) docker run -d python:3.14-slim sh -c 'python -m http.server' ; (c) docker run -d --init python:3.14-slim sh -c 'python -m http.server'.
Solution
(a) Python est PID 1. Le module http.server n'installe pas de gestionnaire pour SIGTERM, donc en PID 1 le signal est ignoré : arrêt en 10 secondes, code 137. (b) dash est PID 1, Python son enfant : même résultat, 10 secondes, code 137. (c) tini est PID 1, sh son enfant, Python le petit-enfant. docker stop est instantané (code 143), mais regardez pourquoi : tini relaie SIGTERM à sh, qui n'est plus PID 1 et meurt aussitôt ; tini voit son enfant terminé et se termine à son tour ; or, quand le PID 1 d'un namespace disparaît, le noyau tue tous les processus restants avec SIGKILL. Python est donc tué brutalement, sans avoir reçu SIGTERM. L'arrêt est rapide mais pas propre. Leçon : --init aide, mais la vraie correction est une application qui gère SIGTERM, lancée sans shell intermédiaire (ou avec exec).
3. Une santé honnête. Relancez la base de la leçon avec une vérification de santé qui teste la connexion par le réseau, et vérifiez dans .State.Health.Log la sortie de la commande.
Solution
$ docker run -d --name pg2 -e POSTGRES_PASSWORD=essai \
--health-cmd 'pg_isready -U postgres -h 127.0.0.1' \
--health-interval 2s --health-start-period 30s postgres:18
$ docker inspect pg2 --format '{{json .State.Health.Log}}' | jq '.[-1].Output'
La sortie mentionne désormais 127.0.0.1:5432 - accepting connections au lieu du socket Unix. Nettoyez avec docker rm -f pg2.
4. Déboguer sans shell (niveau 200). Lancez docker run -d --name web nginx:1.29. Sans utiliser docker exec, depuis un conteneur busybox:1.37, vérifiez que nginx répond sur son port 80 et listez les processus de web.
Solution
$ docker run --rm --network container:web busybox:1.37 wget -qO- http://127.0.0.1/ | head -4
$ docker run --rm --pid container:web busybox:1.37 ps
Le premier partage la pile réseau de web : 127.0.0.1 y désigne nginx. Le second partage son namespace de PID : on voit le processus maître de nginx en PID 1 et ses workers. Nettoyez avec docker rm -f web.
Récapitulatif
- Observer :
docker logs(avec--tail,-f,--since),docker top,docker stats,docker events. - Interroger :
docker inspectavec un gabarit Go oujq; attention aux champs retirés comme.NetworkSettings.IPAddress. - Agir :
docker exec(avec-upour le bon utilisateur),docker cp,docker diff. Comprendre, jamais réparer à la main. - Arrêter :
docker stopenvoieSIGTERM, puisSIGKILLaprès le délai de grâce. Le PID 1 ignore les signaux qu'il ne gère pas : gérezSIGTERMdans l'application, utilisezexecdans les scripts,--initen complément. - Surveiller la santé avec une commande qui teste le chemin réel des clients.
- Déboguer une image sans shell en partageant ses namespaces avec un conteneur d'outillage.
Pour aller plus loin
- Le README de tini, qui explique en détail les responsabilités d'un init et le problème des zombies.
- La section Termination of Pods de la documentation Kubernetes, pour voir la même mécanique à l'échelle d'un cluster.
- La documentation de formatage de Docker, avec les fonctions disponibles dans les gabarits (
json,join,range,index). - Leçon suivante : les images, leurs couches, leurs étiquettes et leurs empreintes.
Sources
- Docker, docker container stop
- Docker, docker container logs
- Docker, Format command and log output (gabarits Go)
- Docker, Dockerfile reference : HEALTHCHECK
- Docker Engine API, historique des versions (champs retirés en 1.52)
- signal(7) et kill(2), Linux manual pages (cas particulier du processus init)
- tini, README : pourquoi un init dans un conteneur
- Kubernetes, Pod lifecycle : termination of Pods
- Dash, page de manuel (exécution des commandes)