컨테이너의 라이프사이클(Container Lifecycle)

Series: Docker

Dockercontains 3

들어가며

Docker를 사용하다 보면 run, start, stop, restart, pause, rm 같은 명령어를 자연스럽게 사용하게 된다. 처음 Docker를 사용할 때는 이 명령어들을 단순히 '컨테이너를 실행한다', '컨테이너를 멈춘다', '컨테이너를 삭제한다' 정도로 이해해도 크게 문제는 없다. 하지만 Docker를 조금 더 자주 사용하다 보면 예상과 다른 상황을 만나게 된다. 예를 들어 다음과 같은 경험을 한 번쯤 하게 된다.

docker run ubuntu

명령어는 정상적으로 실행된 것 같은데, docker ps를 입력하면 컨테이너가 보이지 않는다.

docker ps

실행 중인 컨테이너 목록이 비어 있다. 그런데 docker ps -a를 입력하면 방금 실행한 컨테이너가 Exited 상태로 남아 있다.

docker ps -a

또 어떤 경우에는 docker stop으로 컨테이너를 멈췄는데 컨테이너가 사라지지 않는다. 반대로 docker rm을 실행하면 그제야 컨테이너가 목록에서 사라진다. 처음에는 이 차이가 헷갈릴 수 있다. 왜냐하면 컨테이너를 프로그램처럼 생각하면 실행과 종료만 있을 것 같지만, 실제 Docker 컨테이너는 여러 상태를 거치며 관리되기 때문이다. 컨테이너는 단순히 실행되는 대상이 아니다. 컨테이너는 이미지로부터 생성되고, 실행되고, 일시 정지되고, 종료되고, 다시 시작될 수 있으며, 마지막에는 삭제된다. 이 흐름을 컨테이너의 라이프사이클이라고 한다. 이번 글에서는 Docker 컨테이너가 어떤 상태를 가지는지, 각 상태가 어떤 의미를 가지는지, 그리고 docker run, docker start, docker stop, docker rm 같은 명령어가 실제로 어떤 상태 전이를 일으키는지 정리해보겠다.

1. 이미지와 컨테이너는 다르다

컨테이너 라이프사이클을 이해하기 전에 먼저 이미지와 컨테이너를 구분해야 한다. Docker를 처음 배울 때 가장 많이 헷갈리는 지점이기도 하다. 이미지는 실행 환경을 정의한 정적인 결과물이다. 애플리케이션 코드, 런타임, 라이브러리, 환경 설정, 기본 실행 명령 등이 이미지 안에 들어 있다. 하지만 이미지는 그 자체로 실행 중인 프로세스가 아니다. 이미지는 실행 가능한 설계도에 가깝다. 반면 컨테이너는 이미지를 기반으로 만들어진 실행 단위이다. 이미지가 클래스라면 컨테이너는 인스턴스라고 볼 수 있다. 또는 이미지를 붕어빵 틀, 컨테이너를 실제로 찍어낸 붕어빵이라고 생각해도 된다. 같은 이미지에서 여러 개의 컨테이너를 만들 수 있고, 각 컨테이너는 서로 다른 이름, 네트워크 설정, 파일 시스템 변경 사항, 실행 상태를 가질 수 있다.

이미지
→ 실행 환경을 담은 정적인 템플릿

컨테이너
→ 이미지를 기반으로 생성된 실행 가능한 객체

예를 들어 nginx:alpine 이미지를 기반으로 컨테이너를 두 개 만들 수 있다.

docker run -d --name web-1 nginx:alpine
docker run -d --name web-2 nginx:alpine

두 컨테이너는 같은 이미지를 사용하지만 서로 다른 컨테이너이다. web-1을 중지해도 web-2가 같이 중지되지 않는다. web-1을 삭제해도 web-2nginx:alpine 이미지가 함께 삭제되지 않는다.

flowchart TB
    I[nginx:alpine 이미지] --> C1[web-1 컨테이너]
    I --> C2[web-2 컨테이너]
    I --> C3[web-3 컨테이너]

이 차이를 이해해야 docker rmdocker rmi의 차이도 자연스럽게 이해할 수 있다.

docker rm web-1

이 명령어는 컨테이너를 삭제한다.

docker rmi nginx:alpine

이 명령어는 이미지를 삭제한다. 즉, 컨테이너 라이프사이클은 이미지의 라이프사이클이 아니다. 이미지는 컨테이너를 만들기 위한 기준이고, 컨테이너는 그 이미지 위에서 실제 상태를 가지며 움직이는 대상이다.

2. 컨테이너 상태를 하나의 흐름으로 보기

Docker 컨테이너는 여러 상태를 가진다. 우리가 자주 만나는 상태는 Created, Running, Paused, Exited 정도이다. Docker의 docker ps --filter status=... 기준으로는 created, running, paused, exited뿐 아니라 restarting, removing, dead 같은 상태도 확인할 수 있다. 여기서 Stopped라는 표현은 일상적으로 많이 쓰지만, Docker CLI에서는 보통 Exited 상태로 표시된다. (Docker Documentation) 컨테이너의 기본 흐름을 단순하게 그리면 다음과 같다.

