테마 전환

Docker 컨테이너가 시작하자마자 종료될 때: Exit Code 137/1까지 다루는 완전한 문제 해결 가이드

Easton editorial illustration: bottleneck pressure gauge

컴퓨터를 끄고 퇴근하려던 순간 휴대전화가 울렸습니다. 프로덕션 환경 경보였습니다. 확인해 보니 핵심 서비스 4개의 컨테이너가 모두 Exited 상태로 바뀌어 있었습니다. 터미널을 열고 docker ps를 입력했습니다. 아무것도 없었습니다. 완전히 비어 있었습니다.

음료를 꺼내려고 냉장고를 열었는데 냉장고 전체가 텅 빈 것을 발견한 기분과 비슷했습니다. 당황스러웠습니다.

솔직히 그때 가장 먼저 든 생각은 “끝났다. 이번 주말은 망했다”였습니다. 하지만 진정하고 나니 컨테이너 시작 실패가 처음 겪는 문제는 아니라는 사실을 깨달았습니다. 다만 이번에는 문제가 더 갑작스럽게 발생했고 영향도 더 컸을 뿐입니다.

2시간 넘게 조사한 끝에 원인은 사실 간단하다는 것을 알아냈습니다. 한 서비스의 설정 파일 경로가 잘못되어 의존하는 데이터베이스 연결이 실패했고, 컨테이너가 시작하자마자 종료된 것입니다. 그때 체계적인 진단 절차가 있었다면 10분 안에 해결할 수도 있었을 문제였습니다.

이 글은 수많은 시행착오를 겪은 뒤 정리한 컨테이너 시작 실패 문제 해결 가이드입니다. Exit Code 1, 137 또는 다른 종료 코드가 표시되더라도 이 방법을 따르면 문제의 근본 원인을 빠르게 찾을 수 있습니다.

컨테이너 수명 주기와 종료 코드 이해하기

문제를 살펴보기 전에 먼저 기본적인 질문부터 알아보겠습니다. 컨테이너는 왜 종료될까요?

컨테이너의 본질: 프로세스의 수명 주기

쉽게 말해 Docker 컨테이너는 격리된 프로세스입니다. 이 프로세스가 살아 있으면 컨테이너가 실행되고, 프로세스가 끝나면 컨테이너도 종료됩니다.

Web 서비스 컨테이너를 시작한다고 생각해 보겠습니다. 컨테이너의 메인 프로세스는 nginx나 node일 수 있습니다. 이 프로세스가 실행되는 동안에는 docker ps에서 컨테이너를 볼 수 있습니다. 하지만 메인 프로세스가 어떤 이유로든 종료되면, 정상적으로 끝났든 충돌했든 시스템에 의해 강제로 종료됐든 컨테이너는 즉시 Exited 상태로 바뀝니다.

그래서 때로는 docker ps를 실행해도 아무것도 표시되지 않고, 이미 종료된 컨테이너를 보려면 -a 옵션을 추가해야 합니다.

종료 코드 빠른 참조표: 숫자에 담긴 의미

컨테이너가 종료될 때마다 Docker는 종료 코드(Exit Code)를 기록합니다. 이 숫자는 신비해 보이지만 실제로는 무슨 일이 일어났는지 알려 주는 단서입니다.

Exit Code 0: 모든 것이 정상이며 작업이 완료되었습니다.
예를 들어 데이터 가져오기 스크립트를 실행했고 작업을 마친 뒤 자연스럽게 종료되었다면 코드가 0입니다. 문제가 아니라 컨테이너가 해야 할 일을 끝낸 것입니다.

Exit Code 1: 프로그램 자체에서 문제가 발생했습니다.
가장 흔한 오류 코드입니다. 설정 파일 오류, 의존성 누락, 코드 버그 등이 원인일 수 있습니다. 어떤 경우든 컨테이너 안의 애플리케이션 자체가 종료된 것입니다.

예전에 MySQL 컨테이너를 배포했는데 계속 Exit Code 1로 종료된 적이 있습니다. 로그를 한참 살펴본 뒤에야 설정 파일의 등호 하나를 실수로 콜론으로 입력했다는 것을 발견했습니다. MySQL은 시작하자마자 설정의 구문 오류를 감지하고 바로 작동을 멈췄습니다.

Exit Code 137: 메모리가 부족하거나 프로세스가 강제로 종료되었습니다.
제가 가장 보기 싫어하는 코드입니다. 137은 일반적으로 두 가지 상황을 뜻합니다.

  1. 컨테이너가 허용된 메모리를 초과해 Linux 커널의 OOM Killer가 프로세스를 종료했습니다.
  2. 누군가 또는 시스템이 docker kill이나 kill -9를 실행했습니다.

둘은 어떻게 구분할까요? docker inspectOOMKilled 필드를 확인하면 됩니다. 값이 true이면 메모리 문제이고, false이면 사람이 직접 또는 다른 방식으로 종료했을 수 있습니다.

Exit Code 127: 명령을 찾을 수 없습니다.
일반적으로 Dockerfile의 CMD나 ENTRYPOINT 경로가 잘못되었거나 컨테이너 이미지에 해당 실행 파일이 아예 없는 경우입니다.

