Docker container 는 "가벼운 VM" 이라는 흔한 설명이 사실 부정확. VM 은 별도 kernel 을 가지지만 container 는 host kernel 을 공유. 그러면 어떻게 한 컨테이너 안에서 ps 가 다른 컨테이너 의 프로세스를 못 보고, 한 컨테이너의 rm -rf / 가 host 를 안 망가뜨릴까? 답은 Linux kernel 의 namespace + cgroup. 이 가이드는 컨테이너의 실제 메카니즘을 정리한다.
컨테이너 = 평범한 프로세스 + 격리
# Host 에서
$ docker run -d nginx
$ ps aux | grep nginx
root 12345 /usr/sbin/nginx ← 실제로는 host 의 프로세스
PID 12345 (host 입장)
# Container 안에서
$ docker exec -it <id> ps aux
root 1 /usr/sbin/nginx ← 같은 프로세스가 PID 1 (container 입장)한 프로세스에 두 가지 PID — host 는 12345, container 안은 1. 어떻게 가능한가? PID namespace.
Namespace — 격리의 6 가지 차원
Linux kernel 이 제공하는 격리. 각 namespace 는 특정 자원의 view 를 독립적으로 가짐:
| Namespace | 격리 대상 | 효과 |
|---|---|---|
| PID | process tree | container 안 ps 는 own process 만 |
| NET | network stack (interface, route, iptables) | container 마다 lo, eth0 별도 |
| MNT | mount points (filesystem view) | container 마다 / 다름 |
| UTS | hostname + domain | container 마다 own hostname |
| IPC | inter-process communication | shared memory / semaphore 격리 |
| USER | UID / GID mapping | container 안 root 가 host 의 non-root 와 매핑 |
| CGROUP | cgroup view | (2016 추가) |
| TIME | system clock | (2020 추가) |
실험 (Linux):
$ unshare --pid --fork --mount-proc bash
# 새 PID namespace 의 bash. PID 1 부터 시작.
$ ps aux
USER PID ...
root 1 bash
root 2 ps
# 호스트의 다른 프로세스 안 보임cgroups — 자원 한도
Control Groups. namespace 가 "view" 격리라면 cgroup 은 "자원 할당" 격리:
- CPU — 컨테이너의 CPU 사용량 cap (예:
--cpus=2) - Memory — RAM 한도 + OOM 처리 (
--memory=512m) - Block I/O — disk read/write throttle
- Network I/O — bandwidth shaping (tc 와 결합)
- PID — fork bomb 방지 (max process count)
Linux 의 /sys/fs/cgroup/ 안 파일로 동작:
$ cat /sys/fs/cgroup/docker/abc123.../memory.max
536870912 ← 512 MB
$ cat /sys/fs/cgroup/docker/abc123.../cpu.max
200000 100000 ← 100ms 마다 200ms 사용 가능 (2 CPU)Docker run 시 --memory, --cpus 옵션이 이 파일에 write.
Union Filesystem — 가벼운 이미지의 비밀
Docker image 는 layer 의 stack:
Image: my-app:v1
├── Layer 4: COPY ./app /app (5 MB)
├── Layer 3: RUN npm install (200 MB)
├── Layer 2: COPY package.json /app (1 KB)
├── Layer 1: WORKDIR /app (0 bytes)
└── Layer 0: FROM node:20 (200 MB, base image)
Total: 405 MB
같은 host 에 my-app:v2 (= app code 만 변경) 추가 시:
├── NEW Layer 4' (5 MB)
├── Layer 3 (shared) (재사용 0 bytes)
├── Layer 2 (shared) (재사용 0 bytes)
├── Layer 1 (shared) (재사용 0 bytes)
└── Layer 0 (shared) (재사용 0 bytes)
Disk 증가: 5 MB 만Layer 는 read-only. 컨테이너 실행 시 최상단에 writable layer 추가 — copy-on-write (CoW):
Container 시작:
┌─────────────────────────┐
│ Writable layer (RW) │ ← 컨테이너의 변경 사항
├─────────────────────────┤
│ Image Layer 4 (RO) │
├─────────────────────────┤
│ Image Layer 3 (RO) │
├─────────────────────────┤
│ ... (RO) │
└─────────────────────────┘
파일 수정 시:
- read-only layer 의 파일을 writable layer 에 copy
- writable layer 에서 수정
- read 는 위→아래로 찾아 첫 번째 match모던 Docker 의 storage driver — overlayfs. Linux kernel 의 union filesystem 구현. /var/lib/docker/ overlay2/ 에서 디렉토리 단위로 관리.
Network — 4 가지 mode
- bridge (default) — Docker 가 만든 가상 bridge (docker0). container 마다 veth pair. NAT 통해 외부.
- host — host network namespace 직접 사용. 격리 X, 성능 최대.
- none — network 격리만, interface 없음. 보안 ↑.
- overlay — Swarm/Kubernetes 같은 multi-host cluster. VXLAN tunnel.
bridge 모드 흐름:
container1 (172.17.0.2) ── veth1 ── docker0 (172.17.0.1) ── NAT ── eth0 ── Internet
container2 (172.17.0.3) ── veth2 ─┘
container 간 통신 — docker0 통해 직접Docker daemon 의 역할
사용자 → docker CLI → dockerd (daemon, REST API)
↓
containerd (high-level runtime)
↓
runc (low-level runtime, OCI spec)
↓
Linux kernel (namespace + cgroup + overlayfs)실제 격리 작업은 runc (Open Container Initiative runtime). namespace 만들고, cgroup 설정하고, chroot 하고, exec. Docker 는 그 위의 사용자 편의 layer.
Docker 대체:
- Podman — daemon-less, rootless. 같은 OCI image 사용.
- containerd — Kubernetes 의 default runtime. Docker 보다 가벼움.
- CRI-O — Red Hat. Kubernetes 전용.
Dockerfile 의 build 단계
FROM node:20
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
CMD ["node", "server.js"]
각 줄이 새 layer:
1. node:20 base image pull
2. /app 디렉토리 (0 bytes)
3. package.json copy
4. npm install (deps install)
5. 나머지 코드 copy
6. metadata (CMD)
CACHE 활용:
- COPY package.json 이 변경 안 됐다면 RUN npm install 캐시 사용
- 그래서 package.json copy 를 npm install 전에 두는 게 best practiceMulti-stage build — image 크기 축소
# Stage 1 — builder
FROM node:20 AS builder
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build ← dist/ 생성
# Stage 2 — final runtime
FROM node:20-alpine ← Alpine 작음 (60 MB vs 1 GB)
WORKDIR /app
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/node_modules /app/node_modules
CMD ["node", "dist/server.js"]
# 결과 — builder 의 모든 dev deps, source code 안 박힘. 최종 image 가볍게VM 과의 차이
| VM (VirtualBox, VMware) | Container (Docker) | |
|---|---|---|
| Kernel | 별도 (Guest OS) | Host 공유 |
| 크기 | GB 단위 | MB 단위 |
| 시작 시간 | 분 단위 | 초 단위 (혹은 ms) |
| 격리 강도 | 매우 강 (hypervisor) | kernel namespace 단위 |
| OS 호환 | 임의 OS 가능 (Linux on Mac 등) | host OS 와 같은 kernel ABI |
| resource overhead | 높음 (10-20%) | 거의 없음 (1-2%) |
Mac / Windows 의 Docker Desktop — host 위에 Linux VM ( HyperKit / WSL2) 위에 Docker. 그래서 macOS 의 Docker container 도 Linux 환경.
Container 의 한계 — 격리 강도
Container 가 VM 만큼 강하게 격리 X. 위험:
- kernel exploit — host kernel 의 bug → 모든 container 영향. VM 은 hypervisor 가 보호.
- root in container = host root 가능 — USER namespace 안 쓰면 container 의 root 가 host 의 root mapping. breakout 위험.
- shared resources — /dev, /proc 일부 노출. 잘못 mount 시 host 정보 노출.
해결:
--user로 non-root 실행- rootless Docker / Podman
- gVisor (Google) — application kernel 으로 한 단계 더 격리
- Kata Containers — micro VM 안 container (격리 강 + container UX)
흔한 함정
1. PID 1 의 책임
Linux 의 PID 1 는 special — orphan process 의 부모, signal 의 default handler. CMD ["bash", "-c", "node server.js"] 같이 shell 으로 wrap 하면 bash 가 PID 1 → server 종료 시 signal forward X. tini 또는 직접 exec.
2. layer 폭주
# Bad — 매 RUN 이 새 layer
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN rm -rf /var/lib/apt/lists/*
# 결과 — 4 layer + 중간 cache 가 image 에 영구 포함
# Good — chain
RUN apt-get update && \
apt-get install -y curl vim && \
rm -rf /var/lib/apt/lists/*
# 1 layer, no cache leftovers3. .dockerignore 누락
COPY . . 가 .git / node_modules / .env 모두 image 에 포함. .dockerignore 로 명시적 제외:
# .dockerignore
node_modules
.git
.env
*.log
.DS_Store4. host filesystem mount 의 권한
# host UID 1000, container UID 100 (alpine)
docker run -v $(pwd):/data alpine touch /data/file
↓
UID 100 으로 file 생성
host 에서 보면 UID 100 — 권한 충돌
해결:
docker run -u $(id -u):$(id -g) -v $(pwd):/data ...5. dev image 를 production 으로
FROM node:20 1 GB. FROM node:20-alpine 60 MB. production 은 distroless / Alpine. dev 에서 일반 이미지 쓰더라도 final stage 는 가벼움.
참고 자료
- Linux man — namespaces(7) — man7.org
- OCI Runtime Spec — GitHub
- Container from scratch (Liz Rice 영상) — YouTube
- Overlay filesystem — kernel.org
요약
- Container = host kernel 을 공유하는 평범한 프로세스 + Linux namespace + cgroup 격리.
- Namespace 8 종 (PID/NET/MNT/UTS/IPC/USER/CGROUP/TIME) 이 view 격리.
- cgroup 이 CPU / Memory / I/O / PID 자원 한도. /sys/fs/cgroup/ 파일로 control.
- Image = read-only layer stack + writable layer. Overlayfs 가 copy-on-write.
- Docker daemon = runc + containerd 위의 user 편의 layer. Podman / containerd / CRI-O 대안.
- Multi-stage build 로 image 크기 축소. .dockerignore 로 불필요 파일 제외.
- Container ≠ VM. kernel 공유 = 가볍지만 격리 약함. 보안 민감 환경은 gVisor / Kata Containers.
- 실험 — Docker Compose 시각화 / Kubernetes YAML 시각화 로 docker- compose / k8s manifest 시각화.