stateDiagram-v2
    [*] --> Image

    Image --> Created: docker create
    Image --> Running: docker run

    Created --> Running: docker start

    Running --> Paused: docker pause
    Paused --> Running: docker unpause

    Running --> Exited: docker stop
    Running --> Exited: main process exit

    Exited --> Running: docker start

    Created --> Removed: docker rm
    Exited --> Removed: docker rm
    Running --> Removed: docker rm -f

    Removed --> [*]

이 흐름에서 중요한 점은 각 명령어가 단순히 독립적으로 동작하는 것이 아니라 컨테이너의 상태를 다른 상태로 이동시킨다는 것이다. docker create는 이미지를 기반으로 컨테이너를 만들지만 실행하지 않는다. docker start는 이미 만들어진 컨테이너를 실행한다. docker run은 컨테이너를 생성하고 바로 실행한다. docker stop은 실행 중인 컨테이너를 종료 상태로 전환한다. docker rm은 컨테이너 자체를 삭제한다. 이렇게 보면 Docker 명령어를 외우는 방식이 조금 달라진다. 단순히 '이 명령어는 실행', '이 명령어는 중지'라고 외우는 것이 아니라, '이 명령어는 어떤 상태에서 어떤 상태로 이동시키는가?'를 기준으로 이해할 수 있다.

3. 컨테이너 생성 상태(Created)

컨테이너의 시작은 이미지다. 하지만 이미지는 정적인 결과물이기 때문에 그 자체로 실행되지 않는다. 이미지를 기반으로 컨테이너 객체가 만들어지는 순간부터 컨테이너의 라이프사이클이 시작된다. 컨테이너를 생성만 하고 실행하지 않으려면 docker create 명령어를 사용할 수 있다.

docker create --name lifecycle-demo nginx:alpine

이 명령어를 실행하면 Docker는 nginx:alpine 이미지를 기반으로 새로운 컨테이너를 만든다. 하지만 아직 컨테이너 내부의 애플리케이션은 실행되지 않는다. Docker 공식 문서에서도 docker create는 지정한 이미지로부터 새 컨테이너를 만들지만 시작하지는 않는다고 설명한다. 이때 Docker daemon은 이미지 위에 writable container layer를 만들고, 지정된 명령을 실행할 준비를 한다. 생성 직후의 초기 상태는 created이다(Docker Documentation). 생성된 컨테이너는 기본 docker ps 명령으로는 보이지 않는다.

docker ps

docker ps는 기본적으로 실행 중인 컨테이너만 보여주기 때문이다. 생성되었지만 실행되지 않은 컨테이너까지 확인하려면 -a 옵션을 사용해야 한다.

docker ps -a

특정 상태만 보고 싶다면 다음처럼 필터링할 수 있다.

docker ps -a --filter status=created

Created 상태의 컨테이너는 존재하지만 아직 움직이지 않는 상태라고 볼 수 있다. 비유하자면 공연장을 준비해두고 무대 장치와 조명을 모두 설치했지만, 아직 배우가 무대에 올라오지 않은 상태와 비슷하다. 컨테이너의 파일 시스템 레이어, 이름, 환경 변수, 네트워크 설정 등은 준비되어 있지만 메인 프로세스는 아직 실행되지 않았다. 이 상태가 중요한 이유는 docker run이 내부적으로 생성과 실행을 함께 수행하기 때문이다. 평소에는 docker run을 자주 사용하기 때문에 Created 상태를 직접 볼 일이 많지 않다. 하지만 Docker의 동작을 정확히 이해하려면 생성과 실행이 분리된 단계라는 점을 알아야 한다.

flowchart LR
    A[Image] --> B[docker create]
    B --> C[Created Container]
    C --> D[docker start]
    D --> E[Running Container]

docker run은 위 흐름에서 docker createdocker start를 한 번에 수행하는 명령어라고 볼 수 있다.

flowchart LR
    A[Image] --> B[docker run]
    B --> C[Created]
    C --> D[Running]

4. 컨테이너 실행 상태(Running)

컨테이너가 실제로 실행되면 Running 상태가 된다. 이 시점부터 컨테이너 내부에서는 이미지에 정의된 명령어가 실행된다. Dockerfile 기준으로 보면 보통 ENTRYPOINTCMD가 이 실행 흐름에 관여한다. 예를 들어 다음 명령어를 실행해보자.

docker run -d --name web nginx:alpine

nginx:alpine 이미지는 Nginx 서버를 실행하도록 구성되어 있다. 따라서 컨테이너가 시작되면 Nginx 프로세스가 실행되고, 이 프로세스가 살아 있는 동안 컨테이너도 Running 상태로 유지된다. Docker의 docker run 명령은 이미지 뒤에 [COMMAND] [ARG...]를 붙여 컨테이너 시작 시 실행할 명령과 인자를 지정할 수 있다. 이미지에 ENTRYPOINT가 있으면 CMD나 사용자가 넘긴 command가 그 entrypoint의 인자로 붙는 방식으로 실행된다. (Docker Documentation)

