컨테이너가 왜 죽었는지를 가장 먼저 알려 주는 신호.
docker inspect web --format '{{.State.ExitCode}} {{.State.OOMKilled}}'
| 코드 | 뜻 | 흔한 원인 |
|---|---|---|
| 0 | 정상 종료 | |
| 1 | 애플리케이션 오류 | 예외로 죽었다. 로그를 본다 |
| 125 | 도커 데몬 오류 | docker run 옵션이 잘못됐다 |
| 126 | 실행 권한 없음 | 스크립트에 실행 비트가 없다 |
| 127 | 명령을 못 찾음 | 경로 오류 · libc 불일치 |
| 137 | SIGKILL (128+9) | OOMKilled 또는 유예 시간 초과 |
| 139 | SIGSEGV (128+11) | 세그폴트 |
| 143 | SIGTERM (128+15) | 정상 정지 요청으로 종료 |
128 + 시그널 번호 규칙이다
137 은 두 갈래다
-
137 + OOMKilled=true — 메모리 제한 초과 — 커널이 죽였다
-
137 + OOMKilled=false — 유예 시간 안에 안 죽어 강제 종료됐다
- PID 1 문제를 의심한다
-
이 한 줄 확인이 진단 방향을 완전히 가른다
127 은 알파인에서 특히 자주 난다
glibc 로 빌드한 바이너리를 alpine(musl libc)에 올리면
-
"not found" 인데 파일은 분명히 있다
-
동적 링커를 못 찾는 것이다
-
확인 — ldd ./app 필요한 라이브러리를 본다
-
대응 — 정적 빌드 · glibc 기반 슬림 이미지 · alpine 에서 빌드
멀티스테이지에서 빌드 스테이지와 런타임 스테이지의 libc 계열이 다를 때 발생한다 — 매우 흔한 실수다
OOMKilled 인데 로그가 없다
- 커널이 SIGKILL 로 죽인 것이라 정리 코드가 돌지 않는다
- 애플리케이션 입장에서는 아무 일도 안 일어난 것처럼 보인다
확인
dmesg | grep -i "killed process" 호스트 커널 로그
docker events --filter event=oom 실시간 이벤트
원인은 대개 둘 중 하나다
- 제한이 실제 사용량보다 작다
- 런타임이 제한을 모르고 호스트 메모리 기준으로 잡는다
재시작 루프를 조사할 때
docker ps -a # STATUS: Restarting (137)
docker logs --tail 200 web
docker logs --since 10m web | head -50 # 루프면 처음 부분을 본다
쿠버네티스에서 보는 법
kubectl describe pod web
# Last State: Terminated
# Reason: OOMKilled
# Exit Code: 137
kubectl get pod web -o jsonpath='{.status.containerStatuses[0].lastState}'
Reason 이 함께 나와 진단이 더 쉽다
- OOMKilled · Error · Completed · ContainerCannotRun
CrashLoopBackOff 는 종료 코드가 아니라 '반복 실패' 상태다
- 실제 원인은 Last State 의 Exit Code 와 로그에 있다
종료 코드가 0 인데 재시작되는 경우
정상 종료(0)인데 계속 뜬다면 restart 정책을 본다
always 종료 코드와 무관하게 다시 띄운다
on-failure 0 이 아닐 때만
unless-stopped 수동으로 멈춘 것은 제외
- 배치성 작업을 always 로 두면 끝날 때마다 다시 돈다
- 일회성 작업은 restart 를 걸지 않거나 잡(Job)으로 실행한다
면접 함정
- ❌ "137은 항상 OOM" → 유예 시간 초과 강제 종료도 137이다.
- ❌ "파일이 있으니 127은 아니다" → libc 불일치면 파일이 있어도 not found가 난다.