Docker 아키텍처와 컨테이너 런타임
Docker는 단일 프로세스가 아니다
"Docker"라고 말할 때 사람들은 보통 docker run 명령어를 떠올린다. 하지만 그 뒤에서는 최소 세 개의 독립된 데몬이 협력한다. 이 아키텍처를 모르면 컨테이너가 왜 죽지 않는지, 왜 dockerd가 재시작해도 실행 중인 컨테이너가 살아남는지 이해하기 어렵다.
Docker가 2015~2017년에 걸쳐 모놀리식 구조를 해체한 데는 두 가지 이유가 있었다. 첫째, Kubernetes를 비롯한 오케스트레이터들이 Docker의 고수준 툴링 없이 컨테이너 런타임만 직접 호출하기를 원했다. 둘째, OCI(Open Container Initiative) 표준화로 런타임과 이미지 포맷을 교체 가능하게 만들 필요가 있었다.
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이 존재하는 이유는 데몬 독립성이다.
containerd가 재시작·업그레이드되어도 컨테이너는 계속 실행된다.- 컨테이너의
stdin/stdout/stderr스트림을 들고 있어docker attach가 동작한다. - 좀비 프로세스(zombie process)를 리핑(reaping)한다 — 컨테이너 PID 1이 종료한 자식 프로세스를 정리.
- OCI 런타임(runc)에 container 설정을 넘기고, runc는 실행 후 즉시 종료한다.
runc — 커널과 직접 대화하는 최소 런타임
runc는 OCI runtime-spec의 참조 구현이다. 주어진 config.json(OCI 번들)을 읽고:
clone()시스템 콜로 새 namespace를 가진 프로세스 생성- cgroup 계층 설정 (CPU·메모리·I/O 제한)
pivot_root()또는chroot()로 루트 파일시스템 교체- 네트워크 namespace에 veth 인터페이스 연결
- 컨테이너의 진입점(entrypoint) exec → 이후 runc 자체는 종료
docker run nginx
REST API 수신
이미지 pull
(없으면 레지스트리)
gRPC 위임
스냅샷(OverlayFS)
마운트 준비
fork → OCI 번들
config.json 생성
namespace·cgroup
루트FS 설정
runc 종료
(containerd 재시작해도 생존)
상태 보고 (running)
Linux 커널 격리 메커니즘
컨테이너는 VM과 달리 게스트 OS 커널이 없다. 격리는 전적으로 Linux 커널 기능에 의존한다.
Namespaces — "무엇을 볼 수 있는가"
| Namespace | 격리 대상 | 컨테이너 관점 |
|---|---|---|
pid | 프로세스 ID 공간 | 컨테이너 내 PID 1이 호스트의 PID 12345 |
net | 네트워크 인터페이스·라우팅 | 독립적인 eth0, 127.0.0.1 |
mnt | 마운트 포인트 | 독립적인 루트 파일시스템 |
uts | 호스트명·도메인 | 자체 hostname 설정 가능 |
ipc | System V IPC, POSIX 메시지 큐 | 격리된 IPC 자원 |
user | UID/GID 매핑 | 컨테이너 root → 호스트 비특권 UID |
cgroup | cgroup 루트 뷰 | 컨테이너가 자신의 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: 536870912OverlayFS — 이미지 레이어의 물리적 구현
Docker 이미지는 레이어 스택이다. OverlayFS는 이 스택을 하나의 파일시스템 뷰로 보여준다.
컨테이너 쓰기 레이어
(삭제 시 whiteout 파일)
nginx 설정 레이어 lowerdir[1] (R/O)
nginx 바이너리 레이어 lowerdir[2] (R/O)
debian:bookworm-slim 베이스
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 인터페이스에 연결된다.
- runc가 컨테이너에 새 network namespace(
net ns) 생성 - dockerd가 veth pair 생성: 한쪽은 컨테이너 내
eth0, 다른 쪽은 호스트의docker0에 연결 iptables(또는 nftables) 규칙으로 NAT 처리 → 컨테이너 → 외부 트래픽 가능-p 8080:80은iptables 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 | 컨테이너 당 하나, 데몬 재시작 시 컨테이너 생존 보장 |
| runc | OCI 번들 → 실제 프로세스 생성, 즉시 종료 |
| namespace | 프로세스·네트워크·파일시스템 등 격리 |
| cgroup v2 | CPU·메모리·I/O 자원 제한·측정 |
| OverlayFS | 이미지 레이어를 하나의 통합 뷰로 제공, Copy-on-Write |
다음 편에서는 이 아키텍처 위에서 이미지를 어떻게 효율적으로 빌드하는지 — Dockerfile 최적화와 멀티스테이지 빌드를 다룬다.
References
- containerd vs. Docker — Docker Blog
- Demystifying the Open Container Initiative (OCI) — Docker Blog
- Ultimate Guide to Container Runtimes: From Docker to RunC and Beyond — DEV Community
- How containers work: overlayfs — Julia Evans
- How to Understand Docker UnionFS and Overlay2 — OneUptime Blog
- 3 Types of Container Runtime and the Kubernetes Connection — Aqua Security
- Open Container Initiative