Problème des zombies dans les processus du conteneur Docker

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 paquet tini et j’enveloppe la commande tini -g -- python3 …. Maintenant, le PID 1 est tini, qui :

    • réapit les enfants orphelins (source principale des zombies — ssh avec ProxyCommand/bastion, tué par ConnectTimeout/SSH_TIMEOUT_SEC, laisse le sous-processus ssh au bastion suspendu sur init) ;
    • propage correctement 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.
  • docker-compose.yml : init: true — couche de sécurité supplémentaire, au cas où quelqu’un redéfinirait localement entrypoint: (dans ce cas, tini de l’image disparaîtrait, mais docker-init prendrait 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.