Docker 컨테이너 프로세스에서 좀비 프로세스 문제

문제는 전형적인데, 컨테이너 내에서 Python이 PID 1로 실행되고, SSH 타임아웃(특히 ProxyCommand/bastion 시나리오) 발생 시 자식 프로세스들이 PID 1에 의존하지 않는 상태로 대기하게 되어 Zombie 프로세스가 발생합니다. 이를 두 단계로 해결합니다: 첫째, 이미지 자체에 tini를 init으로 설정하여 모든 docker run에서 보호하고, 둘째, docker-compose.ymlinit: 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 패키지를 설치하고, ENTRYPOINTtini -g -- python3 ...으로 감싸서 PID 1을 tini로 설정합니다. 이제 tini는:
    • 부모가 사라진 자식 프로세스(주요 원인: sshProxyCommand/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.runstart_new_session=True를 추가하여 ssh가 장기적인 멀티플렉서를 생성하지 않도록 하고, TimeoutExpired 발생 시 전체 프로세스 그룹을 종료할 수 있게 할 수 있습니다. 하지만 이는 Zombie 문제의 근본 원인(Init 없음)을 해결한 이후의 추가적인 최적화입니다.