Skip to content
Published on

컨테이너와 Docker 내부 완전 정복 — Namespace, cgroups, OverlayFS, seccomp, Capabilities 그리고 Kubernetes까지 (2025)

Authors

0. "컨테이너는 VM 이 아니다" 가 의미하는 것

많은 개발자가 컨테이너를 "가벼운 VM" 으로 이해한다. 실제로는 완전히 다르다:

항목VMContainer
격리 단위하드웨어OS 네임스페이스
커널VM 마다 독립호스트와 공유
부팅 시간수 초 ~ 수 분수십 ms
메모리 오버헤드수백 MB수 MB
디스크 오버헤드GBMB
격리 강도강력약함

컨테이너 = 프로세스 + 커널 기능들 (namespace, cgroup, rootfs) 의 조합. 별도 OS 가 있는 게 아니다.

이 글은 docker run nginx 라는 한 줄이 실제로 리눅스 커널에서 어떻게 작동하는지, 그리고 그 뒤에 숨은 10년 넘는 엔지니어링 역사를 파헤친다.

1. 컨테이너의 짧지 않은 역사

1.1 1979 — chroot

Unix v7 의 chroot() 시스템 콜. "이 프로세스의 파일 시스템 루트를 바꾼다" → 제한된 디렉터리만 보임.

하지만 chroot 만으로는 부족:

  • 프로세스 목록, 네트워크, 사용자는 그대로 보임.
  • 탈출 기법 잘 알려짐 (chdir, 마운트 등).

1.2 2000 — FreeBSD Jails

chroot 확장판. 네트워크, 프로세스, 사용자까지 격리. 진짜 "경량 가상화" 의 시작.

1.3 2004 — Solaris Zones

완전한 격리 기술. 컨테이너 개념의 상업적 첫 성공.

1.4 2006 — cgroups, 2007 — LXC

Google 엔지니어들이 내부 자원 격리를 위해 "process containers" 개발 → cgroups 로 Linux 에 도입. 네임스페이스도 단계적 추가.

LXC (Linux Containers) 가 이들을 조합해 첫 실용 컨테이너 런타임 제공.

1.5 2013 — Docker

Solomon Hykes 의 dotCloud 에서 LXC 를 쉬운 API 로 포장. docker run 한 줄의 마법.

킬러 기능: 이미지 레이어 (OverlayFS) + 레지스트리 (Docker Hub). 개발자가 "내 환경을 그대로 공유" 가능.

1.6 2015 — OCI, 2016 — containerd, 2017 — Kubernetes 급부상

Open Container Initiative (Docker + CoreOS + Linux Foundation) 가 이미지/런타임 표준 규격 (OCI) 확립.

Docker 가 런타임을 containerd 로 분리. 작고 재사용 가능한 컴포넌트로 진화.

Kubernetes 가 오케스트레이션 표준이 되며 "docker 엔진" 은 점점 추상화됨.

1.7 2024 — 현재

Docker Desktop, Podman (daemonless), Kubernetes + containerd + runc, ECS, Cloud Run, Fly.io. 컨테이너는 이제 모든 클라우드 배포의 기본 단위.

2. Linux Namespace — 격리의 7가지 차원

네임스페이스는 "이 프로세스가 보는 OS 자원" 을 제한한다. 7가지 종류 (2024 기준):

2.1 PID Namespace

프로세스 ID 격리. 컨테이너 안에서 ps 하면:

PID TTY      TIME CMD
  1 ?     00:00:00 nginx
 10 ?     00:00:00 worker
 11 ?     00:00:00 worker

컨테이너의 nginx 가 PID 1. 호스트에서 보면 그건 PID 12345. 서로 다른 숫자 공간이다.

PID 1 의 특별함:

  • 커널이 좀비 리핑 (init 역할) 을 기대.
  • 종료 시 모든 자식도 종료.
  • 대부분 앱은 init 으로 쓰도록 설계 안 됨 → tini, dumb-init 같은 작은 init.