docker run alpine echo "hello container"

이 명령은 alpine 이미지를 기반으로 컨테이너를 만들고, 그 안에서 echo "hello container"를 실행한다. 그런데 이 컨테이너는 오래 실행되지 않는다. echo 명령은 메시지를 출력한 뒤 바로 종료되기 때문이다. 실제로 확인해보면 컨테이너가 실행 중 목록에는 보이지 않을 수 있다.

docker ps

대신 전체 컨테이너 목록을 보면 종료된 상태로 남아 있다.

docker ps -a

출력은 대략 다음과 비슷하다.

CONTAINER ID   IMAGE     COMMAND                  STATUS
abc123         alpine    "echo 'hello container'" Exited (0) ...

여기서 중요한 개념이 나온다. 컨테이너는 VM처럼 독립된 운영체제 하나를 계속 켜두는 구조가 아니다. 컨테이너는 격리된 환경에서 실행되는 프로세스에 가깝다. 일반적인 Linux 컨테이너에서는 컨테이너 내부의 메인 프로세스가 PID 1로 실행되고, 이 프로세스가 컨테이너의 생명주기를 결정한다.

flowchart TB
    C[Container] --> P1[Main Process PID 1]
    P1 --> P2[Child Process]
    P1 --> P3[Child Process]

컨테이너 내부에 여러 프로세스가 있을 수는 있다. 하지만 Docker 관점에서 컨테이너의 생명은 메인 프로세스와 강하게 연결된다. 메인 프로세스가 계속 실행 중이면 컨테이너도 Running 상태로 유지된다. 메인 프로세스가 종료되면 컨테이너도 종료된다. 예를 들어 다음 컨테이너는 바로 종료된다.

docker run --name short alpine sh -c "echo start && echo end"

반면 다음 컨테이너는 계속 실행된다.

docker run -d --name long alpine sh -c "while true; do sleep 60; done"

첫 번째 컨테이너는 실행할 작업이 끝나자마자 종료된다. 두 번째 컨테이너는 무한 루프를 돌며 sleep을 반복하기 때문에 계속 Running 상태로 남아 있다. 물론 실제 운영 환경에서 단순히 컨테이너를 살려두기 위해 무한 루프를 사용하는 것은 좋은 방식이 아니다. 중요한 것은 컨테이너가 유지되려면 foreground에서 살아 있는 메인 프로세스가 필요하다는 점이다. 웹 서버 컨테이너라면 웹 서버 프로세스가 foreground로 실행되어야 하고, 워커 컨테이너라면 워커 프로세스가 계속 실행되어야 한다.

5. docker rundocker start는 다르다

Docker를 사용할 때 많이 헷갈리는 명령어가 runstart이다. 둘 다 컨테이너를 실행하는 것처럼 보이지만 실제 의미는 다르다. docker run은 새 컨테이너를 생성하고 실행한다.

docker run -d --name app nginx:alpine

반면 docker start는 이미 존재하는 컨테이너를 다시 실행한다.

docker start app

이 차이는 매우 중요하다. 같은 이미지로 docker run을 여러 번 실행하면 매번 새로운 컨테이너가 만들어진다.

docker run -d nginx:alpine
docker run -d nginx:alpine
docker run -d nginx:alpine

위 명령을 실행하면 nginx:alpine 이미지를 사용하는 컨테이너가 세 개 만들어진다. 같은 이미지를 사용하지만 각각 다른 컨테이너이다. 반면 이미 존재하는 컨테이너를 다시 실행하고 싶다면 docker run이 아니라 docker start를 사용해야 한다.

docker start app

이 차이를 흐름으로 보면 다음과 같다.

flowchart TB
    I[Image] --> R[docker run]
    R --> C1[New Container]
    C1 --> RUN1[Running]

    C2[Existing Container] --> S[docker start]
    S --> RUN2[Running]

docker run은 새 컨테이너를 만든다. docker start는 기존 컨테이너를 다시 시작한다. 따라서 컨테이너 내부 writable layer에 남아 있던 변경 사항, 컨테이너 이름, 포트 설정, 환경 변수 같은 기존 설정을 유지한 채 다시 실행하려면 docker start를 사용해야 한다. 다만 여기서 한 가지 주의할 점이 있다. docker start는 컨테이너를 이전 메모리 상태 그대로 이어서 실행하는 것이 아니다. 종료된 컨테이너를 다시 시작하면 컨테이너의 메인 프로세스가 처음부터 다시 실행된다. 파일 시스템 변경 사항과 컨테이너 설정은 유지되지만, 프로세스의 실행 지점이 그대로 복원되는 것은 아니다. 이 점은 pause와 구분해야 한다. pause는 실행 중인 프로세스를 얼려두었다가 다시 이어가는 것에 가깝고, stopstart는 프로세스를 종료했다가 같은 컨테이너 설정으로 새로 시작하는 것에 가깝다.

