docker-compose exit code 137 — OOM이 아닌데 137이 나오는 이유와 SIGTERM 처리로 해결하기
1. 문제 정의
docker-compose up --exit-code-from combined 명령으로 통합 테스트를 실행한 CI 환경에서,
테스트 컨테이너 combined는 정상적으로 종료(exit code 0)했는데도 docker-compose가 반환하는
최종 exit code가 137로 뜨는 문제가 있었습니다.
| |
핵심 오해 지점은 **“테스트가 성공했으니 exit code는 당연히 0이어야 한다”**는 전제였습니다. 실제로는 종료 코드가 테스트 컨테이너의 결과만 반영하지 않습니다.
에러 로그에서 OOMKilled가 명시적으로 false이므로 메모리 부족(OOM)은 원인이 아닙니다.
2. 표면 증상
- 테스트 컨테이너
combined의 로그는EXITING SCRIPT WITH EXIT CODE OF: 0으로 마무리됨. - 그런데
docker-compose가 반환하는 코드는 0이 아닌137. docker inspect <container-id>결과State.OOMKilled: false(컨테이너는 메모리 부족으로 안 죽음).- Cypress 테스트 라인을
init.sh에서 주석 처리하면 exit code가0이 됨 — 즉 테스트 실행 여부와 상관이 있는 문제.
| |
증상 fingerprint (symptom fingerprint) — 빠른 판단 기준
- 에러 메시지: 터미널에
Aborting on container exit...이후137반환- 발생 단계:
docker-compose up --exit-code-from <svc>의 종료 단계- 관련 도구: docker-compose v1 (–exit-code-from, –abort-on-container-exit)
- 흔한 오해: “137 = 무조건 OOM” → ❌ (SIGKILL로도 137이 됨)
- 빠른 판단:
OOMKilled: false+Aborting on container exit...로그가 보이면 OOM이 아니라 강제 종료(신호 9) 문제
3. 원인 추적
exit code 137은 128 + 9 입니다. 리눅스/도커에서 신호(시그널)로 종료된 프로세스의
exit code는 128 + 신호번호가 됩니다.
| 신호번호 | 신호명 | 프로세스 exit code |
|---|---|---|
| 9 | SIGKILL (즉시 강제 종료) | 128 + 9 = 137 |
| 15 | SIGTERM (graceful 요청) | 128 + 15 = 143 |
즉 137의 뜻은 단순히 “SIGKILL로 강제 종료되었다"는 것입니다.
OOM이 아닌 데도 SIGKILL이 발생하는 경로를 따라가면 원인이 드러납니다.
--exit-code-from combined옵션은 docker-compose에서--abort-on-container-exit을 함께 활성화합니다.- docker-compose 문서:
--abort-on-container-exit→ “Stops all containers if any container was stopped.”
- docker-compose 문서:
combined컨테이너가 테스트를 끝내고 exit 0으로 종료됩니다.- 이 신호를 받은 docker-compose는 “한 컨테이너가 종료됐으니 나머지도 모두 중단해야 한다"고 판단해
함께 떠 있던
b-db(MongoDB) 컨테이너를 중단(stop)합니다. - 이때 docker는
b-db에게 먼저 SIGTERM(15) 을 보내 graceful shutdown을 요청하지만, 기본 종료 타임아웃(기본값 10초,stop?t=10) 안에 스스로 종료되지 못하면 SIGKILL(9) 을 보내 강제로 죽입니다. - 그 결과
b-db가137로 종료되고, abort 흐름에 따라 docker-compose가 반환한 최종 코드도137이 됩니다.
디버그 로그가 이 과정을 그대로 보여줍니다.
| |
stop?t=10이 10초 종료 타임아웃입니다.b-db가 계속Pending상태로 대기하다가 grace period(10초)를 넘겨 SIGKILL 되면서137이 됩니다.
원인 후보 매트릭스 (root cause matrix)
원인 후보 확인 명령 해당 시 증상 해결 방향 메모리 부족 (OOM) docker inspect <id>→State.OOMKilledOOMKilled: true컨테이너/엔진 메모리 상향, 리소스 제한 조정 다른 컨테이너 SIGKILL (이번 사례) OOMKilled: false+Aborting on container exit...+stop?t=10대기 로그테스트는 0인데 전체 137 graceful shutdown, --timeout증가디스크 부족 로그에 No space left on device로그에 명시적 오류 docker system prune, 용량 확보
4. 근본 원인
표면적으로는 “테스트 컨테이너가 0으로 나가는데 왜 137?“이지만,
진짜 근본 원인은 abort가 발생했을 때 함께 종료되는 b-db 컨테이너가 SIGTERM을 제대로 처리하지 못해
10초 안에 graceful 하게 종료되지 않고, 결국 SIGKILL(9)로 강제 종료되면서 137이 되었다는 것입니다.
핵심 포인트:
--exit-code-from은 **“이 컨테이너의 exit code를 명령의 exit code로 사용하라”**는 의미지만, docker-compose v1에서 이 옵션은--abort-on-container-exit을 수반합니다.- abort가 트리거되면 모든 다른 컨테이너도 함께 중단되며, 그 강제 중단의 결과가 전체 exit code에 반영됩니다.
- 즉 테스트 성공 여부와 무관하게, 함께 죽는 컨테이너가 죽는 방식이 최종 exit code를 결정합니다.
DevTrace verdict (핵심 판단) 이 문제의 핵심은 “테스트 컨테이너가 0으로 끝났는데도 137이 나온다"는 겉보기 모순이 아니라,
--abort-on-container-exit으로 함께 종료되는 다른 컨테이너(DB)가 SIGTERM을 graceful 하게 처리하지 못해 SIGKILL로 강제 종료되면서 전체 코드가 137이 된다는 점입니다. OOM 여부를 확인하기 전에OOMKilled: false부터 먼저 배제하는 것이 첫 단계입니다.
5. 코드 해결책
방법 A — 종료 대기 시간(timeout)을 늘리기 (가장 빠른 해결)
DB가 graceful 하게 종료될 시간을 주면 SIGKILL로 밀리지 않습니다.
| |
--timeout은 각 컨테이너가 SIGTERM을 받은 뒤 강제 종료(SIGKILL)되기 전에 얼마나 기다릴지(초) 를 지정합니다.- 기본값(10초)보다 긴 시간을 주면 MongoDB 등 충분한 종료 시간이 필요한 컨테이너가 정상적으로 내려갑니다.
- 적용 후 테스트가 모두 통과하면 exit code가
0으로 반환됩니다.
방법 B — DB 컨테이너가 SIGTERM을 graceful 하게 처리하도록 개선 (근본 해결)
가장 바람직한 방법은 종료 시점에 SIGTERM 핸들러가 DB 클라이언트를 정리하고 빠르게 종료되도록 하는 것입니다.
combined 컨테이너의 스타트 스크립트에서 테스트 종료 후 백엔드/프론트 서버를 종료할 때도,
kill -9가 아니라 kill(기본 SIGTERM) 후 어플리케이션이 세션을 정리하도록 하는 것이 권장됩니다.
| |
주의: 종료 시점의 graceful 처리는 애플리케이션 구조와 DB 라이브러리에 따라 결과가 달라질 수 있으므로, 여기서는 “SIGTERM을 받으면 커넥션을 정리하고 종료되도록 처리한다"는 일반적인 점검 기준을 안내합니다.
방법 C — 더 이상 죽을 컨테이너가 없도록 구성
b-db가 실행 중일 필요가 없는 테스트라면 DB가 강제 종료될 때 반환 코드에 영향받지 않도록
별도 실행, 또는 테스트가 끝난 뒤에만 필요한 컨테이너를 분리하는 것이 대안이 될 수 있습니다.
(프로젝트 구성마다 다르므로 환경에 따라 적용 여부를 판단해야 합니다.)
6. 검증 환경 및 로컬 실측 재현
원문(StackOverflow 59296801)은 docker-compose v1.25.0 사례입니다. 이 글의 해결 원리는 글 작성 환경에서 docker compose v2로 재현·검증했습니다. 원문은 v1, 아래 실측은 v2에서 일반 원리를 확인한 것이므로, 두 버전의 CLI 반환 코드가 다를 수 있음을 아래 6.3에서 분리해 정리합니다.
6.1 검증 환경
아래 명령은 다음 환경에서 실행했습니다.
| 항목 | 값 |
|---|---|
| 호스트 OS / 커널 | Ubuntu 22.04.5 LTS / Linux 6.8.0-1049-oracle |
| CPU 아키텍처 | aarch64 (arm64) |
| Docker Engine (Client/Server) | 29.5.2 |
| Docker Compose | v5.1.4 (v2) |
Docker Compose v1 (docker-compose) | 미설치 → v2로 재현 |
| |
docker compose up --help에서 확인한 v2 옵션 구조: v1의--abort-on-container-exit,--exit-code-from,--timeout과 동일한 명령·플래그 구조를 사용합니다.
6.2 재현 절차 (최소 재현 구성)
문제 상황을 최소로 재현하기 위해 두 서비스를 구성했습니다. app 컨테이너는 곧바로 exit 0 하는 테스트 역할, db 컨테이너는 SIGTERM을 무시해서 graceful 종료가 안 되는 DB 역할입니다.
| |
| |
| |
6.3 로컬 실측 실행 결과 (docker compose v2)
(a) docker compose down 후 up --abort-on-container-exit (전체 서비스)
app이 exit 0으로 종료되자 compose가 abort하며 db를 중단하려 합니다.
db는 SIGTERM(trap '' TERM)을 무시하므로 grace period(10초)를 넘겨 SIGKILL로 밀립니다.
| |
docker inspect로 실제 상태를 확인하면 OOMKilled: false + ExitCode: 137 입니다.
| |
- 이 실측에서 db 컨테이너가 SIGKILL로
137이 되는 핵심 메커니즘을 v2에서 재현했습니다. - 다만 compose CLI의 반환값은
echo $?기준0이었습니다. v2는--exit-code-from에 지정된 컨테이너의 exit code를 CLI 반환값으로 사용하므로, app(0)이 선택된 경우 db(137)는 전체 반환값이 아니라 해당 컨테이너의 inspect ExitCode에만 남습니다. 이는 v1.25.0에서 CLI가 137을 보고한 것과 버전별 차이입니다.
(b) --exit-code-from db로 지정 (강제 종료된 컨테이너가 선택되는 경우)
강제 종료된 컨테이너를 --exit-code-from으로 지정하면 CLI 반환값이 137이 되는 것을 확인할 수 있습니다.
| |
(c) SIGTERM을 graceful 하게 처리하는 db로 교체 (방법 B 재현)
SIGTERM 핸들러가 정상 동작하면 db가 강제 종료되지 않고 exit 0으로 내려가며, CLI도 0을 반환합니다.
| |
| |
| |
로컬 실측 요약 (docker compose v2):
| 시나리오 | CLI echo $? | db ExitCode | OOMKilled |
|---|---|---|---|
| (a) SIGTERM 무시 → SIGKILL | 0 (exit-code-from=app) | 137 | false |
(b) --exit-code-from db | 137 | 137 | false |
| (c) graceful 처리 | 0 | 0 | false |
7. 버전 차이 (v1 vs v2) 구체화
원문 사례는 docker-compose v1.25.0이고, 이 글에서 재현·검증한 환경은 docker compose v2(v5.1.4) 입니다. 두 버전의 실제 동작 차이를 아래와 같이 정리합니다.
| 항목 | docker-compose v1 (원문) | docker compose v2 (이 글 실측) |
|---|---|---|
| 명령어 | docker-compose (파이썬 기반) | docker compose (플러그인/standalone) |
--exit-code-from 동작 | 묵시적으로 --abort-on-container-exit 활성화 | up --help 문서상 --exit-code-from이 --abort-on-container-exit 수반(같은 구조) |
| 기본 stop grace period | 10초 (stop?t=10) | 서버 기본 10초 (compose --timeout 기본 동일 -t, docker compose up --help) |
| CLI 반환 코드 | 원문에서 SIGKILL된 컨테이너가 137을 CLI에 반영 (137 보고) | --exit-code-from에 지정된 컨테이너의 exit code를 CLI 반환값으로 사용 → 검증 서비스가 0이면 CLI는 0, 강제 종료 컨테이너를 지정하면 137 |
정리: 원문은 v1, 재현은 v2에서 일반 원리를 확인했습니다. 핵심 원리(그레이스 종료 안 되는 컨테이너가 SIGKILL로 137이 되고, OOMKilled는 false)는 두 버전에서 동일하게 재현되지만, CLI 자체의 반환 코드가 v1에서는 강제 종료된 컨테이너의 137을, v2에서는
--exit-code-from의 대상 컨테이너 코드를 따르는 차이가 있습니다. 실무에서는 반드시 해당 환경에서docker compose up --help(또는 v1이라면docker-compose up --help)로 옵션 의미를 확인하세요.
8. 재발 방지 조치
예방 체크
docker compose up --abort-on-container-exit을 쓸 때는 “다른 컨테이너도 함께 죽는다"는 것을 항상 인지.- DB 등 무거운 컨테이너를 함께 두는 경우
--timeout으로 grace period를 충분히 보장. - 프로세스가 SIGTERM을 받았을 때 동작을 점검(커넥션 정리, 파일 플러시).
운영 예방책 (검증 명령)
- 해결 후 최종 성공 여부는 아래 명령으로 확인합니다.
| |
해결책 비교:
| 해결책 | 장점 | 단점 | 적합한 상황 |
|---|---|---|---|
--timeout 600 증가 | 변경 최소, 즉시 적용 | 종료 시간 늘어날 수 있음 | DB 등 graceful 종료가 느린 컨테이너 |
| SIGTERM graceful 처리 | 근본 해결, 종료 시간 단축 (6.3-(c)에서 실측) | 코드/스크립트 수정 필요 | 정기적으로 반복되는 CI |
| 컨테이너 구성 분리 | 불필요한 종속 제거 | 구조 변경 부담 | 테스트가 DB와 독립적인 경우 |
잘못된/임시방편 해결책:
kill -9를 습관적으로 사용해 프로세스를 죽이면 graceful 정리를 건너뛰어 데이터 손상·장애의 원인이 됩니다.- 137이 나올 때 무조건 메모리만 올리는 것은 원인이 OOM이 아닐 경우 효과가 없고, 리소스를 낭비합니다.
반드시
OOMKilled필드와 병행 로그를 먼저 확인하세요.