본문으로 건너뛰기
yutils

Docker 컨테이너는 어떻게 격리될까?

컨테이너는 가벼운 VM 이 아닌, namespace 와 cgroup 으로 격리된 Linux 프로세스. namespace · cgroup · overlay 파일시스템 · image layer · Docker daemon 의 실제 동작을 풀어본다.

약 9분 읽기

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격리 대상효과
PIDprocess treecontainer 안 ps 는 own process 만
NETnetwork stack (interface, route, iptables)container 마다 lo, eth0 별도
MNTmount points (filesystem view)container 마다 / 다름
UTShostname + domaincontainer 마다 own hostname
IPCinter-process communicationshared memory / semaphore 격리
USERUID / GID mappingcontainer 안 root 가 host 의 non-root 와 매핑
CGROUPcgroup view(2016 추가)
TIMEsystem 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 practice

Multi-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 leftovers

3. .dockerignore 누락

COPY . . 가 .git / node_modules / .env 모두 image 에 포함. .dockerignore 로 명시적 제외:

# .dockerignore
node_modules
.git
.env
*.log
.DS_Store

4. 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 는 가벼움.

참고 자료

요약

  • 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 시각화.
가이드 목록으로