Exit Code 139: 세그멘테이션 오류(Segmentation Fault)입니다.
주로 C/C++ 프로그램에서 발생하며, 프로그램이 접근해서는 안 되는 메모리 주소에 접근했다는 뜻입니다. 저수준 프로그램을 실행하지 않는다면 거의 볼 일이 없습니다.

종료 코드의 규칙

상태 코드에는 다음과 같은 규칙이 있습니다.

  • 0: 문제없는 정상 종료
  • 1-128: 애플리케이션 오류, 설정 오류 등 프로그램 자체의 문제
  • 129-255: 신호로 중단되거나 시스템에 종료되는 등의 외부 개입

이 규칙을 알면 종료 코드를 보는 순간 대략 어떤 유형의 문제인지 판단할 수 있어 다음 진단 방향을 정하기 쉽습니다.

4단계 진단법으로 문제를 빠르게 찾기

이제 종료 코드의 의미를 알았습니다. 하지만 의미만 아는 것으로는 부족합니다. 문제를 단계별로 파고드는 방법도 알아야 합니다.

제가 정리한 4단계 진단법은 컨테이너 시작 실패 사례의 약 90%를 다룹니다. 이 순서대로 확인하면 문제가 생각보다 신비롭지 않다는 것을 알게 될 것입니다.

1단계: 컨테이너 상태 확인

로그부터 서둘러 보지 말고, 먼저 컨테이너가 실제로 존재하며 종료된 상태인지 확인합니다.

docker ps -a

이 명령은 종료된 컨테이너를 포함한 모든 컨테이너를 나열합니다. 다음 핵심 정보를 확인해야 합니다.

CONTAINER ID: 컨테이너의 고유 식별자이며 이후 명령에서 사용합니다. 앞부분 몇 자리만 복사해도 Docker가 자동으로 일치하는 컨테이너를 찾습니다.

STATUS 열: 가장 중요합니다. 정상 실행 중인 컨테이너는 Up X minutes, 종료된 컨테이너는 Exited (종료 코드) X minutes ago로 표시됩니다.

예를 들어 다음과 같이 표시될 수 있습니다.

CONTAINER ID   IMAGE         STATUS
a1b2c3d4e5f6   mysql:8.0     Exited (1) 2 minutes ago

종료 코드가 1이면 애플리케이션 계층의 문제입니다. 137이면 메모리 문제일 가능성이 있습니다.

컨테이너의 생성 시각과 종료 시각도 확인하세요. 생성된 지 1초도 되지 않아 종료됐다면 대개 시작 명령이나 설정에 문제가 있습니다. 일정 시간 실행된 뒤 종료됐다면 리소스 부족이나 의존 서비스 장애가 원인일 수 있습니다.

2단계: 컨테이너 로그 확인

가장 중요한 단계입니다. 컨테이너는 종료되기 전에 대개 단서를 남기며, 그 단서는 로그에 있습니다.

기본 로그 확인:

docker logs <container_id>

컨테이너의 모든 표준 출력과 표준 오류를 보여 줍니다. Permission denied, No such file or directory, Connection refused 같은 오류 메시지를 바로 볼 수 있는 경우가 많습니다.

실시간 추적(시작 과정 진단에 적합):

docker logs -f <container_id>

컨테이너가 시작되는 동안 무슨 일이 일어나는지 보고 싶다면 -f 옵션을 사용합니다. tail -f처럼 새로운 로그를 실시간으로 표시합니다. 이미 종료된 컨테이너에는 큰 의미가 없으며, 주로 다시 시작을 시도하면서 관찰할 때 사용합니다.

최근 로그만 확인:

docker logs --tail 100 <container_id>

컨테이너 로그가 너무 길다면 마지막 100줄만 볼 수 있습니다. 대개 마지막 몇 줄에서 문제를 찾을 수 있습니다.

타임스탬프 표시:

docker logs -t <container_id>

-t 옵션은 각 로그 줄 앞에 타임스탬프를 붙여 문제 발생 시각을 정확히 파악하기 쉽게 합니다.

오류 로그 필터링:

docker logs <container_id> 2>&1 | grep -i error

로그가 너무 많다면 “error”가 포함된 줄만 볼 수 있습니다. 핵심 오류를 빠르게 찾을 수 있는 요령입니다.

3단계: 컨테이너 설정 확인

때로는 로그만으로 원인을 알기 어렵습니다. 이럴 때는 컨테이너의 설정과 상태를 더 깊이 살펴봐야 합니다.

전체 설정 확인:

docker inspect <container_id>

컨테이너의 모든 설정, 환경 변수, 마운트 지점, 네트워크 설정 등을 포함한 많은 JSON 정보를 출력합니다. 정보량이 많지만 매우 유용합니다.

특정 정보 빠르게 확인하기:

종료 코드 확인:

docker inspect --format '{{.State.ExitCode}}' <container_id>

OOM Killed 여부 확인:

docker inspect --format '{{.State.OOMKilled}}' <container_id>

출력이 true라면 메모리 문제로 확정할 수 있습니다.

환경 변수 확인:

docker inspect --format '{{.Config.Env}}' <container_id>

데이터베이스 연결 문자열이나 API 키 같은 환경 변수가 잘못 설정된 경우도 있습니다.

마운트 경로 확인:

docker inspect --format '{{.Mounts}}' <container_id>