2.2 Mount Namespace

파일 시스템 마운트 포인트 격리. 각 컨테이너는 자기만의 / 트리를 가진다.

chroot 의 진화형. 호스트의 특정 마운트만 공유 가능.

2.3 Network Namespace

각 컨테이너가 자기 네트워크 인터페이스, 라우팅 테이블, iptables 규칙.

# 호스트에서 컨테이너 네트워크 진입
sudo nsenter -t <container_pid> -n ip addr

Docker 의 bridge 모드: 가상 브리지 docker0 + veth 쌍으로 연결.

2.4 UTS Namespace

Unix Timesharing System. 호스트네임 + 도메인 격리. 컨테이너가 자기 호스트네임 설정 가능.

2.5 IPC Namespace

System V IPC, POSIX 메시지 큐 격리. 컨테이너 간 공유 메모리 분리.

2.6 User Namespace

UID/GID 매핑. 컨테이너의 root (UID 0) 이 호스트에서는 비특권 사용자 UID 100000.

보안에 결정적이지만 설정 복잡 → Docker 는 기본적으로 활성화 안 함. Podman rootless 가 활용.

2.7 Cgroup Namespace

cgroup 계층을 격리. 컨테이너가 자기 cgroup 경로를 / 로 봄. 2016년 도입.

2.8 Time Namespace

Linux 5.6 (2020) 신규. 각 컨테이너가 다른 부팅 시간 (CLOCK_BOOTTIME) 가질 수 있음. 체크포인트/복원 (CRIU) 에 활용.

2.9 네임스페이스 조작

unshare --pid --mount --net --fork bash   # 새 네임스페이스로 bash
nsenter -t PID -n -p                       # 기존 네임스페이스 진입

Docker 의 docker exec 는 사실 대상 컨테이너의 네임스페이스들을 조합해 새 프로세스 생성.

3. cgroups — 자원 제한

3.1 cgroups v1 (2007-)

각 자원 (CPU, 메모리, 네트워크 etc) 마다 별도 계층:

/sys/fs/cgroup/
├── cpu/
│   ├── docker/
│   │   └── <container_id>/
│   │       ├── cpu.cfs_quota_us
│   │       └── cpu.cfs_period_us
├── memory/
├── blkio/
└── ...

각 계층 독립 → 복잡.

3.2 cgroups v2 (2016-, systemd 232+)

단일 통합 계층:

/sys/fs/cgroup/
├── user.slice/
├── system.slice/
│   └── docker-<id>.scope/
│       ├── cpu.max         # "100000 100000" = 1 CPU
│       ├── memory.max      # "536870912" = 512MB
│       ├── io.max
│       └── pids.max
  • 하나의 트리에 모든 자원.
  • 계층적 리소스 배분.
  • 더 단순한 모델.

3.3 CPU 제한의 실제

cpu.max = "50000 100000" 의미: "100ms 주기마다 최대 50ms CPU 사용" = 0.5 CPU.

하지만 버스팅 허용 은 설정에 따라. JVM, Node 가 짧게 CPU 폭주하면 throttle 걸림 → 응답 지연.

Docker의 --cpus=1 = cpu.max = "100000 100000".

3.4 메모리 제한

  • memory.max: 상한. 초과 시 OOM.
  • memory.high: soft limit. 넘으면 reclaim 압박.
  • memory.min, memory.low: 보호 메모리.

컨테이너가 memory.max 에 도달하면 컨테이너 내부 OOM killer 발동. 호스트는 멀쩡.

3.5 JVM, Node, Go 의 cgroup 인식

JVM 10+ (-XX:+UseContainerSupport 기본): cgroup 한도를 읽어 힙 설정. Go 1.19+ GOMEMLIMIT: 수동으로 cgroup limit 반영. Node --max-old-space-size: 직접 설정.