6. 컨테이너 일시 정지 상태(Paused)

컨테이너는 실행 중인 상태에서 일시 정지될 수 있다. 이때 사용하는 명령어가 docker pause이다.

docker pause web

Paused 상태는 컨테이너가 종료된 상태가 아니다. 컨테이너 내부 프로세스는 사라지지 않는다. 다만 실행이 멈춘다. 비유하자면 동영상을 정지 버튼으로 끈 것이 아니라, 일시 정지 버튼을 누른 것과 비슷하다. 다시 재생하면 멈춘 지점에서 이어진다. Docker 공식 문서에 따르면 docker pause는 지정된 컨테이너 안의 모든 프로세스를 일시 중단한다. Linux에서는 이를 위해 freezer cgroup을 사용한다. 일반적인 프로세스 중단에서 떠올릴 수 있는 SIGSTOP과 비슷하게 느껴질 수 있지만, Docker 문서는 freezer cgroup 방식에서는 중단되는 프로세스가 자신이 suspend/resume 되는 사실을 인지하거나 잡아낼 수 없다고 설명한다(Docker Documentation). 일시 정지된 컨테이너는 다음처럼 확인할 수 있다.

docker ps --filter status=paused

다시 실행하려면 docker unpause를 사용한다.

docker unpause web

간단한 예제를 보면 Paused 상태를 이해하기 쉽다.

docker run -d --name timer alpine sh -c "while true; do date; sleep 1; done"

이 컨테이너는 1초마다 현재 시간을 출력한다.

docker logs -f timer

이제 다른 터미널에서 컨테이너를 일시 정지한다.

docker pause timer

로그 출력이 멈춘다. 컨테이너가 삭제된 것도 아니고, 프로세스가 정상 종료된 것도 아니다. CPU 스케줄링에서 제외되어 실행이 멈춘 것이다.

docker unpause timer

위 명령어를 실행하면 멈춰 있던 컨테이너가 다시 실행을 이어간다. Docker의 unpause 명령은 지정된 컨테이너 안의 모든 프로세스를 다시 실행 상태로 되돌리며, Linux에서는 이 작업도 freezer cgroup을 통해 수행된다(Docker Documentation). 다만 일반적인 애플리케이션 운영에서 pause를 자주 사용하는 경우는 많지 않다. 보통은 디버깅, 리소스 제어, 특정 시점의 상태를 잠시 멈춰두고 확인해야 하는 상황에서 사용한다. 대부분의 운영 흐름에서는 stop, start, restart가 더 자주 사용된다.

7. 컨테이너 종료 상태(Stopped, Exited)

컨테이너가 종료되면 더 이상 Running 상태가 아니다. 일상적으로는 이 상태를 Stopped 상태라고 부르지만, Docker CLI에서는 보통 Exited 상태로 표시된다.

docker stop web

docker stop을 실행하면 Docker는 컨테이너의 메인 프로세스에 종료 신호를 보낸다. Docker 공식 문서에 따르면 컨테이너 내부의 메인 프로세스는 먼저 SIGTERM을 받고, 유예 시간이 지난 뒤에도 종료되지 않으면 SIGKILL을 받는다. 기본 종료 신호는 Dockerfile의 STOPSIGNAL이나 docker run, docker create--stop-signal 옵션으로 바꿀 수 있다(Docker Documentation). 흐름으로 보면 다음과 같다.

sequenceDiagram
    participant User as 사용자
    participant Docker as Docker daemon
    participant Container as Container main process

    User->>Docker: docker stop web
    Docker->>Container: SIGTERM 전송
    Container-->>Docker: 정상 종료 시도

    alt 유예 시간 안에 종료됨
        Docker-->>User: 컨테이너 종료 완료
    else 유예 시간 초과
        Docker->>Container: SIGKILL 전송
        Docker-->>User: 강제 종료 완료
    end

종료 유예 시간은 -t 또는 --timeout 옵션으로 지정할 수 있다.

docker stop -t 30 web

이 명령은 컨테이너가 정상적으로 종료될 시간을 30초 준다. 웹 서버라면 이 시간 동안 처리 중인 요청을 마무리하거나, 워커라면 진행 중인 작업을 정리할 수 있다. 물론 애플리케이션이 SIGTERM을 제대로 처리하도록 작성되어 있어야 한다. 컨테이너가 종료되는 경우는 docker stop뿐만이 아니다. 컨테이너 내부의 메인 프로세스가 스스로 종료되어도 컨테이너는 Exited 상태가 된다.

docker run --name once alpine echo "done"

이 컨테이너는 echo "done"을 실행한 뒤 바로 종료된다.

docker ps -a --filter name=once

