Problema clásico: Python se ejecuta como PID 1 dentro del contenedor, y en caso de timeout de SSH (especialmente con ProxyCommand/bastion), los hijos quedan colgados en el PID 1, que ya no los espera — de ahí los zombies. Lo soluciono con dos capas: instalo tini como init dentro de la propia imagen (protege cualquier docker run), y activo init: true en el compose como señal explícita.
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 como PID 1: recoge a los hijos huérfanos (principal origen de zombies — ssh con ProxyCommand/bastion, matado por ConnectTimeout/SSH_TIMEOUT_SEC, deja al ssh hijo colgado en init);
# y transmite correctamente los señales SIGTERM/SIGINT a todo el grupo de procesos (flag -g), para que docker stop no tarde 10 segundos ni deje huellas de ssh.
ENTRYPOINT ["/usr/bin/tini", "-g", "--", "python3", "/app/server.py", "--host", "0.0.0.0", "--config", "/config/config.yaml"]
restart: always
# En caso de que el ENTRYPOINT en la imagen haya sido reemplazado: docker-init (tini)
# capturará los zombies de ssh/ProxyCommand en caso de timeout.
init: true
ports:
¿Qué cambió y por qué?
-
Dockerfile: Instalo el paquetetiniy envuelvo el comando contini -g -- python3 …. Ahora el PID 1 estini, que:- reaper a los hijos huérfanos (principal fuente de zombies —
sshconProxyCommand/bastion, matado porConnectTimeout/SSH_TIMEOUT_SEC, deja alsshhijo colgado en init); - transmite correctamente
SIGTERM/SIGINTa todo el grupo de procesos (flag-g), para quedocker stopno tarde 10 segundos ni deje huellas dessh.
- reaper a los hijos huérfanos (principal fuente de zombies —
-
docker-compose.yml:init: true— capa adicional de seguridad, en caso de que alguien localmente redefinaentrypoint:(entoncestinidel contenedor desaparecerá, perodocker-initseguirá siendo el PID 1).
Cómo aplicar:
docker compose build --no-cache ansible-status
docker compose up -d
Verificar que ya no se generen más zombies (dentro del contenedor no debe haber líneas con Z en STAT):
docker compose exec ansible-status sh -c 'ps -e -o pid,ppid,stat,comm | awk "NR==1 || /Z/"'
Si se desea aún más cuidado, se puede añadir en el paso ssh_check_for_target las opciones -o ControlMaster=no -o ControlPath=none y start_new_session=True en subprocess.run, para que ssh no genere multiplexores persistentes, y al TimeoutExpired podamos matar toda la process group, y no solo al proceso padre ssh. Pero esto ya es una mejora del comportamiento, y la causa principal de los zombies — la ausencia de init — ya no existe.