LLM WikiAccess-protected knowledge portal
← 스터디 홈
1편 · 약 18분

Docker 아키텍처와 컨테이너 런타임

Docker는 단일 프로세스가 아니다

"Docker"라고 말할 때 사람들은 보통 docker run 명령어를 떠올린다. 하지만 그 뒤에서는 최소 세 개의 독립된 데몬이 협력한다. 이 아키텍처를 모르면 컨테이너가 왜 죽지 않는지, 왜 dockerd가 재시작해도 실행 중인 컨테이너가 살아남는지 이해하기 어렵다.

Docker가 2015~2017년에 걸쳐 모놀리식 구조를 해체한 데는 두 가지 이유가 있었다. 첫째, Kubernetes를 비롯한 오케스트레이터들이 Docker의 고수준 툴링 없이 컨테이너 런타임만 직접 호출하기를 원했다. 둘째, OCI(Open Container Initiative) 표준화로 런타임과 이미지 포맷을 교체 가능하게 만들 필요가 있었다.

🖥️ 개발자 / CI 도구 ↓ REST API / Unix socket
Docker Engine dockerd (Docker Daemon) — 이미지·네트워크·볼륨 관리, API 서빙
↓ gRPC
고수준 런타임 (CNCF Graduated) containerd — 이미지 pull/push, 컨테이너 생명주기, 스냅샷 containerd-shim — 컨테이너 당 하나, 데몬 재시작 시에도 유지
↓ OCI Runtime Spec
저수준 런타임 (OCI 참조 구현) runc — namespace/cgroup 설정 후 프로세스 exec, 즉시 종료
↓ syscall (clone, mount, ...) Linux Kernel — namespaces · cgroups · OverlayFS
Docker 컴포넌트 계층 구조

OCI 표준: 런타임과 이미지의 공통 언어

2015년 Docker와 CoreOS 등이 공동 설립한 OCI(Open Container Initiative) 는 세 가지 스펙을 정의한다.

스펙내용참조 구현
runtime-spec컨테이너 실행 환경·생명주기 정의runc
image-spec이미지 포맷·레이어 구조 정의Docker Image, OCI Image
distribution-spec레지스트리 API 정의Distribution (Docker Registry v2)

OCI 덕분에 runc 대신 gVisor(runsc)나 Kata Containers 같은 샌드박스 런타임으로 교체해도 containerd·dockerd는 그대로 사용할 수 있다.

핵심 컴포넌트 상세

dockerd — 사용자를 위한 관문

dockerd는 Docker CLI와 REST API로 통신하며 다음을 담당한다.

  • 이미지 빌드: BuildKit을 내장해 docker build를 처리
  • 네트워크 관리: bridge, overlay, macvlan 드라이버로 가상 네트워크 생성
  • 볼륨 관리: 로컬 볼륨·플러그인 볼륨 생명주기 관리
  • containerd 위임: 실제 컨테이너 실행은 containerd에 gRPC로 위임

containerd — 진짜 런타임 조율자

containerd는 CNCF 졸업 프로젝트(2019)로, Kubernetes도 직접 사용하는 고수준 런타임이다.

  • 이미지 레지스트리 통신: pull, push, verify (content-addressable 저장)
  • 스냅샷 관리: OverlayFS 레이어를 스냅샷으로 추상화
  • 컨테이너 생명주기: create, start, stop, delete
  • Shim 관리: 컨테이너마다 containerd-shim 프로세스를 fork

containerd-shim — 데몬과 컨테이너 사이의 완충재

shim이 존재하는 이유는 데몬 독립성이다.

  1. containerd가 재시작·업그레이드되어도 컨테이너는 계속 실행된다.
  2. 컨테이너의 stdin/stdout/stderr 스트림을 들고 있어 docker attach가 동작한다.
  3. 좀비 프로세스(zombie process)를 리핑(reaping)한다 — 컨테이너 PID 1이 종료한 자식 프로세스를 정리.
  4. OCI 런타임(runc)에 container 설정을 넘기고, runc는 실행 후 즉시 종료한다.

runc — 커널과 직접 대화하는 최소 런타임

runc는 OCI runtime-spec의 참조 구현이다. 주어진 config.json(OCI 번들)을 읽고:

  1. clone() 시스템 콜로 새 namespace를 가진 프로세스 생성
  2. cgroup 계층 설정 (CPU·메모리·I/O 제한)
  3. pivot_root() 또는 chroot()로 루트 파일시스템 교체
  4. 네트워크 namespace에 veth 인터페이스 연결
  5. 컨테이너의 진입점(entrypoint) exec → 이후 runc 자체는 종료
① CLI
docker run nginx
② dockerd
REST API 수신
③ dockerd
이미지 pull
(없으면 레지스트리)
④ containerd에
gRPC 위임
⑤ containerd
스냅샷(OverlayFS)
마운트 준비
⑥ containerd-shim
fork → OCI 번들
config.json 생성
⑦ runc 실행
namespace·cgroup
루트FS 설정
⑧ exec nginx
runc 종료
⑨ shim이 컨테이너 감시
(containerd 재시작해도 생존)
⑩ containerd → dockerd
상태 보고 (running)
docker run 내부 흐름 (10단계)

Linux 커널 격리 메커니즘

컨테이너는 VM과 달리 게스트 OS 커널이 없다. 격리는 전적으로 Linux 커널 기능에 의존한다.

Namespaces — "무엇을 볼 수 있는가"