출력에는 Exited (0) 같은 상태가 보일 수 있다. 여기서 (0)은 exit code이다. 일반적으로 exit code 0은 정상 종료를 의미하고, 0이 아닌 값은 오류 종료를 의미한다.

Exited (0)    정상 종료
Exited (1)    일반적인 오류 종료
Exited (137)  SIGKILL로 종료된 경우에 자주 보임

종료된 컨테이너는 사라진 것이 아니다. 파일 시스템의 writable layer, 컨테이너 설정, 로그, 이름 등의 정보가 남아 있다. 그래서 다시 시작할 수 있다.

docker start once

하지만 앞에서 설명했듯이 docker start는 이전 실행 지점을 이어서 실행하는 것이 아니다. 같은 컨테이너 설정으로 메인 프로세스를 다시 실행하는 것이다. 이 차이를 정리하면 다음과 같다.

Paused
→ 프로세스가 종료되지 않음
→ 멈춘 지점에서 다시 이어짐

Exited
→ 프로세스가 종료됨
→ docker start 시 메인 프로세스를 새로 실행

이 차이를 이해하면 pause, stop, start의 의미가 훨씬 명확해진다.

8. Restarting 상태와 재시작 정책

컨테이너 라이프사이클을 이야기할 때 기본 흐름은 Created → Running → Exited → Removed 정도로 설명해도 충분한 경우가 많다. 하지만 실제 운영에서는 Restarting 상태도 자주 만난다. Restarting 상태는 컨테이너가 재시작 정책에 의해 다시 시작되는 중인 상태이다. Docker는 컨테이너가 종료되었을 때 자동으로 다시 시작할지 여부를 restart policy로 제어할 수 있다. Docker 공식 문서에서는 --restart 옵션으로 재시작 정책을 설정할 수 있고, 값으로 no, on-failure[:max-retries], always, unless-stopped 등을 사용할 수 있다고 설명한다(Docker Documentation). 예를 들어 다음 명령은 컨테이너가 오류로 종료되면 최대 3번까지 재시작하도록 설정한다.

docker run -d \
  --name restart-demo \
  --restart on-failure:3 \
  alpine sh -c "echo fail && exit 1"

이 컨테이너는 실행되자마자 exit 1로 실패한다. Docker는 재시작 정책에 따라 컨테이너를 다시 실행하려고 시도한다. 이 과정에서 컨테이너가 Restarting 상태로 보일 수 있다.

docker ps -a --filter name=restart-demo

재시작 정책은 운영 환경에서 유용하다. 예를 들어 일시적인 오류로 프로세스가 종료되었을 때 컨테이너를 자동으로 다시 띄울 수 있다. 하지만 재시작 정책이 모든 문제를 해결해주는 것은 아니다. 애플리케이션이 시작하자마자 설정 오류로 종료되는 상황을 생각해보자. 이때 --restart always가 걸려 있으면 컨테이너는 계속 죽고 다시 시작하는 루프에 빠질 수 있다.

flowchart TB
    A[Container Running] --> B[Application Error]
    B --> C[Exited]
    C --> D[Restart Policy]
    D --> A

이런 상황에서는 컨테이너를 단순히 계속 재시작하는 것보다 로그를 확인하고 원인을 해결해야 한다.

docker logs restart-demo

재시작 정책은 장애를 숨기는 도구가 아니라, 일시적인 종료에 대응하기 위한 안전장치로 보는 것이 좋다.

9. 컨테이너 삭제 상태(Removed)

컨테이너를 완전히 제거하려면 docker rm을 사용한다.

docker rm web

삭제된 컨테이너는 더 이상 docker ps -a에서도 보이지 않는다. 이 상태를 개념적으로 Removed 상태라고 부를 수 있다. 다만 Docker 상태 필터에서 removed라는 상태를 조회하는 것은 아니다. 컨테이너가 삭제되면 Docker가 관리하는 컨테이너 객체 자체가 사라지기 때문에 조회 대상이 없어지는 것이다. Docker 상태 중에는 삭제 진행 중을 의미하는 removing 상태가 있다(Docker Documentation). 일반적으로 docker rm은 중지된 컨테이너에 사용한다.

docker stop web
docker rm web

실행 중인 컨테이너를 강제로 삭제하려면 -f 옵션을 사용할 수 있다.

docker rm -f web

Docker 공식 문서에 따르면 docker rm --force는 실행 중인 컨테이너를 강제로 제거하며, 이때 컨테이너의 메인 프로세스는 SIGKILL을 받은 뒤 컨테이너가 제거된다(Docker Documentation). 하지만 운영 환경에서 docker rm -f를 습관적으로 사용하는 것은 조심해야 한다. SIGKILL은 애플리케이션이 정리 작업을 할 기회를 주지 않고 즉시 종료시키는 신호이기 때문이다. 데이터베이스나 메시지 큐처럼 종료 과정이 중요한 프로세스라면 강제 삭제는 위험할 수 있다. 컨테이너를 삭제하면 컨테이너의 writable layer와 메타데이터가 사라진다. 컨테이너 안에만 저장해둔 파일도 함께 사라진다. 하지만 이미지는 삭제되지 않는다.