설정 파일과 데이터 디렉터리가 올바르게 마운트되었는지 확인합니다.

로그 파일 경로 확인:

docker inspect --format='{{.LogPath}}' <container_id>

docker logs도 제대로 작동하지 않는다면 호스트에서 로그 파일을 직접 찾을 수 있습니다.

4단계: 대화형 시작으로 검증

앞의 세 단계를 모두 거쳤는데도 문제를 찾지 못했다면 컨테이너 내부를 직접 확인해야 합니다.

컨테이너를 대화형으로 시작하기:

원래 시작 명령이 다음과 같다면:

docker run -d my-app

-d-it로 바꿔 컨테이너를 포그라운드에서 실행합니다.

docker run -it my-app

이렇게 하면 컨테이너 시작 과정의 모든 출력을 실시간으로 볼 수 있어 많은 오류가 화면에 바로 나타납니다.

컨테이너에 직접 들어가기:

컨테이너가 시작된 뒤 바로 종료된다면 shell로 들어가 명령을 직접 실행할 수 있습니다.

docker run -it my-app /bin/bash

또는:

docker run -it my-app /bin/sh

내부에서 다음을 확인할 수 있습니다.

  • 설정 파일이 존재하는지 확인: ls /etc/app/config.yaml
  • 설정 파일 구문 테스트: 예를 들어 MySQL의 mysqld --verbose --help는 설정을 검사합니다.
  • 시작 명령을 직접 실행해 구체적인 오류 확인
  • 의존 서비스 연결 확인: ping database, telnet redis 6379

이 방법은 특히 경로, 권한, 의존성 문제를 조사할 때 유용합니다.

단계가 많아 보일 수 있지만 실제로는 2단계에서 로그를 확인할 때 답을 찾는 경우가 많습니다. 드문 난제에만 네 단계를 모두 거쳐야 합니다.

자주 발생하는 5가지 실패 시나리오와 해결책

진단 방법을 살펴봤으니 이제 실전에서 가장 자주 만나는 사례를 알아보겠습니다. 일상적으로 발생하는 문제 대부분을 다섯 가지 유형으로 정리했습니다.

시나리오 1: 설정 파일 오류 또는 존재하지 않는 경로

대표적인 증상:

  • Exit Code 1
  • 로그에 No such file or directory, config file not found, syntax error 같은 오류가 표시됨

실제 사례:

Node.js 애플리케이션을 배포했는데 컨테이너가 계속 시작되지 않은 적이 있습니다. 로그에는 다음과 같이 표시됐습니다.

Error: ENOENT: no such file or directory, open '/app/config/prod.json'

확인해 보니 docker run 명령의 마운트 경로를 다음과 같이 작성했습니다.

-v /home/user/config:/app/conf  # 여기에는 conf가 사용됨

하지만 애플리케이션이 읽는 경로는 /app/config였습니다. 경로에서 한 글자가 달라 애플리케이션이 설정 파일을 찾지 못했고 시작에 실패했습니다.

진단 방법:

  1. docker inspect --format '{{.Mounts}}'로 마운트 경로를 확인합니다.
  2. 컨테이너 안에서 ls를 실행해 파일이 실제로 해당 위치에 있는지 확인합니다.
  3. 설정 파일의 구문 오류라면 대부분 애플리케이션 로그에 문제가 있는 줄이 명확히 표시됩니다.

해결책:

마운트 경로 오류:

# 잘못된 예
docker run -v /host/path:/wrong/path my-app

# 올바른 방법
docker run -v /host/path:/app/config my-app

설정 파일 구문 오류:

  • YAML 파일은 온라인 도구나 yamllint로 구문을 검사할 수 있습니다.
  • JSON 파일은 jq 명령으로 검증합니다: jq . config.json
  • MySQL 설정은 컨테이너 안에서 mysqld --verbose --help를 실행해 구문 오류를 확인할 수 있습니다.

시나리오 2: 메모리 부족(OOM Killed)

대표적인 증상:

  • Exit Code 137
  • docker inspect --format '{{.State.OOMKilled}}'true를 반환함
  • 로그에 Cannot allocate memory, Out of memory 같은 문구가 표시될 수 있음

실제 사례:

로컬 개발 환경에서는 잘 실행되던 Java 애플리케이션이 테스트 서버에 배포하자 계속 다시 시작된 적이 있습니다. 로그를 확인하니 다음 메시지가 있었습니다.

OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory failed; error='Cannot allocate memory' (errno=12)

테스트 서버의 Docker Desktop 메모리 제한은 512MB였는데 Java 애플리케이션은 시작할 때부터 600MB를 사용하고 있었습니다.

진단 방법:

# OOM 여부 확인
docker inspect --format '{{.State.OOMKilled}}' <container_id>

# 호스트 메모리 상태 확인
free -h

# 컨테이너 실행 중 메모리 사용량 확인
docker stats <container_id>

해결책:

컨테이너 메모리 제한 늘리기:

docker run -m 1g my-app  # 최대 메모리를 1GB로 제한
docker run -m 512m --memory-swap 1g my-app  # swap도 함께 설정

Docker Desktop이라면 설정에서 조정합니다.

  • macOS: Docker Desktop → Preferences → Resources → Memory
  • Windows: Docker Desktop → Settings → Resources → Memory