이걸 안 하면 "호스트 메모리 64GB" 로 오해하고 힙을 크게 → OOM.

4. OverlayFS — 이미지 레이어의 비밀

4.1 왜 레이어인가

Docker 이미지는 읽기 전용 레이어의 스택:

Layer 4 (10KB): 앱 코드
Layer 3 (50MB): npm install 결과
Layer 2 (80MB): apt packages
Layer 1 (70MB): ubuntu base
  • 각 레이어는 독립 + 불변.
  • 여러 이미지가 base 레이어 공유.
  • 100 GB 이미지 있어도 변경된 top layer 만 push/pull.

4.2 OverlayFS 의 구조

┌─────────────────────┐
upperdir (RW)      │  ← 컨테이너 쓰기
├─────────────────────┤
│  merged view        │  ← 앱이 보는 파일시스템
├─────────────────────┤
lowerdir (RO) ×N   │  ← 이미지 레이어들
└─────────────────────┘
  • 읽기: upperdir 에 없으면 lowerdir 에서 찾음.
  • 쓰기: 항상 upperdir 에.
  • 수정: Copy-up — 원본을 upperdir 에 복사 후 수정.

4.3 Copy-up 의 비용

큰 파일 수정 시:

  • 원본을 upperdir 에 전체 복사.
  • 수정은 그 후.

100MB 파일에 1바이트 수정 = 100MB 복사. 대용량 DB 파일을 컨테이너 내부에 두면 안 되는 이유.

해결: Volume mount. DB 데이터는 호스트 볼륨으로.

4.4 Whiteout — 파일 삭제

rm 한다고 lowerdir 에서 지워지지 않음. 대신:

  • upperdir 에 특수 "whiteout" 파일 생성.
  • merged view 에서 해당 파일 안 보임.
  • 실제 파일은 여전히 아래에.

결과: "Dockerfile 에서 RUN rm 으로 용량 줄이려 해도" 레이어 크기 안 줄어든다. 한 RUN 안에서 생성+삭제 해야 레이어에 안 남음.

4.5 StorageDriver 진화

Docker 의 storage driver 변천사:

  • aufs (2013-): 초기. 커널 upstream 없음.
  • devicemapper (2014-): thin provisioning. 느림.
  • btrfs, zfs: 파일시스템 의존.
  • overlay (2014): upstream 커널, 빠름.
  • overlay2 (2016-): 현재 기본. 여러 lowerdir 동시 지원.

Podman 은 fuse-overlayfs (rootless 지원) 도 옵션.

5. 컨테이너 런타임 — runc 와 친구들

5.1 Docker 의 내부 계층

docker CLI
dockerd (daemon)
containerd (컨테이너 lifecycle)
containerd-shim (각 컨테이너당 1)
runc (OCI runtime, 실제 컨테이너 생성)
Linux kernel (namespace, cgroup, ...)

5.2 runc 의 역할

OCI Runtime Specification 구현. 입력: rootfs + config.json. 출력: 실행 중인 컨테이너.

C / Go 로 구현, 순수 시스템 콜 호출:

  • clone() + namespace flags.
  • setns() 로 네임스페이스 진입.
  • cgroup 설정.
  • rootfs 마운트.
  • execve() 로 사용자 프로세스 시작.

5.3 대안 런타임들

  • crun: C 로 작성, runc 보다 빠름 (Go 런타임 오버헤드 없음).
  • youki: Rust 구현.
  • Kata Containers: 각 컨테이너를 초경량 VM 안에서 실행 → VM 급 격리.
  • gVisor: Google. 유저 공간 커널로 시스템 콜 가로챔 → 커널 격리.
  • Firecracker: AWS Lambda 기반. microVM.

보안이 중요한 멀티테넌트 환경 (Lambda, Fargate) 은 VM 격리 런타임을 씀.

5.4 shim 의 역할

