Problema dei zombie nei processi del contenitore Docker

Problema classico: Python è avviato come PID 1 all’interno del contenitore, e nei timeout SSH (soprattutto con ProxyCommand/bastion) i figli figliolini rimangono sospesi sul PID 1, che non li attende — da qui i processi zombie. Risolvo con due livelli: installo tini come init all’interno dell’immagine (protegge qualsiasi docker run), e attivo init: true nel compose come segnale esplicito.

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 come PID 1: raccoglie i figli orfani (principale fonte di zombie — ssh con ProxyCommand/bastion, ucciso per ConnectTimeout/SSH_TIMEOUT_SEC, lascia il figlio ssh al bastion sospeso sull'init);
# e propaga correttamente i segnali (SIGTERM/SIGINT) a tutta la group dei processi (flag -g), così che docker stop non impatti per 10 secondi e non lasci code ssh.

ENTRYPOINT ["/usr/bin/tini", "-g", "--", "python3", "/app/server.py", "--host", "0.0.0.0", "--config", "/config/config.yaml"]

    restart: always
    # Nel caso in cui l'ENTRYPOINT nell'immagine sia stato ridefinito: docker-init (tini)
    # raccoglierà i zombie da ssh/ProxyCommand nei timeout.

    init: true
    ports:

Cosa è cambiato e perché:

  • Dockerfile: Installo il pacchetto tini e avvolgo il comando con tini -g -- python3 .... Ora il PID 1 è tini, che:

    • raccoglie i figli orfani (principale fonte di zombie — ssh con ProxyCommand/bastion, terminato per ConnectTimeout/SSH_TIMEOUT_SEC, lascia il figlio ssh al bastion sospeso sull’init);
    • propaga correttamente SIGTERM/SIGINT a tutta la group dei processi (flag -g), così che docker stop non impatti per 10 secondi e non lasci code ssh.
  • docker-compose.yml: init: true — un ulteriore strato di sicurezza, nel caso in cui qualcuno ridefinisca localmente entrypoint: (allora tini dall’immagine scompare, ma docker-init rimane comunque come PID 1).

Applicare:

docker compose build --no-cache ansible-status
docker compose up -d

Verificare che i zombie non si moltiplichino più (all’interno del contenitore non dovrebbero esserci righe con Z in STAT):

docker compose exec ansible-status sh -c 'ps -e -o pid,ppid,stat,comm | awk "NR==1 || /Z/"'

Se si desidera ancora più cura — si può aggiungere, in un passaggio separato, nelle opzioni di ssh_check_for_target i parametri -o ControlMaster=no -o ControlPath=none e start_new_session=True nel subprocess.run, così che ssh non crei multiplexer a lunga durata e, in caso di TimeoutExpired, si possa terminare tutta la process group, e non solo il processo padre ssh. Ma questo è un miglioramento del comportamento, mentre la causa principale dei zombie — l’assenza di init — è già risolta.