애플리케이션 자체 최적화:

  • Java 애플리케이션은 JVM 힙 크기를 제한할 수 있습니다: java -Xmx512m -jar app.jar
  • Node.js는 다음과 같이 설정할 수 있습니다: node --max-old-space-size=512 app.js
  • 코드에 메모리 누수가 있는지 확인합니다.

프로덕션 환경 권장 사항:

  • 애플리케이션의 실제 요구 사항에 따라 적절한 메모리 상한을 설정합니다.
  • --memory-reservation으로 소프트 제한을 설정합니다.
  • 메모리 사용 추이를 모니터링하고 미리 확장합니다.

시나리오 3: 포트 충돌

대표적인 증상:

  • Exit Code 1
  • 로그에 port is already allocated, address already in use, bind: address already in use가 표시됨

실제 사례:

월요일 아침에 출근해 docker-compose up을 실행했는데 Nginx 컨테이너가 시작되지 않았습니다. 오류 메시지는 다음과 같았습니다.

Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: address already in use

금요일 퇴근 전에 테스트한 로컬 Nginx를 끄는 것을 잊은 것이 원인이었습니다. 포트 80이 사용 중이었으니 새 컨테이너가 시작될 수 없었습니다.

진단 방법:

포트 사용 확인(Linux/macOS):

lsof -i :8080
netstat -tuln | grep 8080

포트 사용 확인(Windows):

netstat -ano | findstr 8080

다른 컨테이너의 포트 매핑 확인:

docker ps --format "table {{.Names}}\t{{.Ports}}"

해결책:

방법 1: 매핑 포트 변경

# 기존 명령
docker run -p 8080:8080 my-app

# 다른 포트로 변경
docker run -p 8081:8080 my-app

방법 2: 포트를 사용 중인 서비스 중지

# 프로세스 ID 확인
lsof -i :8080

# 프로세스 종료
kill -9 <PID>

방법 3: 다른 컨테이너가 사용 중이라면 먼저 중지

docker stop <conflicting_container>

특히 주의할 점: --network=host 모드를 사용하면 컨테이너가 호스트 네트워크를 직접 사용하므로 포트 충돌 가능성이 더 큽니다. 이 모드에서는 컨테이너 내부 포트가 호스트의 포트와 충돌하면 안 됩니다.

시나리오 4: 권한 부족

대표적인 증상:

  • Exit Code 1
  • 로그에 Permission denied, Operation not permitted, chown: changing ownership failed가 표시됨

실제 사례:

MongoDB 컨테이너를 배포하면서 데이터 디렉터리를 호스트에 마운트했는데 컨테이너가 계속 시작되지 않았습니다.

chown: changing ownership of '/data/db': Permission denied

호스트의 마운트 디렉터리는 root 사용자 소유였고, 컨테이너 안의 MongoDB 프로세스는 mongodb 사용자(UID 999)로 실행되어 해당 디렉터리에 쓸 권한이 없었습니다.

진단 방법:

호스트 디렉터리 권한 확인:

ls -la /host/data/path

컨테이너 내부 사용자 확인:

docker run -it my-app /bin/bash
whoami
id

SELinux 확인(CentOS/RHEL):

getenforce  # SELinux 상태 확인

해결책:

방법 1: 호스트 디렉터리 권한 조정

# 모든 사용자에게 읽기 및 쓰기 권한 부여(안전하지 않으므로 개발 환경에서만 사용)
chmod 777 /host/data/path

# 더 안전한 방법: 소유자 변경
chown -R 999:999 /host/data/path  # 999는 컨테이너 내부 사용자의 UID

방법 2: 특권 모드 사용(신중하게 사용)

docker run --privileged=true my-app

주의: 특권 모드는 컨테이너에 호스트의 거의 모든 권한을 부여하므로 보안 위험이 있습니다. 프로덕션 환경에서는 권장하지 않습니다.

방법 3: 실행 사용자 지정

docker run --user 1000:1000 my-app  # 호스트의 UID/GID 사용

방법 4: SELinux 문제 처리

# 방법 1: Z 레이블 추가(호스트 파일 레이블 변경)
docker run -v /host/path:/container/path:Z my-app

# 방법 2: z 레이블 추가(공유 레이블)
docker run -v /host/path:/container/path:z my-app

# 방법 3: SELinux 임시 비활성화(프로덕션 환경에서는 권장하지 않음)
setenforce 0

시나리오 5: 의존 서비스가 아직 준비되지 않음

대표적인 증상:

  • Exit Code 1
  • 로그에 데이터베이스 연결 실패, Redis 연결 시간 초과 등이 표시됨
  • Connection refused, ECONNREFUSED, could not connect to server

실제 사례:

docker-compose로 마이크로서비스 세트를 배포했는데 애플리케이션 컨테이너가 MySQL 데이터베이스에 의존하고 있었습니다. 두 컨테이너가 거의 동시에 시작됐지만 애플리케이션 컨테이너는 항상 실패했습니다.

Error: connect ECONNREFUSED 172.18.0.2:3306

문제는 MySQL 컨테이너는 시작됐지만 MySQL 서비스가 여전히 초기화 중이라 실제로 연결을 받을 준비가 되지 않았다는 점입니다. 애플리케이션 컨테이너가 너무 빨리 시작되어 연결에 실패한 뒤 종료됐습니다.

