Проблема зомби в процессах Docker-контейнера

Проблема классическая: python запущен PID 1 в контейнере, и при таймаутах SSH (особенно с ProxyCommand/bastion) внуки переподвешиваются на PID 1, который их не жнёт — отсюда зомби. Чиню двумя слоями: ставлю tini как init в самом образе (защищает любой docker run), и включаю init: true в compose как явный сигнал.

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 как PID 1: жнёт осиротевших внуков (ProxyCommand/bastion, ssh-таймауты)
# и корректно прокидывает сигналы в python-сервер.
ENTRYPOINT ["/usr/bin/tini", "-g", "--", "python3", "/app/server.py", "--host", "0.0.0.0", "--config", "/config/config.yaml"]
    restart: always
    # На случай, если ENTRYPOINT в образе был переопределён: docker-init (tini)
    # подхватит зомби от ssh/ProxyCommand при таймаутах.
    init: true
    ports:

Что изменилось и почему:

  • Dockerfile: ставлю пакет tini и оборачиваю команду tini -g -- python3 …. Теперь PID 1 — это tini, который:
    • реапит осиротевших внуков (главный источник зомби — ssh с ProxyCommand/bastion, убитый по ConnectTimeout/SSH_TIMEOUT_SEC, оставляет дочерний ssh к bastion подвешенным на init);
    • корректно прокидывает SIGTERM/SIGINT всей группе процессов (флаг -g), чтобы docker stop не висел 10 секунд и не оставлял ssh-хвостов.
  • docker-compose.yml: init: true — пояс поверх подтяжек, на случай если кто-то локально переопределит entrypoint: (тогда tini из образа исчезнет, но docker-init всё равно встанет PID 1).

Применить:

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

Проверить, что зомби больше не плодятся (внутри контейнера не должно быть строк с Z в STAT):

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

Если хочется ещё аккуратнее — можно отдельным шагом добавить в ssh_check_for_target опции -o ControlMaster=no -o ControlPath=none и start_new_session=True в subprocess.run, чтобы ssh не плодил долгоживущих мультиплексоров и при TimeoutExpired мы могли убивать всю process group, а не только головной ssh. Но это уже улучшение поведения, а самой причины зомби — отсутствия init — уже нет.