docker rm
→ 컨테이너 삭제
→ 이미지 유지

docker rmi
→ 이미지 삭제

또 하나 주의할 점은 볼륨이다. 컨테이너를 삭제한다고 해서 항상 볼륨까지 삭제되는 것은 아니다. Docker 문서에서도 docker rm -v를 사용하면 컨테이너와 연결된 볼륨을 제거할 수 있지만, 이름이 지정된 볼륨은 제거되지 않는다고 설명한다(Docker Documentation). 예를 들어 다음처럼 named volume을 사용했다고 해보자.

docker volume create app-data

docker run -d \
  --name app \
  -v app-data:/data \
  alpine sh -c "while true; do sleep 60; done"

컨테이너를 삭제해도 named volume은 남아 있을 수 있다.

docker rm -f app
docker volume ls

이 동작은 데이터 보존 측면에서 중요하다. 컨테이너는 언제든지 삭제되고 다시 만들어질 수 있는 실행 단위로 보는 것이 좋다. 반면 중요한 데이터는 컨테이너 writable layer가 아니라 volume이나 외부 저장소에 두는 것이 안전하다.

10. 파일 시스템 관점에서 보는 컨테이너 라이프사이클

컨테이너 라이프사이클은 프로세스 상태만의 문제가 아니다. 파일 시스템 관점에서도 이해할 필요가 있다. 이미지는 여러 개의 read-only layer로 구성된다. 컨테이너가 생성되면 그 이미지 레이어 위에 컨테이너 전용 writable layer가 추가된다. 컨테이너 내부에서 파일을 만들거나 수정하면 기본적으로 이 writable layer에 기록된다.

flowchart TB
    W[Container writable layer] --> I3[Image layer 3]
    I3 --> I2[Image layer 2]
    I2 --> I1[Image layer 1]

컨테이너가 Running 상태일 때 애플리케이션이 로그 파일을 쓰거나 임시 파일을 만들면 writable layer에 변경 사항이 생긴다. 컨테이너가 Exited 상태가 되어도 이 writable layer는 남아 있다. 그래서 컨테이너를 다시 시작하면 이전에 만들어진 파일이 그대로 보일 수 있다. 간단한 예제를 보자.

docker run -it --name fs-demo alpine sh

컨테이너 안에서 파일을 만든다.

echo "hello" > /tmp/hello.txt
exit

컨테이너는 종료된다. 다시 시작한 뒤 파일을 확인한다.

docker start -ai fs-demo
cat /tmp/hello.txt

파일이 남아 있는 것을 볼 수 있다. 컨테이너가 종료되었다고 writable layer가 사라지는 것은 아니기 때문이다. 하지만 컨테이너를 삭제하면 이야기가 달라진다.

docker rm fs-demo

이제 해당 컨테이너의 writable layer도 함께 사라진다. 같은 이미지로 새 컨테이너를 만들어도 이전 컨테이너의 /tmp/hello.txt는 존재하지 않는다.

docker run -it --name fs-demo-2 alpine sh
cat /tmp/hello.txt

파일이 없다. 새 컨테이너는 같은 이미지에서 만들어졌지만, 이전 컨테이너와는 다른 writable layer를 가지기 때문이다. 이 지점에서 Docker를 사용할 때의 중요한 원칙이 나온다. 컨테이너 내부 파일 시스템은 영구 저장소로 생각하지 않는 것이 좋다. 컨테이너는 삭제되고 다시 만들어질 수 있다. 중요한 데이터는 volume, bind mount, 외부 데이터베이스, 오브젝트 스토리지 같은 별도 저장소에 두는 것이 안전하다.

docker run -d \
  --name mysql \
  -v mysql-data:/var/lib/mysql \
  mysql:8

이렇게 volume을 사용하면 컨테이너를 삭제하고 새로 만들어도 데이터 디렉터리를 컨테이너 생명주기와 분리할 수 있다.

11. 컨테이너가 바로 종료되는 이유

Docker를 처음 사용할 때 가장 자주 만나는 문제가 "컨테이너가 바로 꺼지는 현상"이다. 예를 들어 다음 명령을 실행해보자.

docker run ubuntu

명령어가 실패한 것 같지는 않은데, docker ps에는 아무것도 보이지 않는다.

docker ps

하지만 docker ps -a를 보면 컨테이너가 종료된 상태로 남아 있다.

docker ps -a

이유는 간단하다. 컨테이너 안에서 계속 실행될 메인 프로세스가 없기 때문이다. ubuntu 이미지는 기본적으로 장시간 실행되는 서버 프로세스를 띄우지 않는다. 실행할 명령이 끝나면 컨테이너도 종료된다. 인터랙티브하게 셸을 실행하고 싶다면 다음처럼 실행해야 한다.

docker run -it ubuntu bash