진단 방법:

의존 서비스가 시작되었는지 확인:

docker ps  # 의존 컨테이너가 실행 중인지 확인

네트워크 연결 확인:

docker exec my-app ping database
docker exec my-app telnet database 3306
docker exec my-app nc -zv database 3306

Docker 네트워크 설정 확인:

docker network ls
docker network inspect <network_name>

해결책:

방법 1: docker-compose의 상태 검사와 depends_on 사용

version: '3.8'
services:
  database:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  app:
    image: my-app
    depends_on:
      database:
        condition: service_healthy  # 데이터베이스 상태 검사가 통과할 때까지 대기

방법 2: 애플리케이션 계층에 재시도 메커니즘 추가

애플리케이션 코드에 연결 재시도 로직을 추가합니다.

// Node.js 예제
async function connectWithRetry(maxRetries = 5) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      await db.connect();
      console.log('Database connected');
      return;
    } catch (err) {
      console.log(`Connection failed, retrying... (${i+1}/${maxRetries})`);
      await new Promise(resolve => setTimeout(resolve, 5000));
    }
  }
  throw new Error('Failed to connect to database');
}

방법 3: 시작 대기 스크립트 사용

컨테이너 시작 전에 wait-for-it.sh 같은 대기 스크립트를 추가할 수 있습니다.

# Dockerfile에서
COPY wait-for-it.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/wait-for-it.sh

# 시작할 때 사용
CMD ["wait-for-it.sh", "database:3306", "--", "node", "app.js"]

방법 4: 재시작 정책 설정

컨테이너가 실패하면 자동으로 재시도하게 합니다.

docker run --restart=on-failure:3 my-app  # 실패 후 최대 3회 재시작

docker-compose에서는 다음과 같이 설정합니다.

services:
  app:
    restart: on-failure

상태 검사, 애플리케이션 계층 재시도, 재시작 정책을 함께 사용하는 등 여러 방법을 조합하면 삼중으로 보호할 수 있습니다.

예방 조치와 권장 사례

앞에서는 문제가 발생한 뒤 해결하는 방법을 설명했습니다. 하지만 몇 가지 메커니즘을 처음부터 제대로 설정하면 많은 문제를 아예 피하거나 발생하더라도 자동으로 복구할 수 있습니다.

상태 검사(HEALTHCHECK) 설정

상태 검사는 Docker 컨테이너의 자체 점검 메커니즘입니다. Docker는 정기적으로 검사 명령을 실행해 프로세스가 살아 있는지만 보는 것이 아니라 컨테이너가 실제로 정상 작동하는지 판단할 수 있습니다.

Dockerfile에서 다음과 같이 설정합니다.

FROM nginx:alpine

# 30초마다 검사하고 3초 안에 응답이 없거나 3회 연속 실패하면 unhealthy로 표시
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD curl -f http://localhost/ || exit 1

Web 서비스는 HTTP 엔드포인트를 확인할 수 있습니다.

HEALTHCHECK --interval=30s --timeout=5s --start-period=40s \
  CMD curl -f http://localhost:8080/health || exit 1

데이터베이스는 전용 명령을 사용할 수 있습니다.

# MySQL
HEALTHCHECK CMD mysqladmin ping -h localhost || exit 1

# PostgreSQL
HEALTHCHECK CMD pg_isready -U postgres || exit 1

# Redis
HEALTHCHECK CMD redis-cli ping || exit 1

상태 검사의 장점:

  • Kubernetes/Swarm 같은 오케스트레이션 도구가 상태에 따라 컨테이너를 자동으로 재시작하거나 다시 배치합니다.
  • docker-compose의 depends_on이 서비스가 실제로 정상 상태가 될 때까지 기다린 뒤 의존 컨테이너를 시작할 수 있습니다.
  • 모니터링 시스템이 상태를 바탕으로 경보를 보낼 수 있습니다.

상태 확인:

docker ps  # STATUS 열에 상태가 표시됨
docker inspect --format='{{.State.Health.Status}}' <container_id>

재시작 정책 설정

재시작 정책을 사용하면 컨테이너가 실패한 뒤 자동으로 복구되므로 한밤중에 일어나 직접 재시작할 필요가 없습니다.

Docker는 네 가지 재시작 정책을 제공합니다.

no(기본값): 자동으로 재시작하지 않음

docker run --restart=no my-app

한 번 실행한 뒤 끝나는 작업용 컨테이너에 적합합니다.

on-failure[:max-retries]: 비정상 종료일 때만 재시작

docker run --restart=on-failure:5 my-app  # 최대 5회 재시도

오류가 날 수 있지만 무한 재시도를 원하지 않는 서비스에 적합합니다. Exit Code가 0이 아닐 때만 재시작한다는 점에 주의하세요.

always: 항상 재시작

docker run --restart=always my-app

Web 서버, API 서비스처럼 장시간 실행되는 서비스에 적합합니다. 직접 stop하더라도 다음에 Docker Daemon이 재시작되면 컨테이너가 자동으로 시작됩니다.

unless-stopped: 직접 중지하지 않는 한 항상 재시작

docker run --restart=unless-stopped my-app

