컨테이너 런타임 — Docker Engine, containerd, runc의 관계
docker run을 실행하면 Docker 데몬이 뭔가를 하고 컨테이너가 실행됩니다. 그런데 내부를 들여다보면 dockerd, containerd, runc라는 세 개의 프로세스가 관여합니다. 이들은 각각 무슨 일을 하는 걸까요?
Docker의 계층 구조
┌── 사용자 ──────────────────────────────────────────┐
│ docker CLI │
│ docker compose │
└──────────┬─────────────────────────────────────────┘
│ REST API (unix socket)
↓
┌── Docker 데몬 (dockerd) ──────────────────────────┐
│ • 이미지 빌드 (BuildKit) │
│ • 네트워크 관리 │
│ • 볼륨 관리 │
│ • API 서버 │
└──────────┬─────────────────────────────────────────┘
│ gRPC
↓
┌── containerd ─────────────────────────────────────┐
│ • 이미지 pull/push │
│ • 컨테이너 생명주기 관리 │
│ • 스냅샷(스토리지) 관리 │
│ • 태스크 관리 │
└──────────┬─────────────────────────────────────────┘
│ OCI Runtime Spec
↓
┌── runc ───────────────────────────────────────────┐
│ • namespace 생성 │
│ • cgroups 설정 │
│ • 프로세스 실행 │
│ • 실행 후 즉시 종료 (상주하지 않음) │
└───────────────────────────────────────────────────┘
각 컴포넌트의 역할
Docker CLI
사용자가 직접 상호작용하는 명령줄 도구입니다.
# CLI → dockerd로 REST API 요청
docker run nginx
# 실제로는 POST /containers/create + POST /containers/{id}/start
# Docker 소켓 직접 호출 (CLI 없이)
curl --unix-socket /var/run/docker.sock \
http://localhost/v1.44/containers/json | jq
dockerd (Docker Daemon)
Docker의 메인 데몬입니다. REST API를 제공하고, 빌드, 네트워크, 볼륨 등 고수준 기능을 관리합니다.
# dockerd 프로세스 확인
ps aux | grep dockerd
# root 1234 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
# 설정 파일
cat /etc/docker/daemon.json
핵심: dockerd는 컨테이너를 직접 실행하지 않습니다. containerd에 위임합니다.
containerd
CNCF 졸업 프로젝트로, Docker뿐만 아니라 Kubernetes에서도 직접 사용하는 고수준 컨테이너 런타임입니다.
# containerd 프로세스
ps aux | grep containerd
# root 2345 /usr/bin/containerd
# containerd 직접 제어 (ctr 명령어)
ctr images ls
ctr containers ls
ctr tasks ls
containerd가 담당하는 것:
- 이미지 pull/push (레지스트리 통신)
- 이미지를 스냅샷으로 변환 (OverlayFS 레이어 관리)
- 컨테이너 생성/삭제/시작/중지
- 컨테이너 이벤트 모니터링
- runc 같은 저수준 런타임 호출
runc
OCI Runtime Spec을 구현한 저수준 컨테이너 런타임입니다.
# runc로 직접 컨테이너 실행 (개념 이해용)
# 1. OCI 번들 준비 (rootfs + config.json)
mkdir -p bundle/rootfs
docker export $(docker create busybox) | tar xf - -C bundle/rootfs
runc spec --bundle bundle
# 2. config.json 편집 (namespace, cgroups 설정)
# 3. 실행
cd bundle
runc run my-container
핵심 특징:
- namespace와 cgroups를 직접 설정
- 프로세스를 실행한 후 runc 자체는 종료됨 (상주하지 않음)
- containerd-shim이 컨테이너 프로세스의 부모 역할을 이어받음
containerd-shim
runc가 종료된 후 컨테이너 프로세스의 부모 역할을 합니다.
containerd → runc(실행 후 종료) → containerd-shim(부모 역할) → 컨테이너 프로세스
shim의 역할:
- 컨테이너의 stdin/stdout 처리
- 컨테이너 종료 코드 보고
- containerd가 재시작되어도 컨테이너 유지
# shim 프로세스 확인
ps aux | grep containerd-shim
# root 3456 /usr/bin/containerd-shim-runc-v2 -namespace moby -id abc123...
OCI (Open Container Initiative) 표준
배경
Docker가 컨테이너 시장을 주도하면서, 벤더 종속을 방지하기 위해 2015년에 OCI가 설립되었습니다.
세 가지 표준
1. Runtime Spec — 컨테이너 실행 방법
// config.json (OCI 런타임 번들의 핵심 파일)
{
"ociVersion": "1.0.2",
"process": {
"terminal": false,
"user": { "uid": 0, "gid": 0 },
"args": ["nginx", "-g", "daemon off;"],
"env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"],
"cwd": "/"
},
"root": {
"path": "rootfs",
"readonly": false
},
"linux": {
"namespaces": [
{ "type": "pid" },
{ "type": "network" },
{ "type": "mount" },
{ "type": "ipc" },
{ "type": "uts" }
],
"resources": {
"memory": { "limit": 536870912 },
"cpu": { "shares": 1024 }
}
}
}
2. Image Spec — 이미지 형식
이미지 매니페스트, 설정, 레이어의 형식을 정의합니다. Docker 이미지와 OCI 이미지는 호환됩니다.
3. Distribution Spec — 이미지 배포
레지스트리 API를 표준화합니다. Docker Hub, GHCR, ECR 등이 이 표준을 따릅니다.
대안 런타임
Podman — Docker 호환 데몬리스 런타임
# Docker CLI와 거의 동일한 명령어
podman run -d --name web nginx
podman ps
podman images
특징:
- 데몬 없음: 각 명령이 독립적으로 실행. 데몬이 죽어도 컨테이너에 영향 없음
- Rootless 기본: 일반 사용자 권한으로 컨테이너 실행
- Systemd 통합: 컨테이너를 systemd 유닛으로 관리 가능
- Pod 지원: Kubernetes Pod 개념을 로컬에서 시뮬레이션
# Docker에서 Podman으로 전환 (별칭 설정)
alias docker=podman
# Kubernetes YAML 생성
podman generate kube myapp > myapp.yaml
CRI-O — Kubernetes 전용 런타임
Kubernetes (kubelet)
│
│ CRI (Container Runtime Interface)
↓
┌── CRI-O ──┐
│ │
│ runc │
└────────────┘
- Kubernetes CRI만 구현 (Docker API는 지원하지 않음)
- 경량화된 설계
- Red Hat/OpenShift의 기본 런타임
gVisor — 샌드박스 런타임
일반 컨테이너:
컨테이너 → 시스템콜 → 호스트 커널
gVisor 컨테이너:
컨테이너 → 시스템콜 → gVisor(사용자 공간 커널) → 제한된 호스트 시스템콜
# gVisor(runsc) 설치 후
docker run --runtime=runsc nginx
- Go로 작성된 사용자 공간 커널
- 약 200개의 시스템 콜을 재구현
- 호스트 커널 노출을 최소화하여 보안 강화
- 성능 오버헤드가 있음 (시스템 콜 집약적 워크로드)
- GCP의 Cloud Run에서 기본 사용
Kata Containers — 경량 VM 런타임
Kata Containers:
컨테이너 → 경량 VM(QEMU/Firecracker) → 전용 커널 → 호스트 커널
- 각 컨테이너가 경량 가상 머신 안에서 실행
- 진정한 커널 격리 (하드웨어 가상화)
- 보안이 매우 높지만 시작 시간과 리소스 오버헤드 존재
런타임 비교
| 런타임 | 격리 수준 | 시작 시간 | 리소스 오버헤드 | 호환성 |
|---|---|---|---|---|
| runc | namespace/cgroups | 매우 빠름 | 낮음 | 표준 |
| gVisor | 사용자 공간 커널 | 빠름 | 보통 | 대부분 호환 |
| Kata | 경량 VM | 보통 | 높음 | 대부분 호환 |
Kubernetes에서의 컨테이너 런타임
┌── Kubernetes ──────────────────────────────────┐
│ kubelet │
│ │ │
│ │ CRI (Container Runtime Interface) │
│ ├─→ containerd → runc │
│ ├─→ CRI-O → runc │
│ └─→ containerd → gVisor (runsc) │
└─────────────────────────────────────────────────┘
Kubernetes 1.24부터 dockershim이 제거되었습니다. 이제 containerd나 CRI-O를 직접 사용합니다.
# Kubernetes 노드의 컨테이너 런타임 확인
kubectl get nodes -o wide
# NAME STATUS ROLES VERSION CONTAINER-RUNTIME
# node-1 Ready <none> v1.29.0 containerd://1.7.13
프로세스 관계 확인
# Docker로 실행된 컨테이너의 프로세스 트리
pstree -p $(pgrep dockerd)
# dockerd(1234)
# └── (containerd는 별도 프로세스)
pstree -p $(pgrep -x containerd)
# containerd(2345)
# ├── containerd-shim(3456)
# │ └── nginx(4567) ← 컨테이너 프로세스
# └── containerd-shim(3457)
# └── postgres(4568)
정리
- Docker는 dockerd → containerd → runc 계층 구조로 동작합니다.
- dockerd는 API, 빌드, 네트워크 등 고수준 기능을, containerd는 이미지와 컨테이너 생명주기를, runc는 namespace/cgroups 설정과 프로세스 실행을 담당합니다.
- OCI 표준이 이미지 형식과 런타임을 표준화하여, 다양한 도구가 상호 운용 가능합니다.
- Podman은 데몬 없이 동작하는 Docker 호환 도구이고, CRI-O는 Kubernetes 전용 경량 런타임입니다.
- gVisor와 Kata Containers는 보안이 중요한 환경에서 추가 격리를 제공합니다.
- 이 계층 구조를 이해하면 "Docker가 없어도 컨테이너를 실행할 수 있다"는 말이 왜 사실인지 알게 됩니다.