여기서 -i는 표준 입력을 열어두는 옵션이고, -t는 TTY를 할당하는 옵션이다. 이 옵션을 사용하면 컨테이너 안에서 bash 셸을 사용할 수 있다. 반면 서버처럼 계속 떠 있어야 하는 컨테이너는 foreground에서 실행되는 메인 프로세스가 필요하다.

docker run -d nginx:alpine

Nginx 이미지는 컨테이너가 실행될 때 Nginx 프로세스를 foreground로 유지하도록 구성되어 있기 때문에 컨테이너가 계속 Running 상태로 남는다. 직접 이미지를 만들 때도 이 점을 조심해야 한다. 예를 들어 Dockerfile에서 다음처럼 작성했다고 해보자.

FROM alpine

CMD ["echo", "hello"]

이 이미지를 실행하면 echo hello가 실행되고 바로 종료된다.

docker build -t hello-image .
docker run hello-image

반면 Node.js 서버를 실행하는 이미지라면 메인 프로세스가 서버 프로세스여야 한다.

FROM node:22-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

CMD ["node", "dist/main.js"]

여기서 node dist/main.js가 서버 프로세스를 계속 유지한다면 컨테이너도 Running 상태로 유지된다. 하지만 애플리케이션이 시작 직후 예외로 종료된다면 컨테이너도 함께 종료된다. 결국 컨테이너가 바로 꺼진다는 것은 대부분 Docker 자체의 문제가 아니다. 컨테이너 안에서 실행된 메인 프로세스가 끝났다는 뜻이다.

12. 상태 확인하기

컨테이너 라이프사이클을 제대로 이해하려면 현재 컨테이너가 어떤 상태인지 확인하는 명령어에 익숙해져야 한다. 가장 기본적인 명령어는 docker ps이다.

docker ps

이 명령은 기본적으로 실행 중인 컨테이너만 보여준다. Docker 공식 문서에서도 docker ps는 기본적으로 running container만 보여주며, 모든 컨테이너를 보려면 --all 또는 -a 옵션을 사용한다고 설명한다(Docker Documentation).

docker ps -a

상태별로 필터링할 수도 있다.

docker ps -a --filter status=created
docker ps -a --filter status=running
docker ps -a --filter status=paused
docker ps -a --filter status=exited

출력을 원하는 형태로 바꾸고 싶다면 --format 옵션을 사용할 수 있다.

docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"

좀 더 자세한 상태를 보고 싶다면 docker inspect를 사용한다.

docker inspect web

필요한 필드만 출력할 수도 있다.

docker inspect -f '{{.State.Status}}' web

컨테이너의 exit code, 시작 시간, 종료 시간을 함께 확인하려면 다음처럼 볼 수 있다.

docker inspect -f 'status={{.State.Status}} exitCode={{.State.ExitCode}} startedAt={{.State.StartedAt}} finishedAt={{.State.FinishedAt}}' web

출력은 대략 다음과 비슷하다.

status=exited exitCode=0 startedAt=2026-04-30T01:00:00Z finishedAt=2026-04-30T01:05:00Z

컨테이너 상태 변화를 실시간으로 보고 싶다면 docker events도 사용할 수 있다.

docker events

특정 컨테이너에 대한 이벤트만 보고 싶다면 필터를 걸 수 있다.

docker events --filter container=web

컨테이너를 실행하고 중지하면 start, stop, die, destroy 같은 이벤트를 확인할 수 있다. 이 명령어는 컨테이너 라이프사이클을 눈으로 따라가기에 좋다.

sequenceDiagram
    participant User as 사용자
    participant Docker as Docker
    participant Events as docker events

    User->>Docker: docker run
    Docker-->>Events: create
    Docker-->>Events: start

    User->>Docker: docker stop
    Docker-->>Events: stop
    Docker-->>Events: die

    User->>Docker: docker rm
    Docker-->>Events: destroy

13. stop, kill, rm -f의 차이

컨테이너를 종료하거나 제거할 때 stop, kill, rm -f를 비슷하게 사용하는 경우가 있다. 하지만 세 명령은 의미가 다르다. docker stop은 정상 종료를 요청한다.

docker stop web

이 명령은 먼저 컨테이너의 메인 프로세스에 종료 신호를 보내고, 일정 시간 기다린 뒤 그래도 종료되지 않으면 강제 종료한다. 애플리케이션이 종료 신호를 처리할 기회를 가진다는 점에서 운영 환경에서는 보통 stop이 기본 선택이다. docker kill은 컨테이너의 메인 프로세스를 즉시 종료시키는 데 가깝다.

docker kill web

기본적으로 강한 종료 신호를 보내기 때문에 애플리케이션이 정리 작업을 수행할 기회가 거의 없다. 꼭 필요한 상황이 아니라면 습관적으로 사용할 명령은 아니다. docker rm -f는 실행 중인 컨테이너를 강제로 제거한다.

docker rm -f web