always와 비슷하지만 docker stop을 직접 실행했다면 다음에 Docker Daemon이 재시작되어도 이 컨테이너는 자동으로 시작되지 않습니다. 일정한 제어권을 유지할 수 있어 제가 가장 자주 사용하는 정책입니다.

중요한 안내:

  1. 10초 규칙: 컨테이너를 처음 시작한 뒤 최소 10초 동안 실행되어야 재시작 정책이 적용됩니다. 설정 오류로 무한 재시작되어 시스템 리소스를 모두 사용하는 일을 방지하기 위한 규칙입니다.
  2. 무한 재시작의 함정: 포트 충돌 같은 설정 오류로 컨테이너가 계속 재시작되면 로그가 폭발적으로 늘어날 수 있습니다. 로그 로테이션도 함께 설정하세요.

실행 중인 컨테이너의 정책도 동적으로 바꿀 수 있습니다.

docker update --restart=unless-stopped <container_id>

docker-compose에서는 다음과 같이 설정합니다.

services:
  web:
    image: nginx
    restart: unless-stopped  # 프로덕션 환경에 권장

  worker:
    image: my-worker
    restart: on-failure  # 실패할 수 있지만 무한 재시도는 원하지 않음

로그 관리: 디스크가 가득 차는 문제 방지

Docker는 기본적으로 모든 컨테이너 로그를 JSON 파일에 저장합니다. 시간이 지나면 이 로그 파일들이 수십 GB의 디스크 공간을 차지할 수 있습니다. 저도 프로덕션 서버의 디스크가 Docker 로그 때문에 가득 찬 사고를 겪은 적이 있습니다.

로그 로테이션 설정(권장):
/etc/docker/daemon.json을 생성하거나 편집합니다.

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",    // 로그 파일 하나의 최대 크기 10MB
    "max-file": "3"       // 로그 파일 최대 3개 보관
  }
}

변경 후 Docker를 재시작합니다.

sudo systemctl restart docker

이제 컨테이너마다 최대 30MB(10MB × 3)의 로그 공간만 사용하고 오래된 로그는 자동으로 삭제됩니다.

개별 컨테이너 설정:

docker run --log-opt max-size=10m --log-opt max-file=3 my-app

docker-compose 설정:

services:
  app:
    image: my-app
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

다른 로그 드라이버 선택지:

  • syslog: 시스템 로그로 전송
  • journald: systemd의 journal 사용
  • fluentd: 중앙 집중식 관리를 위해 Fluentd로 전송
  • none: 로그를 기록하지 않음(권장하지 않음)

컨테이너 로그 파일 위치와 크기 확인:

docker inspect --format='{{.LogPath}}' <container_id>
du -h $(docker inspect --format='{{.LogPath}}' <container_id>)

모니터링과 경보: 문제를 미리 발견하기

컨테이너가 멈춘 뒤에야 알아서는 안 됩니다. 미리 모니터링하면 많은 프로덕션 사고를 피할 수 있습니다.

기본 모니터링: docker stats

docker stats  # 모든 컨테이너의 리소스 사용량을 실시간 표시
docker stats <container_id>  # 특정 컨테이너 모니터링

이 명령은 CPU, 메모리, 네트워크 IO, 디스크 IO의 실시간 데이터를 보여 줍니다. 메모리 사용량이 계속 증가한다면 메모리 누수일 수 있으므로 미리 조치해야 합니다.

프로덕션 환경: Prometheus + Grafana

더 전문적인 방법은 Prometheus로 메트릭을 수집하고 Grafana로 시각화하는 것입니다.

# docker-compose.yml
services:
  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana
    ports:
      - "3000:3000"

  cadvisor:  # 컨테이너 메트릭 수집
    image: google/cadvisor
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    ports:
      - "8080:8080"

메모리 사용량이 80%를 넘거나 컨테이너 재시작 횟수가 너무 많은 경우 자동으로 알림을 보내도록 경보 규칙을 설정합니다.

간단하고 직접적인 방법: 예약 스크립트

Prometheus가 너무 복잡하다면 간단한 스크립트를 작성할 수 있습니다.

#!/bin/bash
# check-containers.sh

# Exited 상태의 컨테이너가 있는지 확인
EXITED=$(docker ps -a -f "status=exited" --format "{{.Names}}")

if [ -n "$EXITED" ]; then
  echo "Warning: The following containers are exited:"
  echo "$EXITED"
  # 여기에서 이메일이나 푸시 알림을 보낼 수 있음
fi

crontab에 추가해 5분마다 실행합니다.

*/5 * * * * /path/to/check-containers.sh

프로덕션 환경 설정 체크리스트

마지막으로 프로덕션 환경 설정 체크리스트를 제공합니다. 이 내용을 따르면 큰 문제가 생길 가능성을 상당히 줄일 수 있습니다.

version: '3.8'
services:
  web:
    image: my-web-app:latest

    # 재시작 정책
    restart: unless-stopped

    # 리소스 제한
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 1G
        reservations:
          memory: 512M

    # 상태 검사
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 40s

    # 로그 관리
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

    # 환경 변수(민감한 정보에는 secrets 사용)
    environment:
      - NODE_ENV=production

    # 포트 매핑
    ports:
      - "8080:8080"

    # 의존 관계
    depends_on:
      database:
        condition: service_healthy

  database:
    image: postgres:14
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - db-data:/var/lib/postgresql/data
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  db-data:

