Problème classique : Python est lancé en tant que PID 1 dans le conteneur, et lors des timeouts SSH (surtout avec ProxyCommand/bastion), les enfants sont suspendus au PID 1, qui ne les surveille pas — d’où les zombies. Je corrige cela en deux couches : j’installe tini comme init dans l’image elle-même (protège tout docker run), et j’active init: true dans le fichier docker-compose.yml comme signal explicite.
RUN apt-get update \\
&& apt-get install -y --no-install-recommends openssh-client \\
&& apt-get install -y --no-install-recommends openssh-client tini \\
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["python3", "/app/server.py", "--host", "0.0.0.0", "--config", "/config/config.yaml"]
# tini comme PID 1 : récupère les enfants orphelins (principal source de zombies — ssh avec ProxyCommand/bastion, tué par ConnectTimeout/SSH_TIMEOUT_SEC, laisse le sous-processus ssh au bastion suspendu sur init);
# et propage correctement les signaux (SIGTERM/SIGINT) à toute la groupe de processus (flag -g), pour que docker stop ne prenne pas 10 secondes et ne laisse pas de processus ssh en attente.
ENTRYPOINT ["/usr/bin/tini", "-g", "--", "python3", "/app/server.py", "--host", "0.0.0.0", "--config", "/config/config.yaml"]
restart: always
# En cas où l'ENTRYPOINT dans l'image est redéfini localement : docker-init (tini) prendra le relais comme PID 1 et gérera les zombies issus de ssh/ProxyCommand en cas de timeout.
init: true
ports:
Ce qui a changé et pourquoi :
-
Dockerfile: j’installe le paquettiniet j’enveloppe la commandetini -g -- python3 …. Maintenant, le PID 1 esttini, qui :- réapit les enfants orphelins (source principale des zombies —
sshavecProxyCommand/bastion, tué parConnectTimeout/SSH_TIMEOUT_SEC, laisse le sous-processussshau bastion suspendu sur init) ; - propage correctement
SIGTERM/SIGINTà toute la groupe de processus (flag-g), pour quedocker stopne prenne pas 10 secondes et ne laisse pas de processus ssh en attente.
- réapit les enfants orphelins (source principale des zombies —
-
docker-compose.yml:init: true— couche de sécurité supplémentaire, au cas où quelqu’un redéfinirait localemententrypoint:(dans ce cas,tinide l’image disparaîtrait, maisdocker-initprendrait toujours la place de PID 1).
Comment appliquer :
docker compose build --no-cache ansible-status
docker compose up -d
Vérifier qu’aucun zombie ne se forme plus (à l’intérieur du conteneur, il ne doit pas y avoir de lignes avec Z dans le champ STAT) :
docker compose exec ansible-status sh -c 'ps -e -o pid,ppid,stat,comm | awk "NR==1 || /Z/"'
Si on veut encore plus propre — on peut ajouter dans ssh_check_for_target les options -o ControlMaster=no -o ControlPath=none et start_new_session=True dans subprocess.run, pour que ssh ne crée pas de multiplexeurs persistants, et que, en cas de TimeoutExpired, on puisse tuer toute la process group, et non seulement le processus ssh principal. Mais c’est une amélioration du comportement, et la cause fondamentale des zombies — l’absence d’un init — est déjà résolue.