Namespace격리 대상컨테이너 관점
pid프로세스 ID 공간컨테이너 내 PID 1이 호스트의 PID 12345
net네트워크 인터페이스·라우팅독립적인 eth0, 127.0.0.1
mnt마운트 포인트독립적인 루트 파일시스템
uts호스트명·도메인자체 hostname 설정 가능
ipcSystem V IPC, POSIX 메시지 큐격리된 IPC 자원
userUID/GID 매핑컨테이너 root → 호스트 비특권 UID
cgroupcgroup 루트 뷰컨테이너가 자신의 cgroup 계층만 봄

cgroups v2 — "얼마나 쓸 수 있는가"

cgroups(control groups)는 프로세스 그룹이 사용할 수 있는 리소스 양을 제한·측정·격리한다. Docker는 cgroups v2를 기본으로 사용한다(Linux 5.2+, systemd 기반 distro 기준).

# 컨테이너의 cgroup 경로 확인
$ cat /sys/fs/cgroup/system.slice/docker-<id>.scope/cpu.max
100000 100000    # CPU quota/period (ms) — 여기선 무제한

# CPU 1개 제한으로 실행
$ docker run --cpus="1.0" nginx
# → cpu.max: 100000 100000 (1 CPU = 100ms/100ms)

# 메모리 512MB 제한
$ docker run --memory="512m" nginx
# → memory.max: 536870912

OverlayFS — 이미지 레이어의 물리적 구현

Docker 이미지는 레이어 스택이다. OverlayFS는 이 스택을 하나의 파일시스템 뷰로 보여준다.

merged (컨테이너가 보는 통합 뷰) /etc/nginx/nginx.conf · /usr/sbin/nginx · /bin/sh · ...
↕ OverlayFS 커널 드라이버
upperdir (R/W)
컨테이너 쓰기 레이어
(삭제 시 whiteout 파일)
lowerdir[0] (R/O)
nginx 설정 레이어
lowerdir[1] (R/O)
nginx 바이너리 레이어
lowerdir[2] (R/O)
debian:bookworm-slim 베이스
Copy-on-Write: 쓰기 시 lowerdir 파일을 upperdir로 복사 후 수정 (copy-up)
OverlayFS 레이어 구조 (nginx 이미지 예시)

Copy-on-Write 동작: 컨테이너가 read-only 레이어의 파일을 처음 수정할 때 OverlayFS가 해당 파일을 upperdir(쓰기 레이어)로 복사(copy-up)한 뒤 수정한다. 원본 레이어는 변경되지 않으므로 동일 이미지를 공유하는 다른 컨테이너에 영향을 주지 않는다.

# 실제 OverlayFS 마운트 확인
$ docker inspect nginx_container | grep -A5 GraphDriver
"GraphDriver": {
  "Data": {
    "LowerDir": "/var/lib/docker/overlay2/<id>/diff:...",
    "MergedDir": "/var/lib/docker/overlay2/<id>/merged",
    "UpperDir": "/var/lib/docker/overlay2/<id>/diff",
    "WorkDir": "/var/lib/docker/overlay2/<id>/work"
  }
}

네트워크: 기본 bridge 드라이버

docker run 시 기본으로 docker0 bridge 인터페이스에 연결된다.

  1. runc가 컨테이너에 새 network namespace(net ns) 생성
  2. dockerd가 veth pair 생성: 한쪽은 컨테이너 내 eth0, 다른 쪽은 호스트의 docker0에 연결
  3. iptables(또는 nftables) 규칙으로 NAT 처리 → 컨테이너 → 외부 트래픽 가능
  4. -p 8080:80iptables DNAT 규칙을 추가해 호스트 포트를 컨테이너 포트로 포워딩
# 컨테이너 네트워크 확인
$ ip netns list                     # 네트워크 namespace 목록
$ docker network ls                 # Docker 네트워크 목록
$ iptables -t nat -L DOCKER -n      # 포트 포워딩 규칙 확인

rootless 컨테이너

Docker 23.0+에서 rootless 모드가 안정화되었다. 기본 모드에서는 dockerd가 root 권한으로 실행되어 보안 위험이 있다. rootless 모드에서는:

  • dockerd와 컨테이너가 일반 사용자 UID로 실행
  • user namespace로 내부 root(UID 0)를 호스트 비특권 UID에 매핑
  • slirp4netns 또는 pasta로 네트워크 제공 (성능은 다소 낮음)
  • /var/lib/docker 대신 ~/.local/share/docker 사용
# rootless 설치 (Ubuntu/Debian)
$ dockerd-rootless-setuptool.sh install
$ export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock

정리

컴포넌트역할 한 줄 요약
dockerd사용자 API, 이미지·네트워크·볼륨 관리
containerd컨테이너 생명주기, 이미지 저장소, shim 관리
containerd-shim컨테이너 당 하나, 데몬 재시작 시 컨테이너 생존 보장
runcOCI 번들 → 실제 프로세스 생성, 즉시 종료
namespace프로세스·네트워크·파일시스템 등 격리
cgroup v2CPU·메모리·I/O 자원 제한·측정
OverlayFS이미지 레이어를 하나의 통합 뷰로 제공, Copy-on-Write

다음 편에서는 이 아키텍처 위에서 이미지를 어떻게 효율적으로 빌드하는지 — Dockerfile 최적화와 멀티스테이지 빌드를 다룬다.

References