이 설정을 사용하면 컨테이너가 멈춰도 자동으로 재시작되고, 리소스가 제한을 넘지 않으며, 로그가 디스크를 가득 채우지 않고, 모니터링으로 문제를 미리 경고할 수 있습니다. 이제 편안하게 잠들 수 있습니다.

결론

긴 내용을 살펴봤지만 가장 기억해야 할 점은 이것입니다. Docker 컨테이너 시작 실패 자체는 두렵지 않습니다. 체계적인 진단 방법이 없는 것이 더 큰 문제입니다.

이 글의 핵심 내용을 다시 정리하겠습니다.

종료 코드 이해하기: 137을 보면 메모리 문제를, 1을 보면 설정이나 의존성 문제를 먼저 떠올리세요. 종료 코드는 Docker가 남긴 단서이므로 무시해서는 안 됩니다.

4단계 진단법:

  1. 컨테이너 상태 확인(docker ps -a)
  2. 로그 확인(docker logs)
  3. 설정 확인(docker inspect)
  4. 대화형 검증(docker run -it)

문제의 90% 이상은 2단계에서 해결됩니다.

자주 발생하는 5가지 시나리오: 설정 오류, 메모리 부족, 포트 충돌, 권한 문제, 준비되지 않은 의존 서비스입니다. 이 다섯 가지의 진단 방법을 기억하면 일상적인 문제에는 충분히 대응할 수 있습니다.

미리 예방하기: 상태 검사 설정, 재시작 정책 지정, 로그 관리, 모니터링을 적용하세요. 이런 메커니즘은 컨테이너를 더 안정적으로 만들고 문제가 발생했을 때 자동 복구를 돕습니다.

마지막으로 빠른 진단 체크리스트를 제공합니다. 캡처해 두고 활용해 보세요.

Docker 컨테이너 시작 실패 진단 체크리스트

□ 1단계: docker ps -a로 컨테이너 상태와 종료 코드 확인
□ 2단계: docker logs <container_id>로 상세 로그 확인
□ 3단계: docker inspect <container_id>로 설정 확인
□ 4단계: docker run -it <image>로 대화형 검증

자주 발생하는 문제 빠르게 찾기:
- Exit Code 1 + "No such file" → 마운트 경로와 설정 파일 확인
- Exit Code 1 + "port already allocated" → 포트 충돌 확인
- Exit Code 1 + "Permission denied" → 파일 권한과 SELinux 확인
- Exit Code 1 + "Connection refused" → 의존 서비스가 준비되었는지 확인
- Exit Code 137 + OOMKilled=true → 메모리 제한 늘리기
- Exit Code 127 → CMD/ENTRYPOINT 경로가 올바른지 확인

예방 조치:
□ HEALTHCHECK 설정
□ restart 정책 설정(unless-stopped 권장)
□ 로그 로테이션 설정(max-size + max-file)
□ 리소스 제한(-m 메모리 제한)
□ 모니터링과 경보(docker stats 또는 Prometheus)

이 글이 도움이 되었다면 저장해 두었다가 다음에 컨테이너 시작 문제가 생겼을 때 체크리스트로 활용해 보세요. 특별한 컨테이너 시작 문제를 겪었다면 댓글로 공유해 주세요. 다른 사람에게도 도움이 될 수 있습니다.

여러분의 컨테이너가 언제나 Up and Running 상태를 유지하고, 금요일 밤에 컨테이너 장애 경보를 받는 일이 없기를 바랍니다!

Docker 컨테이너 시작 실패 완전 진단 절차

Exit Code 137과 1의 의미, 4단계 진단법, 자주 발생하는 5가지 실패 시나리오의 해결책을 다루는 체계적인 문제 해결 절차입니다.