containerd-shim 이 컨테이너마다 존재:

  • containerd 데몬이 죽어도 컨테이너는 살아남음.
  • stdout/stderr 수집.
  • exit code 기록.

Docker 가 kubelet 에 직접 연결되지 않는 이유. kubelet → CRI → containerd → shim → runc 구조.

6. 보안 — 컨테이너가 "진짜 격리" 가 아닌 이유

6.1 공유 커널의 위험

컨테이너들이 같은 커널을 공유 → 커널 취약점 = 모든 컨테이너 탈출. 실제 사례:

  • Dirty COW (CVE-2016-5195): 권한 상승. 컨테이너 탈출 가능.
  • runc CVE-2019-5736: runc 바이너리 덮어쓰기로 호스트 root 탈취.
  • Leaky Vessels (2024): runc, containerd 다수 취약점.

대응: 커널 자주 업데이트, AppArmor/SELinux, seccomp 프로필.

6.2 Linux Capabilities

전통적 Unix: root (UID 0) = 전체 권한. 컨테이너에는 너무 위험.

Capabilities 가 root 권한을 세분화:

  • CAP_NET_ADMIN: 네트워크 설정.
  • CAP_SYS_ADMIN: 대부분의 위험한 것들.
  • CAP_NET_BIND_SERVICE: 1024 이하 포트 바인딩.
  • ... 수십 가지.

Docker 기본: 제한된 capability set (최소 권한). --cap-add, --cap-drop 로 조정. --privileged 는 모든 cap = 호스트 root 와 거의 동등.

6.3 seccomp — 시스템 콜 필터

"이 컨테이너는 특정 시스템 콜만 허용":

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    { "names": ["read", "write", "open", ...], "action": "SCMP_ACT_ALLOW" }
  ]
}

Docker 의 기본 seccomp profile 이 약 300 개 syscall 만 허용. keyctl, ptrace, mount 등은 차단.

6.4 AppArmor / SELinux

파일 시스템 레벨 MAC (Mandatory Access Control). 컨테이너가 시도할 수 있는 파일 접근을 정책으로 제한.

Ubuntu 는 AppArmor, RHEL 은 SELinux.

6.5 Rootless Container

컨테이너 데몬도 비특권 사용자로 실행. User namespace 로 UID 매핑:

  • Podman 의 기본.
  • Docker 도 rootless 모드 지원.
  • 보안 대폭 강화, 일부 기능 제한 (1024 이하 포트 등).

6.6 보안 체크리스트

  • 최신 커널.
  • seccomp 기본 프로필 유지 (--security-opt seccomp=unconfined 금지).
  • capability 최소화 (--cap-drop ALL + 필요한 것만 add).
  • Non-root 사용자로 앱 실행 (USER 지시어).
  • Read-only rootfs (--read-only + tmpfs 볼륨).
  • Image scanning: Trivy, Grype, Docker Scout.
  • Runtime monitoring: Falco.

7. 이미지 만들기의 기술

7.1 레이어 최적화

