환경 Docker Engine · Dockerfile / docker stop / kill(2) · 2026-08-03 공식 문서 확인
배포할 때마다 컨테이너 종료에 정확히 10초가 걸리고 종료 로그가 남지 않는 경우가 있습니다. SIGTERM 이 애플리케이션까지 닿지 않아 유예 시간을 다 쓰고 강제 종료된 상태입니다. 공식 문서 기준으로 신호가 어디서 끊기는지 짚어 보는 순서를 정리했습니다.
10초는 임의의 숫자가 아니다
docker stop 문서는 이 명령이 하는 일을 두 단계로 적어 두었습니다. 컨테이너 안의 메인 프로세스가 SIGTERM 을 받고, 유예 시간이 지난 뒤에 SIGKILL 을 받는다는 것입니다. 그리고 유예 시간이 지나도 컨테이너가 종료되지 않으면 SIGKILL 로 강제로 죽인다고 이어집니다.
그 유예 시간의 기본값도 같은 문서에 있습니다. 컨테이너에 따로 설정된 기본값이 없으면 데몬이 정하는데, 리눅스 컨테이너는 10초, 윈도우 컨테이너는 30초라고 되어 있습니다. 배포 때 걸리는 10초는 우연히 그만큼 걸린 것이 아니라, SIGTERM 을 보내고 아무 반응이 없어 끝까지 기다린 시간입니다.
종료 코드 137 도 같은 이야기를 합니다. 128 에 SIGKILL 의 번호 9 를 더한 값이라, 프로세스가 스스로 끝난 것이 아니라 강제로 종료됐다는 뜻입니다. 즉 문제는 '느린 종료'가 아니라 '신호를 못 받은 종료'입니다. 타임아웃을 늘려도 10초가 30초로 바뀔 뿐 결과는 같습니다.
# 종료에 실제로 몇 초가 걸리는지 재 본다
time docker stop myapp
# 종료 코드 확인 — 137 이면 SIGKILL 로 끝난 것이다
docker inspect -f '{{.State.ExitCode}}' myapp
# 이 이미지가 종료 신호로 무엇을 쓰는지 확인
docker inspect -f '{{.Config.StopSignal}}' myapp
time 이 10초에 가깝게 나오고 종료 코드가 137 이라면, 유예 시간을 다 쓰고 강제 종료된 경우입니다. 여기서부터는 신호가 어디서 끊겼는지를 봐야 합니다.
10초가 걸렸다는 것은 기다린 쪽이 끝까지 기다렸다는 뜻입니다. 받는 쪽이 응답하지 않았다는 신호입니다.
신호는 PID 1에게만 간다
docker stop 이 신호를 보내는 대상은 컨테이너 안의 메인 프로세스, 즉 PID 1 입니다. 애플리케이션이 PID 1 이 아니라 그 아래 자식 프로세스로 떠 있으면 SIGTERM 은 애초에 그 프로세스까지 도달하지 않습니다. 핸들러를 아무리 잘 달아 두어도 불릴 일이 없습니다.
이 자리가 어긋나는 가장 흔한 원인은 Dockerfile 의 ENTRYPOINT 를 셸 형식으로 쓴 경우입니다. Dockerfile 문서는 셸 형식의 ENTRYPOINT 가 명령을 /bin/sh -c 의 하위 명령으로 시작하며 신호를 전달하지 않는다고 적고 있습니다. 그래서 실행 파일이 컨테이너의 PID 1 이 되지 못하고 유닉스 신호를 받지 못한다고, 문서가 그대로 설명합니다.
같은 문서는 결과까지 명시합니다. 이 경우 실행 파일은 docker stop 이 보내는 SIGTERM 을 받지 못한다는 것입니다. 컨테이너 안에서 프로세스 목록을 찍어 보면 상황이 바로 보입니다. 1번 자리에 sh 가 앉아 있고 애플리케이션이 그 자식으로 붙어 있으면 이 경우에 해당합니다.
# 컨테이너 안에서 1번 자리에 무엇이 앉아 있는지 본다
docker exec myapp ps -eo pid,ppid,comm
# 출력 예 — sh 가 PID 1 이고 애플리케이션은 자식이다
# PID PPID COMMAND
# 1 0 sh
# 7 1 node
# 이미지에 어떤 형식으로 기록돼 있는지 확인
docker inspect -f '{{json .Config.Entrypoint}} {{json .Config.Cmd}}' myapp
Entrypoint 가 ["/bin/sh","-c","..."] 형태로 보이면 셸 형식으로 해석된 것입니다. 이 상태에서는 신호가 sh 에서 멈춥니다.
핸들러가 안 불리는 것이 아니라, 신호가 그 프로세스까지 오지 않는 것입니다.
PID 1은 기본 동작도 다르다
자리를 바로잡아 애플리케이션을 PID 1 로 올려도 한 가지가 더 남습니다. PID 1 은 신호의 기본 처리 방식이 일반 프로세스와 다릅니다. kill(2) 매뉴얼은 프로세스 ID 1 인 init 프로세스에게 보낼 수 있는 신호는 init 이 명시적으로 핸들러를 설치해 둔 신호뿐이라고 적어 두었습니다.
일반 프로세스는 SIGTERM 에 대해 아무것도 하지 않아도 커널의 기본 동작으로 종료됩니다. PID 1 에서는 그 기본 동작이 적용되지 않아서, 핸들러가 없으면 신호가 조용히 무시됩니다. 프로그램은 멀쩡히 살아 있고 docker stop 은 유예 시간을 다 쓴 뒤 SIGKILL 로 넘어갑니다. 겉으로 보이는 증상은 앞의 경우와 똑같습니다.
그래서 확인할 것이 두 가지로 갈립니다. 애플리케이션이 PID 1 자리에 있는지, 그리고 그 자리에서 SIGTERM 핸들러를 실제로 등록했는지입니다. 둘 중 하나만 충족돼도 증상은 그대로 남습니다. 셸 형식을 쓰더라도 앞에 exec 를 붙이면 셸이 자기 자신을 그 프로그램으로 교체하므로 자리 문제는 해결되지만, 핸들러 문제는 별개로 남습니다.
# 신호가 sh 에서 멈추는 형태
ENTRYPOINT node server.js
# JSON 배열(exec 형식) — 프로그램이 직접 PID 1 이 된다
ENTRYPOINT ["node", "server.js"]
# 셸 기능이 꼭 필요하면 exec 로 셸을 교체한다
ENTRYPOINT ["/bin/sh", "-c", "exec node server.js"]
# 종료 신호를 바꿔야 하는 프로그램이면 명시한다
STOPSIGNAL SIGQUIT
JSON 배열로 쓰려면 반드시 큰따옴표를 써야 합니다. Dockerfile 문서가 exec 형식은 JSON 배열로 파싱되며 작은따옴표가 아닌 큰따옴표를 써야 한다고 명시하고 있습니다.
직접 처리하지 못하는 프로그램이라면 init을 끼운다
PID 1 을 그대로 두는 편이 곤란한 경우도 있습니다. 소스를 고칠 수 없는 서드파티 바이너리이거나, 프로세스를 여러 개 띄워 자식이 좀비로 남는 구조라면 그렇습니다. 이럴 때 docker run 문서는 --init 옵션을 안내합니다. 컨테이너의 PID 1 로 init 프로세스를 쓰게 하는 옵션이고, 그 프로세스가 신호를 전달하고 프로세스를 거두어들인다고 설명되어 있습니다.
종료 신호 자체를 바꿔야 하는 프로그램도 있습니다. docker stop 문서는 기본 신호가 이미지의 StopSignal 로 정해지며, 이미지를 만들 때 STOPSIGNAL 명령으로 설정하거나 컨테이너를 만들 때 --stop-signal 옵션으로 지정할 수 있다고 적고 있습니다. SIGTERM 이 아니라 다른 신호를 종료로 해석하는 프로그램이라면 여기서 맞춰 줍니다.
유예 시간을 조정하는 자리도 따로 있습니다. --stop-timeout 은 정해진 신호를 보낸 뒤 컨테이너가 멈추기를 기다릴 초를 설정한다고 문서에 나와 있습니다. 다만 이것은 신호를 제대로 받는 프로그램이 정리에 시간이 더 필요할 때 쓰는 값입니다. 신호를 못 받고 있는 상황에서 이 값을 늘리는 것은 배포가 멈춰 있는 시간만 늘립니다.
# 신호 전달과 좀비 정리를 init 에 맡긴다
docker run --init -d --name myapp myimage:latest
# 종료 신호를 컨테이너 생성 시점에 지정
docker run -d --stop-signal=SIGQUIT --name myapp myimage:latest
# 정리에 시간이 더 필요한 경우에만 유예 시간을 늘린다
docker run -d --stop-timeout=30 --name myapp myimage:latest
# 신호가 실제로 닿는지 따로 확인 — 종료 처리 로그가 찍혀야 한다
docker kill --signal=SIGTERM myapp && docker logs --tail 20 myapp
마지막 줄은 유예 시간과 무관하게 SIGTERM 만 보내 봅니다. 여기서 종료 로그가 찍히면 신호는 닿고 있는 것이고, 아무 반응이 없으면 아직 자리나 핸들러 문제가 남은 것입니다.
유예 시간을 늘리는 것은 정리가 오래 걸릴 때의 조치지, 신호가 안 닿을 때의 조치가 아닙니다.
조치 후 확인할 것
- docker exec 로 ps 를 찍어 애플리케이션이 PID 1 자리에 있는지 확인합니다. sh 가 1번이면 ENTRYPOINT 형식부터 고칩니다.
- docker kill --signal=SIGTERM 을 보낸 뒤 로그에 종료 처리 메시지가 실제로 찍히는지 봅니다.
- time docker stop 이 10초를 다 쓰지 않고 끝나는지, 종료 코드가 137 이 아닌 값으로 바뀌었는지 확인합니다.
- 여러 프로세스를 띄우는 컨테이너라면 --init 을 붙였는지, 좀비 프로세스가 쌓이지 않는지 점검합니다.
- STOPSIGNAL 이나 --stop-signal 을 바꿨다면, 그 신호를 프로그램이 종료로 해석하는지 해당 프로그램 문서에서 확인합니다.
확인한 문서
- docker container stop 공식 문서 (SIGTERM·유예 시간·StopSignal) (2026-08-03 확인)
- Dockerfile reference — ENTRYPOINT 셸 형식과 신호 전달 (2026-08-03 확인)
- kill(2) 리눅스 매뉴얼 (NOTES — PID 1 의 신호 처리) (2026-08-03 확인)
- docker container run 공식 문서 (--init, --stop-signal, --stop-timeout) (2026-08-03 확인)
정리하면 확인 순서는 짧습니다. 10초를 다 썼는지 재고, PID 1 자리에 무엇이 있는지 보고, 그 자리에서 SIGTERM 을 실제로 처리하는지 확인합니다. 타임아웃 값을 손대는 것은 이 세 가지를 다 통과한 다음에 생각할 일입니다.
'복사금지 블로그 짜증나서 만든 개발문서' 카테고리의 다른 글
| npm 배포 전에 어떤 파일이 올라가는지 미리 확인하는 방법 (0) | 2026.08.07 |
|---|---|
| 맥에서 파일명 대소문자만 바꿨더니 git이 못 잡을 때 (0) | 2026.08.05 |
| rm 했는데 디스크 용량이 안 줄어들 때 확인하는 순서 (0) | 2026.08.02 |
| cron에서만 스크립트가 실패한다면 확인할 세 가지 (0) | 2026.08.01 |
| npm ci가 CI에서만 실패한다면 package-lock.json부터 보세요 (0) | 2026.07.31 |