⏱️ Estimated time: 30 min

  1. 1

    Step 1: 종료 코드의 의미와 문제의 심각성 이해하기

    종료 코드의 의미:
    • Exit Code 0: 작업을 완료한 정상 종료
    • Exit Code 1: 애플리케이션 오류 또는 시작 명령 실패(가장 흔함)
    • Exit Code 137: 메모리 부족으로 OOM Killer가 종료
    • Exit Code 127: 명령을 찾을 수 없음
    • Exit Code 139: 세그멘테이션 오류(C/C++ 프로그램)

    문제의 심각성:
    • 프로덕션 환경의 핵심 서비스 컨테이너 4개가 모두 Exited 상태로 변경됨
    • 컨테이너가 시작 직후 종료되므로 체계적인 진단 절차가 필요함

    자주 발생하는 문제 빠르게 찾기:
    • Exit Code 1 + 'No such file' → 마운트 경로와 설정 파일 확인
    • Exit Code 1 + 'port already allocated' → 포트 충돌 확인
    • Exit Code 1 + 'Permission denied' → 파일 권한과 SELinux 확인
    • Exit Code 1 + 'Connection refused' → 의존 서비스가 준비되었는지 확인
    • Exit Code 137 + OOMKilled=true → 메모리 제한 늘리기
  2. 2

    Step 2: 4단계 진단법

    4단계 진단법:

    1단계: 컨테이너 상태와 종료 코드 확인
    • docker ps -a로 컨테이너 상태와 종료 코드 확인
    • State와 Exit Code 열에 주목

    2단계: 상세 로그 확인
    • docker logs <container_id>로 상세 로그 확인
    • docker logs --tail 50 <container_id>로 최근 50줄 확인
    • docker logs -f <container_id>로 로그를 실시간 추적

    3단계: 설정 확인
    • docker inspect <container_id>로 설정 확인
    • Cmd, Entrypoint, Env 등의 필드 확인

    4단계: 대화형 검증
    • docker run -it <image>로 대화형 검증
    • 시작 명령을 직접 실행하고 오류 메시지 관찰
  3. 3

    Step 3: 자주 발생하는 5가지 실패 시나리오와 해결책

    자주 발생하는 5가지 실패 시나리오:

    시나리오 1: 시작 명령 오류
    • CMD/ENTRYPOINT 설정 오류
    • 해결책: 시작 명령을 수정하고 경로와 인수 확인

    시나리오 2: 설정 파일 오류
    • 경로 또는 형식 오류
    • 해결책: 설정 파일을 수정하고 경로와 형식 검증

    시나리오 3: 의존 서비스가 아직 준비되지 않음
    • 데이터베이스가 시작되지 않음
    • 해결책: 의존 서비스가 준비될 때까지 대기(depends_on + healthcheck 사용)

    시나리오 4: 메모리 부족
    • OOM Killer가 프로세스를 종료
    • 해결책: 메모리 제한(--memory)을 늘리거나 애플리케이션의 메모리 사용량 최적화

    시나리오 5: 포트 충돌
    • 포트가 이미 사용 중
    • 해결책: 포트 매핑(-p 8081:80)을 바꾸거나 포트를 점유한 프로세스 중지

    권장 사례:
    • docker-compose로 상태 검사 설정
    • depends_on + healthcheck로 의존 서비스가 준비된 뒤 시작되도록 보장
    • 적절한 리소스 제한(메모리, CPU) 설정

FAQ

Docker 컨테이너가 시작 직후 종료되는 이유는 무엇인가요?
컨테이너가 시작 직후 종료되는 일반적인 원인은 다음과 같습니다.

종료 코드의 의미:
• Exit Code 1: 애플리케이션 오류 또는 시작 명령 실패(가장 흔함)
• Exit Code 137: 메모리 부족으로 OOM Killer가 종료
• Exit Code 0: 작업을 완료한 정상 종료

자주 발생하는 문제 빠르게 찾기:
• Exit Code 1 + 'No such file' → 마운트 경로와 설정 파일 확인
• Exit Code 1 + 'port already allocated' → 포트 충돌 확인
• Exit Code 1 + 'Permission denied' → 파일 권한과 SELinux 확인
• Exit Code 1 + 'Connection refused' → 의존 서비스가 준비되었는지 확인
• Exit Code 137 + OOMKilled=true → 메모리 제한 늘리기

진단 방법: 4단계 진단법(로그 확인 → 종료 코드 확인 → 시작 명령 확인 → 리소스 제한 확인)을 사용합니다.
Docker 컨테이너 시작 실패는 어떻게 진단하나요?
4단계 진단법:
1) 컨테이너 로그 확인: docker logs container-name
2) 종료 코드 확인: docker ps -a
3) 시작 명령 확인: docker inspect container-name
4) 리소스 제한 확인: docker stats

상세 단계:
• 1단계: docker ps -a로 컨테이너 상태와 종료 코드 확인
• 2단계: docker logs <container_id>로 상세 로그 확인
• 3단계: docker inspect <container_id>로 설정 확인
• 4단계: docker run -it <image>로 대화형 검증
Exit Code 1과 Exit Code 137은 어떻게 다른가요?
종료 코드의 의미:
• Exit Code 1: 애플리케이션 오류 또는 시작 명령 실패
• Exit Code 137: 메모리 부족으로 OOM Killer가 종료
• Exit Code 0: 정상 종료
• 기타 종료 코드: 구체적인 애플리케이션에 따라 다름

Exit Code 1의 일반적인 원인:
• 시작 명령 오류(CMD/ENTRYPOINT 설정 오류)
• 설정 파일 오류(경로 또는 형식 오류)
• 의존 서비스가 아직 준비되지 않음(데이터베이스가 시작되지 않음)
• 포트 충돌(포트가 이미 사용 중)

Exit Code 137의 일반적인 원인:
• 메모리 부족(OOM Killer)
• 메모리 제한(--memory)을 늘려야 함
컨테이너 시작 실패의 일반적인 문제는 어떻게 해결하나요?
자주 발생하는 5가지 실패 시나리오:
1) 시작 명령 오류(CMD/ENTRYPOINT 설정 오류)
2) 설정 파일 오류(경로 또는 형식 오류)
3) 의존 서비스가 아직 준비되지 않음(데이터베이스가 시작되지 않음)
4) 메모리 부족(OOM Killer)
5) 포트 충돌(포트가 이미 사용 중)

해결책:
• 시작 명령 수정
• 설정 파일 수정
• 의존 서비스가 준비될 때까지 대기(depends_on + healthcheck 사용)
• 메모리 제한(--memory) 늘리기
• 포트 매핑(-p 8081:80) 변경
• docker-compose로 상태 검사 설정

5분 읽기 · 게시일: 2025년 12월 18일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog