문제는 전형적인데, 컨테이너 내에서 Python이 PID 1로 실행되고, SSH 타임아웃(특히 ProxyCommand/bastion 시나리오) 발생 시 자식 프로세스들이 PID 1에 의존하지 않는 상태로 대기하게 되어 Zombie 프로세스가 발생합니다. 이를 두 단계로 해결합니다: 첫째, 이미지 자체에 tini를 init으로 설정하여 모든 docker run에서 보호하고, 둘째, docker-compose.yml에 init: true를 명시적으로 추가하여 명확한 신호를 전달합니다.
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 타임아웃)를 재부팅하고,
# SIGTERM/SIGINT 신호를 Python 서버 전체 프로세스 그룹에 정확히 전달합니다.
ENTRYPOINT ["/usr/bin/tini", "-g", "--", "python3", "/app/server.py", "--host", "0.0.0.0", "--config", "/config/config.yaml"]
restart: always
# ENTRYPOINT가 이미지 내에서 재정의된 경우에도 fallback으로 docker-init(tini)이 PID 1을 맡아,
# SSH/ProxyCommand 타임아웃 시 발생하는 Zombie 프로세스를 처리합니다.
init: true
ports:
변경 사항 및 이유:
Dockerfile:tini패키지를 설치하고,ENTRYPOINT를tini -g -- python3 ...으로 감싸서 PID 1을tini로 설정합니다. 이제tini는:- 부모가 사라진 자식 프로세스(주요 원인:
ssh의ProxyCommand/bastion,ConnectTimeout/SSH_TIMEOUT_SEC으로 인해 종료된 경우, bastion에 대한 자식ssh프로세스가 init에 의존하게 됨)를 재부팅합니다. SIGTERM/SIGINT신호를 전체 프로세스 그룹에 정확히 전달하여docker stop이 10초 동안 멈추지 않고, SSH残留 프로세스도 남지 않도록 합니다.
- 부모가 사라진 자식 프로세스(주요 원인:
docker-compose.yml:init: true는 이미지 내entrypoint:가 재정의된 경우에도 fallback으로docker-init(tini)이 PID 1을 맡아 Zombie 프로세스를 처리할 수 있도록 보장하는 추가적인 안전장치입니다.
적용 방법:
docker compose build --no-cache ansible-status
docker compose up -d
Zombie 프로세스가 더 이상 생성되지 않는지 확인 (컨테이너 내부에서 STAT 필드에 Z가 포함된 행이 없어야 함):
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 옵션과 subprocess.run의 start_new_session=True를 추가하여 ssh가 장기적인 멀티플렉서를 생성하지 않도록 하고, TimeoutExpired 발생 시 전체 프로세스 그룹을 종료할 수 있게 할 수 있습니다. 하지만 이는 Zombie 문제의 근본 원인(Init 없음)을 해결한 이후의 추가적인 최적화입니다.