# 나쁨: 각 RUN 이 새 레이어
FROM node:20
COPY package.json .
RUN npm install             # 레이어 1
COPY . .
RUN npm run build           # 레이어 2
RUN rm -rf /tmp/cache       # 레이어 3 (1번의 cache 는 그대로 남음)
# 좋음: 임시 파일을 같은 RUN 에서
FROM node:20
COPY package.json .
RUN npm install && rm -rf /tmp/* /var/cache/*
COPY . .
RUN npm run build

7.2 Multi-stage Build

# Stage 1: 빌드
FROM node:20 AS builder
WORKDIR /app
COPY . .
RUN npm install && npm run build

# Stage 2: 런타임 (소형)
FROM node:20-alpine
COPY --from=builder /app/dist /app
COPY --from=builder /app/node_modules /app/node_modules
CMD ["node", "/app/index.js"]

빌드 도구 (gcc, webpack 등) 는 최종 이미지에 없음 → 크기 5배+ 감소.

7.3 Distroless 이미지

Google 이 만든 최소 이미지 (shell 조차 없음):

FROM gcr.io/distroless/nodejs20
COPY --from=builder /app /app
CMD ["/app/index.js"]
  • 크기: 수십 MB.
  • 공격 표면: shell 없어서 RCE 어려움.
  • 단점: 디버깅 어려움 (exec 들어갈 수 없음).

7.4 Alpine vs Debian

  • Alpine (node:20-alpine): 5MB base. musl libc.
  • Debian slim (node:20-slim): 80MB. glibc.

Alpine 주의: musl 호환성 문제 (DNS resolver, pthread 동작 미세 차이). Python 의 C extensions 빌드 시 추가 도구 필요.

7.5 BuildKit 과 캐시

DOCKER_BUILDKIT=1 docker build .
  • 병렬 스테이지 빌드.
  • 캐시 내보내기/가져오기: --cache-to=type=registry,ref=....
  • 보안: 빌드 타임 secrets (--secret).
  • BuildKit 은 containerd 네이티브.

8. 네트워킹 — docker0 부터 CNI 까지

8.1 Docker 기본 브리지

docker0 (bridge, 172.17.0.1)
  ├── veth0 → container1 eth0 (172.17.0.2)
  ├── veth1 → container2 eth0 (172.17.0.3)
  └── veth2 → container3 eth0 (172.17.0.4)
  • veth 쌍: 한쪽 호스트, 다른쪽 컨테이너.
  • NAT: iptables MASQUERADE 로 외부 연결.
  • 포트 포워딩: -p 80:8080 은 iptables DNAT.

8.2 네트워크 모드

  • bridge (기본): 위 설명.
  • host: 호스트 네트워크 그대로 (격리 없음, 가장 빠름).
  • none: 네트워크 없음.
  • container: 다른 컨테이너와 공유 (pod 와 유사).

8.3 Kubernetes CNI

Kubernetes 는 "모든 pod 가 평면적 IP 를 가진다" 는 철학. Docker bridge 로는 부족.

  • Flannel: VXLAN 터널링, 단순.
  • Calico: BGP 기반, eBPF 지원, 네트워크 정책.
  • Cilium: eBPF 네이티브, 관찰성/보안 풍부.
  • AWS VPC CNI: ENI 를 pod 에 직접 할당.

CNI = Container Network Interface. 오케스트레이터가 네트워크 플러그인을 교체 가능하게 하는 표준.

9. Kubernetes 와의 통합

9.1 왜 Kubernetes 가 Docker 를 버렸는가 (2020)

Kubernetes 1.20 에서 "dockershim deprecation" 공지 → 1.24 (2022) 에서 제거.

이유:

  • Docker 엔진은 kubelet 이 쓰지 않는 기능 다수 포함 (이미지 빌드, swarm 등).
  • CRI (Container Runtime Interface) 표준에 맞추기 위한 별도 shim 계층 필요.
  • containerd 가 이미 Docker 의 core 였음.

결과: kubelet → CRI → containerd 직접. 더 단순, 더 빠름. 사용자 관점에서 OCI 이미지는 그대로 작동.

9.2 Pod = 네임스페이스를 공유하는 컨테이너들

Pod 는 여러 컨테이너가 network, IPC, UTS 네임스페이스를 공유. Mount, PID 는 (기본) 독립.

pause container (네임스페이스 소유자)
  ├── app container
  └── sidecar container

pause 는 아무 것도 안 하는 미니 프로세스. 네임스페이스만 유지.

9.3 Init container 와 sidecar

  • Init container: Pod 시작 전 순차 실행.
  • Sidecar (native 2023+): 앱과 나란히 실행되며 독립 재시작 가능.

Envoy (Istio), Fluent Bit 같은 보조 프로세스가 sidecar 로.

9.4 오케스트레이션의 의미

  • Scheduling: 어느 노드에 실행?
  • Health check, auto-restart.
  • Rolling update, canary.
  • Service discovery, load balancing.
  • Storage, Config, Secret.
  • Horizontal autoscaling.

Kubernetes 는 이 모든 걸 declarative YAML 로.

10. 실전 팁

10.1 디버깅 툴킷

docker ps -a                    # 모든 컨테이너
docker logs -f <container>      # 로그 stream
docker exec -it <container> sh  # 진입
docker inspect <container>      # 상세 정보 (JSON)
docker stats                    # 실시간 자원 사용
docker events                   # 이벤트 stream

Kubernetes:

kubectl logs -f <pod>
kubectl exec -it <pod> -- sh
kubectl describe pod <pod>
kubectl get events --sort-by='.lastTimestamp'

10.2 Distroless 에서 디버깅

shell 이 없어서 exec 진입 불가. 대안:

kubectl debug <pod> --image=busybox --target=app
# 또는
docker run --rm -it --pid=container:<id> --net=container:<id> busybox

pid/net 네임스페이스를 공유해서 디버깅 컨테이너로 접근.

10.3 이미지 크기 분석

  • docker history <image>: 레이어별 크기.
  • dive tool: 레이어 내용 탐색.
  • docker image ls --format="..." + 포맷팅.

10.4 성능 관찰

# 컨테이너 자원 사용
docker stats --no-stream

# cgroup 직접 보기
cat /sys/fs/cgroup/memory.current
cat /sys/fs/cgroup/cpu.stat

# perf 로 프로파일
docker run --cap-add SYS_ADMIN --pid host ...

11. 마치며 — docker run 한 줄의 깊이

docker run nginx 한 줄이 실제로 하는 일:

  1. Docker Hub 에서 nginx 이미지 레이어 다운로드.
  2. OverlayFS 로 레이어 스택 조립.
  3. containerd 가 containerd-shim 생성.
  4. shim 이 runc 실행.
  5. runc 가 7개 네임스페이스 + cgroup + rootfs 설정.
  6. seccomp/AppArmor 프로필 적용.
  7. execve("nginx") 로 프로세스 시작.
  8. 컨테이너 네트워크 (veth + bridge) 구성.
  9. iptables 포트 포워딩 추가.

50ms 안에 끝난다. 2000년대 VM 을 부팅하던 수 분과 비교하면 기적이다. 그 뒤에는 2006 cgroups, 2008 namespaces, 2013 Docker, 2015 OCI, 2016 overlay2, 2020 CRI 등 10년 넘는 엔지니어링이 있다.

동시에 이 편의성의 대가는 커널 공유 = 보안 약점. Lambda, Fargate 같은 상용 플랫폼이 microVM 을 쓰는 이유. 멀티테넌트라면 "컨테이너 = 격리" 를 전적으로 신뢰하지 말 것.

다음 글에서는 Kubernetes 내부 — etcd 의 Raft 합의, 스케줄러의 필터/스코어링, 컨트롤러 패턴, 커스텀 리소스, CRI/CNI/CSI 의 플러그인 시스템 — 을 파볼 예정이다. YAML 한 장이 수백 대 노드에서 실행되는 과정.

참고 자료

  • Jérôme Petazzoni — "Anatomy of a Container: Namespaces, cgroups & some filesystem magic" (LinuxCon 2015).
  • Michael Kerrisk — "Understanding Linux Namespaces" (LWN 시리즈).
  • OCI Image/Runtime Specifications (GitHub).
  • Julia Evans — "Container Networking" 시리즈.
  • Liz Rice — "Container Security" (O'Reilly, 2020).
  • Kubernetes 공식 문서 — Pods, CNI, CRI.
  • Red Hat Crun 블로그.
  • Firecracker 논문 (NSDI 2020).
  • Google gVisor 논문.
  • "The Kubernetes Book" — Nigel Poulton.