이 명령은 컨테이너를 종료시키는 것에서 끝나지 않고 컨테이너 객체 자체를 삭제한다. 즉, stoprm을 강하게 합친 것에 가깝다.

docker stop
→ 실행 중인 컨테이너를 종료 상태로 전환
→ 컨테이너는 남아 있음

docker kill
→ 실행 중인 컨테이너 프로세스를 강제 종료
→ 컨테이너는 Exited 상태로 남아 있음

docker rm -f
→ 실행 중인 컨테이너를 강제로 종료하고 삭제
→ 컨테이너가 목록에서 사라짐

이 차이를 모르면 실수로 컨테이너를 삭제하거나, 반대로 삭제했다고 생각했는데 종료된 컨테이너가 계속 쌓이는 상황이 생길 수 있다. 종료된 컨테이너가 너무 많이 쌓였을 때는 다음 명령으로 정리할 수 있다.

docker container prune

이 명령은 중지된 컨테이너들을 한 번에 제거한다. 다만 삭제 작업은 되돌리기 어렵기 때문에 어떤 컨테이너가 삭제되는지 확인하고 사용하는 것이 좋다.

14. 전체 라이프사이클 다시 보기

지금까지 설명한 내용을 하나의 흐름으로 다시 보면 다음과 같다.

stateDiagram-v2
    [*] --> Image: docker pull / docker build

    Image --> Created: docker create
    Image --> Running: docker run

    Created --> Running: docker start

    Running --> Paused: docker pause
    Paused --> Running: docker unpause

    Running --> Exited: main process exits
    Running --> Exited: docker stop
    Running --> Exited: docker kill

    Exited --> Running: docker start

    Running --> Restarting: restart policy
    Restarting --> Running: start succeeds
    Restarting --> Exited: retry limit exceeded

    Created --> Removing: docker rm
    Exited --> Removing: docker rm
    Running --> Removing: docker rm -f

    Removing --> Removed
    Removed --> [*]

이 그림에서 Removed는 Docker가 계속 상태로 보관하는 값이라기보다 컨테이너 객체가 삭제되어 더 이상 조회되지 않는 최종 상태로 이해하는 것이 좋다. 반면 Removing은 삭제가 진행 중인 순간의 상태이다. 이 흐름을 명령어 중심으로 다시 표현하면 다음과 같다.

flowchart TB
    A[Image] -->|docker create| B[Created]
    A -->|docker run| C[Running]

    B -->|docker start| C

    C -->|docker pause| D[Paused]
    D -->|docker unpause| C

    C -->|docker stop| E[Exited]
    C -->|main process 종료| E

    E -->|docker start| C

    B -->|docker rm| F[Removed]
    E -->|docker rm| F
    C -->|docker rm -f| F

컨테이너 라이프사이클을 이해한다는 것은 결국 Docker 명령어가 이 그림의 어느 화살표에 해당하는지 이해하는 것과 같다. docker run은 이미지를 바로 Running 상태의 컨테이너로 만든다. 정확히는 생성과 실행을 함께 수행한다. docker create는 Created 상태까지만 만든다. docker start는 Created 또는 Exited 상태의 기존 컨테이너를 Running 상태로 바꾼다. docker stop은 Running 상태를 Exited 상태로 바꾼다. docker rm은 컨테이너 객체를 삭제한다.

마치며

컨테이너의 라이프사이클은 단순히 실행하고 종료하는 과정이 아니다. Docker 컨테이너는 이미지로부터 생성되고, 실행되고, 일시 정지될 수 있으며, 종료된 뒤에도 다시 시작될 수 있고, 마지막에는 삭제된다. 이 흐름을 이해하면 Docker 명령어를 훨씬 정확하게 사용할 수 있다. docker rundocker start의 차이를 알게 되고, docker stop을 했는데 컨테이너가 왜 목록에 남아 있는지 이해할 수 있다. docker rm을 했을 때 무엇이 사라지고 무엇이 남는지도 구분할 수 있다. 컨테이너가 바로 종료되는 상황을 만났을 때도 Docker가 이상한 것이 아니라 메인 프로세스가 끝났다는 관점으로 문제를 볼 수 있다.

컨테이너는 VM처럼 계속 켜져 있는 작은 서버가 아니다. 컨테이너는 격리된 환경에서 실행되는 프로세스이며, 그 프로세스를 둘러싼 파일 시스템, 네트워크, 메타데이터, 실행 설정을 Docker가 함께 관리하는 구조이다. 결국 Docker를 제대로 이해한다는 것은 명령어를 많이 외우는 것이 아니라, 컨테이너가 어떤 상태를 가지고 있고 각 명령어가 어떤 상태 전이를 만드는지 이해하는 것에 가깝다. 이 상태 흐름이 머릿속에 잡히면 Docker를 사용할 때 단순히 명령어를 따라 치는 것이 아니라 지금 컨테이너가 어디에 있고 다음에 어떤 상태로 이동해야 하는지 판단하면서 사용할 수 있게 된다.

참고자료