본문으로 바로가기
TaeyoungKim.dev

Docker 컨테이너가 바로 종료되는 이유: Dockerfile CMD에 npm run build를 넣었다면

클라우드작성 약 4분 읽기TaeyoungKim
LinkedInX

웹앱 이미지는 만들었는데 컨테이너가 켜졌다가 바로 꺼진다. 포트도 EXPOSE 3000이라고 써 뒀으니 더 억울하다. Dockerfile의 마지막 줄이 CMD ["npm", "run", "build"]라면, 문제는 포트보다 그 명령이 언제 실행되고 언제 끝나는지에 있을 수 있다.

Dockerfile의 CMD에 build를 넣으면 왜 컨테이너가 종료될까?

docker build는 이미지를 만든다. 그 과정에서 CMD 줄은 실행되지 않고, 나중에 컨테이너를 시작할 때 사용할 기본 명령으로 이미지에 기록된다. 반면 RUN은 이미지를 만드는 중에 실행된다. Dockerfile의 RUN·CMD·EXPOSE 정의를 나란히 보면 이 시점 차이가 명확하다.

dockerfile
EXPOSE 3000
CMD ["npm", "run", "build"]

이 구성으로 컨테이너를 시작하면 npm run build가 실행된다. 빌드 스크립트가 산출물을 만들고 정상 종료하면 주 프로세스도 끝난다. 종료 코드 0은 빌드 성공일 수 있지만, 웹 서버가 계속 실행 중이라는 뜻은 아니다. 빌드 스크립트가 실패하면 0이 아닌 코드로 종료될 수도 있다. 양쪽 모두 서비스는 뜨지 않는다.

그림의 왼쪽은 CMD build가 작업을 마치면 끝나는 경로다. 오른쪽은 RUN build로 산출물을 준비한 뒤 CMD start가 서버를 실행하는 경로다. 컨테이너는 이미지를 보관하는 상자가 아니라, 실행할 주 프로세스가 있는 동안 살아 있는 실행 단위다. Docker의 컨테이너 실행 설명도 분리 모드에서 주 프로세스가 끝나면 컨테이너가 종료된다고 밝힌다.

RUN build와 CMD start는 어떻게 나눌까?

웹 서버를 띄우려는 프로젝트라면 빌드와 실행 명령의 역할을 분리한다. 다음은 package-lock.json과 package.json의 build·start 스크립트가 있고, start가 종료되지 않는 서버를 실행한다는 전제의 예시다.

dockerfile
FROM node:22
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "start"]

RUN npm run build는 이미지 생성 중 산출물을 준비한다. CMD의 npm run start는 컨테이너 시작 시 서버를 실행한다. 따라서 start가 실제로 어떤 명령인지 먼저 package.json에서 확인해야 한다. start가 짧은 작업만 하고 끝난다면 이름만 바꿔도 똑같이 종료된다. 프로젝트에 따라 빌드 산출물 경로, 런타임 의존성, 환경 변수도 맞아야 한다.

EXPOSE 3000은 '이 이미지의 앱이 3000번 포트를 사용할 예정'이라는 표시이지 서버를 켜거나 호스트 포트를 여는 명령이 아니다. 앱이 컨테이너 안에서 0.0.0.0:3000에 바인딩하고 있다면, 로컬 확인 때 -p 127.0.0.1:3000:3000처럼 별도로 포트를 연결한다. 외부 공개가 필요하지 않은 실습에서는 로컬 주소에만 묶어 두는 편이 안전하다.

컨테이너가 꺼졌을 때 무엇부터 확인할까?

바로 Dockerfile을 고치기보다 상태 → 종료 코드 → 로그 → 실행 스크립트 순서로 좁혀 보자. 아래 demo-web은 실행할 때 붙인 컨테이너 이름으로 바꿔 쓰면 된다.

bash
docker ps -a --filter name=demo-web
docker inspect demo-web --format '{{.State.ExitCode}}'
docker logs demo-web

Exited (0)이고 로그 끝에 빌드 완료가 보이면 CMD가 서버 대신 빌드를 실행했는지 확인한다. 0이 아닌 종료 코드라면 먼저 로그의 의존성·파일 경로·환경 변수 오류를 본다. 예를 들어 npm ci 없이 패키지 파일만 복사한 Dockerfile이라면 빌드 실행에 필요한 의존성이 없어 다른 이유로 실패할 수 있다. 종료 코드 하나만 보고 'CMD가 원인'이라고 단정하지 말자.

운영에서는 포트가 열리는지만이 아니라 서버 프로세스가 계속 살아 있는지와 준비 상태까지 확인한다. 빌드를 이미지 생성 단계에 옮겨도 시작 스크립트가 잘못됐거나 서버가 127.0.0.1에만 묶이면 접속 문제는 남는다. 그때는 컨테이너 로그와 앱의 바인딩 주소를 함께 봐야 한다.

핵심 요약

RUN은 이미지 생성 중, CMD는 컨테이너 시작 시 실행된다. CMD에 일회성 빌드 명령을 넣으면 성공해도 컨테이너가 종료될 수 있다. 웹앱이라면 빌드와 서버 시작을 분리하고, 종료되었을 때는 상태·코드·로그·package.json의 실제 스크립트를 차례로 확인하자.

작성자

TaeyoungKim

기초 개념을 구현과 검증, 실제 운영 판단까지 연결해 기록합니다.

#Dockerfile#Docker CMD#Docker RUN#컨테이너 종료#npm run build

함께 읽으면 좋은 글