Проблема классическая: 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 — уже нет.