도커 기초 강좌 #3 이미지와 컨테이너 — build, run, ps, logs, exec
#2 Dockerfile 첫 작성에서 이미지를 직접 굽고 한 번 띄워봤습니다. 이번 글은 도커 CLI 명령군을 본격적으로 정리합니다. 일상 작업의 90%는 이 명령들로 끝납니다.
도커 기초 강좌 시리즈에서 이번 글의 위치:
- #1 컨테이너란
- #2 Dockerfile 첫 작성
- #3 이미지와 컨테이너 — build, run, ps, logs, exec ← 이번 글
- #4 볼륨과 네트워크
- #5 레지스트리 — Docker Hub, GHCR, push/pull
- #6
.dockerignore와 빌드 컨텍스트
컨테이너의 라이프사이클 #
명령을 늘어놓기 전에 한 컨테이너가 거치는 상태를 그림으로 잡고 들어갑시다.
┌──────────┐
│ image │
└────┬─────┘
│ docker run
▼
┌──────────┐ docker stop ┌──────────┐
│ running │ ─────────────▶ │ exited │
│ │ ◀───────────── │ │
└────┬─────┘ docker start └────┬─────┘
│ │
│ docker pause │ docker rm
▼ ▼
┌──────────┐ ┌──────────┐
│ paused │ │ (gone) │
└──────────┘ └──────────┘이미지에서 run으로 컨테이너가 만들어지고, 메인 프로세스가 살아있는 동안 running 상태입니다. 메인 프로세스가 끝나거나 stop으로 신호를 받으면 exited가 되고, rm으로 사라집니다. 이 다섯 상태와 화살표만 머리에 두면 각 명령이 어떤 단계에 쓰이는지 정리됩니다.
docker build — 이미지를 굽는다
#
가장 단순한 형태는 #2에서 본 그대로:
docker build -t hello-docker .자주 쓰는 플래그를 묶어 보면:
| 플래그 | 의미 |
|---|---|
-t name:tag | 이미지에 이름과 태그를 붙임. :tag 생략 시 latest. 여러 번 줘서 여러 태그 가능 |
-f Dockerfile.dev | 기본 이름(Dockerfile) 이 아닌 다른 파일을 쓸 때 |
--no-cache | 레이어 캐시 무시하고 처음부터 빌드 |
--pull | 베이스 이미지를 매번 새로 받아옴 (오래된 캐시 회피) |
--build-arg KEY=value | Dockerfile의 ARG에 값 주입 |
--target stage | 멀티스테이지 빌드에서 특정 스테이지만 |
--platform linux/amd64 | 특정 아키텍처용으로 빌드 |
--progress=plain | BuildKit의 한 줄 요약이 아니라 풀 로그를 보고 싶을 때 |
태그를 같은 빌드에 두 개 붙이는 패턴은 자주 씁니다.
docker build -t myapp:1.2.0 -t myapp:latest .릴리스 버전과 latest를 동시에 굳혀 두는 식입니다. 푸시할 땐 둘 다 푸시합니다.
docker images — 캐시된 이미지 보기
#
docker images
# REPOSITORY TAG IMAGE ID CREATED SIZE
# hello-docker latest a1b2c3d4e5f6 2 minutes ago 148MB
# python 3.14-slim 9a8b7c6d5e4f 3 days ago 147MB자주 쓰는 변형:
docker images -q # ID 만 출력 (스크립트용)
docker images --filter dangling=true # 태그가 떨어진 (<none>) 중간 이미지
docker image inspect myapp # JSON 으로 메타데이터 전체
docker history myapp # 레이어별로 어떤 명령이 만들었는지docker history는 처음 보면 흥미롭습니다 — 이미지가 어떤 레이어로 쌓였는지, 각 레이어 크기가 얼마인지 한눈에 보입니다. 이미지가 비대해진 원인을 추적할 때 자주 씁니다.
docker run — 핵심 플래그
#
docker run은 옵션이 정말 많지만, 일상에서 만나는 건 한 줌입니다.
docker run -d -p 8000:8000 --name myapp -e DEBUG=1 --rm hello-docker| 플래그 | 의미 |
|---|---|
-d (--detach) | 백그라운드 실행. 컨테이너 ID가 출력되고 프롬프트가 돌아옴 |
-p HOST:CONTAINER | 포트 매핑. -p 8080:8000은 호스트 8080 → 컨테이너 8000 |
-P (대문자) | EXPOSE 된 포트 모두를 임의의 호스트 포트에 매핑 |
--name N | 컨테이너 이름. 안 주면 도커가 bold_curie 같은 임의 이름 |
-e KEY=val | 환경변수 주입. 여러 번 가능 |
--env-file .env | .env 파일에서 한꺼번에 읽기 |
--rm | 종료되면 자동 삭제. 개발/일회성 실행에 편함 |
-it | 인터랙티브 + TTY. 셸에 들어갈 때 |
-v src:dst | 볼륨/바인드 마운트 (#4) |
--network N | 네트워크 지정 (#4) |
--restart unless-stopped | 컨테이너가 죽으면 자동 재시작 |
-w /path | working dir 오버라이드 |
-u 1000:1000 | UID/GID 오버라이드 |
플래그는 이미지 이름 앞에 옵니다. 이미지 이름 뒤는 컨테이너 안에서 실행할 명령(있으면)입니다.
docker run [플래그들] <image> [명령]docker run --rm ubuntu:24.04 echo hello
# hello-d와 --rm 같이 쓰지 말 것
#
자주 헷갈리는 지점입니다. -d --rm으로 백그라운드로 띄우면 종료 즉시 사라져 로그를 볼 시간이 없습니다. 둘 중 하나만:
- 개발/체험:
--rm(포그라운드, 종료 시 정리) - 운영/시연:
-d(백그라운드, 명시적으로rm으로 정리)
docker ps — 실행 중인 컨테이너
#
docker ps
# CONTAINER ID IMAGE STATUS PORTS NAMES
# a1b2c3d4... hello-docker Up 3 minutes 0.0.0.0:8000->8000/tcp myappdocker ps -a자주 쓰는 변형:
docker ps -q # ID만
docker ps --filter status=exited # 종료된 것만
docker ps --filter ancestor=hello-docker # 특정 이미지에서 만든 것만
docker ps --format '{{.Names}}\t{{.Status}}' # 포맷 커스텀docker logs — stdout/stderr 보기
#
-d로 띄운 컨테이너의 로그를 보려면:
docker logs myapp # 그동안의 출력 전체
docker logs -f myapp # follow — tail -f 처럼
docker logs --tail 100 myapp # 마지막 100줄만
docker logs --since 10m myapp # 최근 10분
docker logs --timestamps myapp # 타임스탬프 포함도커는 컨테이너의 stdout/stderr를 호스트의 로그 파일에 쌓아둡니다. 그래서 앱이 표준 출력으로만 로그를 찍어도 docker logs로 다 볼 수 있습니다. 컨테이너 안에서 로그 파일을 만들지 말고 stdout으로 흘려보내는 패턴이 권장되는 이유가 이겁니다.
docker exec — 떠 있는 컨테이너에 들어가기
#
run은 새 컨테이너를 만들고, exec는 이미 떠 있는 컨테이너 안에서 명령을 실행합니다.
docker exec -it myapp bash
# 또는 sh (alpine 처럼 bash 없는 베이스)
docker exec -it myapp sh들어가서 파일을 보거나, DB 클라이언트를 쳐보거나, ps로 프로세스를 확인할 수 있습니다. 디버깅의 첫 걸음입니다.
docker exec myapp ls /app
docker exec myapp env | grep DB_-it 없이 단발 명령만 실행하는 형태도 자주 씁니다.
run과 exec의 차이를 한 번 더
#
처음에는 둘이 비슷해 보입니다. 큰 차이는:
docker run -it ubuntu bash— 새 컨테이너를 만들고 그 안에서 bash.exit하면 컨테이너 종료.docker exec -it myapp bash— 이미 떠 있는myapp안에서 bash.exit해도myapp은 그대로 살아있음.
운영에선 거의 항상 exec입니다. run으로 들어간 우분투에서 한 작업은 그 컨테이너 종료와 함께 사라지니, “환경 잠깐 만져보기” 외엔 의미가 없습니다.
docker stop / start / restart
#
라이프사이클을 직접 다루는 명령들입니다.
docker stop myapp
# 컨테이너에 SIGTERM → 10초 대기 → 안 끝나면 SIGKILL
docker stop -t 30 myapp # 대기 시간 30초로stop은 그레이스풀 종료가 기본입니다. 도커가 SIGTERM을 보내고, 앱이 깨끗이 정리할 시간(기본 10초)을 줍니다. 이 시간 안에 종료되지 않으면 SIGKILL이 떨어집니다.
#2 에서
CMD를 exec form으로 적어야 한다고 했던 이유가 여기 있습니다. shell form으로 적으면 SIGTERM이 셸에서 멈추고 앱에 닿지 않아, 항상 SIGKILL로 죽습니다. DB 연결이 깔끔히 닫히지 않거나, 진행 중이던 작업이 잘립니다.
docker start myapp # exited 상태에서 다시 running 으로
docker restart myapp # stop → start
docker kill myapp # SIGKILL 즉시 (그레이스풀 안 함)docker rm / docker rmi — 정리
#
docker rm myapp # 종료된 컨테이너 삭제
docker rm -f myapp # 떠 있어도 강제 삭제 (kill + rm)
docker rm $(docker ps -aq) # 모든 컨테이너 삭제 (위험)docker rmi hello-docker
docker rmi $(docker images -q --filter dangling=true) # 태그 떨어진 것<none>:<none>으로 표시되는 dangling 이미지는 빌드를 반복하면 자꾸 쌓입니다. 가끔 정리해 주세요.
docker system prune — 한 번에 청소
#
여러 명령을 일일이 치지 않고, 한 번에 안 쓰는 것을 정리하는 명령:
docker system prune
# 멈춘 컨테이너 + dangling 이미지 + 안 쓰는 네트워크 정리
docker system prune -a
# 위 + 어떤 컨테이너에서도 안 쓰는 이미지까지
docker system prune -a --volumes
# 위 + 안 쓰는 볼륨까지 (데이터 사라질 수 있음 — 주의)CI 머신이나 개발 머신의 디스크가 차오르기 시작했을 때 이 명령부터 치게 됩니다. 다만 --volumes는 DB 데이터까지 날아갈 수 있으니 항상 한 번 더 보고 치십시오.
docker system df
# TYPE TOTAL ACTIVE SIZE RECLAIMABLE
# Images 18 4 3.2GB 2.1GB (65%)
# Containers 6 1 12MB 5MB (41%)
# Local Volumes 8 3 420MB 280MB (66%)docker inspect — 메타데이터 캐기
#
문제를 추적할 때 한 번씩 부르는 명령. 컨테이너 / 이미지 / 네트워크 / 볼륨 무엇이든 JSON으로 풀어 줍니다.
docker inspect myapp
docker inspect --format '{{.State.Status}}' myapp
# running
docker inspect --format '{{.NetworkSettings.IPAddress}}' myapp
# 172.17.0.2--format은 Go 템플릿 문법인데, 한 줄로 특정 값만 뽑아내고 싶을 때 매우 유용합니다.
한 사이클 — 모아서 #
이번 글까지의 명령들로 한 컨테이너를 처음부터 끝까지 몰고 가는 흐름:
# 1. 이미지 빌드
docker build -t myapp .
# 2. 백그라운드로 실행
docker run -d --name myapp -p 8000:8000 -e DEBUG=1 myapp
# 3. 상태 확인
docker ps
docker logs -f myapp
# 4. 안에 들어가서 점검
docker exec -it myapp sh
# 5. 그레이스풀 종료
docker stop myapp
# 6. 정리
docker rm myapp
docker rmi myapp처음에는 길어 보여도, 곧 손에 붙습니다. CI 스크립트, Makefile, 셸 alias에 자주 들어가는 패턴입니다.
정리 #
이번 글에서 잡은 그림:
- 컨테이너 라이프사이클: image → running → exited → (gone). 각 명령은 이 화살표 위의 특정 단계에 대응한다
- **
docker build**의 자주 쓰는 플래그:-t,-f,--no-cache,--build-arg,--target,--platform - **
docker run**의 일상 플래그:-d,-p,--name,-e,--rm,-it - **
docker ps/images**로 현재 상태를, **docker logs/exec**로 안을 들여다본다 - **
docker stop**은 SIGTERM 그레이스풀, **docker kill**은 SIGKILL 즉시 - **
docker system prune**은 한 번에 청소, **docker inspect --format**은 정밀 진단
다음 글(#4 볼륨과 네트워크)에서는 컨테이너가 죽으면 같이 날아가던 데이터를 어떻게 살리는지(볼륨), 그리고 컨테이너끼리 / 호스트와 어떻게 통신하는지(네트워크) 두 가지를 잡습니다. 운영 단계로 한 발 더 들어가는 내용입니다.