Skip to content

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

|

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

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.

Container & Docker Internals Deep Dive — Namespace, cgroups, OverlayFS, seccomp, Capabilities and Kubernetes (2025)

0. "A container is not a VM"

Many engineers think of containers as "lightweight VMs". They are not:

AspectVMContainer
Isolation unitHardwareOS namespace
KernelPer-VMShared with host
Boot timeSeconds to minutesTens of ms
Memory overheadHundreds of MBA few MB
Disk overheadGBMB
Isolation strengthStrongWeaker

Container = a process plus kernel features (namespace, cgroup, rootfs). There is no separate OS inside.

This article unpacks what docker run nginx actually does in the Linux kernel, and the decade-plus of engineering history behind it.

1. A short history of containers

1.1 1979 — chroot

Unix v7's chroot() syscall. "Change the filesystem root for this process" so it only sees a restricted directory. Not enough on its own: processes, network, users remained visible, and escape tricks (chdir, mount) were well known.

1.2 2000 — FreeBSD Jails

chroot extended to isolate network, processes, and users. The real beginning of "lightweight virtualization".

1.3 2004 — Solaris Zones

A fully isolated container concept — the first commercially successful one.

1.4 2006 — cgroups, 2007 — LXC

Google engineers built "process containers" for internal resource isolation, which became Linux cgroups. Namespaces were added in stages. LXC (Linux Containers) combined these into the first practical container runtime.

1.5 2013 — Docker

Solomon Hykes's dotCloud wrapped LXC in a friendly API — the magic of docker run. Killer features: image layers (OverlayFS) plus a registry (Docker Hub). Engineers could "ship their environment" for the first time.

1.6 2015 — OCI, 2016 — containerd, 2017 — Kubernetes

The Open Container Initiative (Docker + CoreOS + Linux Foundation) standardized image and runtime specs. Docker split the runtime out as containerd. Kubernetes became the orchestration standard, abstracting away the Docker engine.

1.7 2024 — Today

Docker Desktop, Podman (daemonless), Kubernetes + containerd + runc, ECS, Cloud Run, Fly.io. Containers are the default unit of cloud deployment.

2. Linux Namespaces — 7 dimensions of isolation

Namespaces restrict "which OS resources this process can see". 7 kinds as of 2024:

2.1 PID Namespace

Process-ID isolation. Inside a container, ps shows:

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

The container's nginx is PID 1. On the host, it might be PID 12345 — different number spaces. PID 1 is special: the kernel expects it to reap zombies (init role), and its termination kills all children. Most apps are not designed as init, so small inits like tini or dumb-init are used.

2.2 Mount Namespace

Isolates mount points. Each container has its own / tree — an evolution of chroot. Specific host mounts can be shared.

2.3 Network Namespace

Each container has its own interfaces, routing table, and iptables rules.

# Enter a container's network namespace from the host
sudo nsenter -t <container_pid> -n ip addr

Docker bridge mode: virtual bridge docker0 plus veth pairs.

2.4 UTS Namespace

Unix Timesharing System — isolates hostname and domain.

2.5 IPC Namespace

Isolates System V IPC and POSIX message queues.

2.6 User Namespace

UID/GID mapping. The container's root (UID 0) can map to an unprivileged host UID like 100000. Critical for security but complex — Docker does not enable it by default; Podman rootless relies on it.

2.7 Cgroup Namespace

Isolates the cgroup hierarchy so the container sees its own cgroup path as /. Introduced in 2016.

2.8 Time Namespace

New in Linux 5.6 (2020). Each container can have its own CLOCK_BOOTTIME. Used by checkpoint/restore (CRIU).

2.9 Manipulating namespaces

unshare --pid --mount --net --fork bash   # new bash in new namespaces
nsenter -t PID -n -p                       # enter existing namespaces

docker exec is essentially creating a new process that joins the target container's namespaces.

3. cgroups — resource limits

3.1 cgroups v1 (2007-)

A separate hierarchy per resource (CPU, memory, network, etc):

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

Independent trees made configuration complex.

3.2 cgroups v2 (2016-, systemd 232+)

A single unified hierarchy:

/sys/fs/cgroup/
├── user.slice/
├── system.slice/
│   └── docker-<id>.scope/
│       ├── cpu.max         # "100000 100000" = 1 CPU
│       ├── memory.max      # "536870912" = 512MB
│       ├── io.max
│       └── pids.max

One tree, hierarchical allocation, simpler model.

3.3 CPU limits in practice

cpu.max = "50000 100000" means "up to 50ms of CPU every 100ms period" = 0.5 CPU. JVM or Node bursts can be throttled, causing tail latency. Docker's --cpus=1 maps to cpu.max = "100000 100000".

3.4 Memory limits

  • memory.max: hard cap. OOM on overflow.
  • memory.high: soft limit triggering reclaim pressure.
  • memory.min, memory.low: protected memory.

Hitting memory.max triggers an in-container OOM killer — the host stays healthy.

3.5 cgroup-aware runtimes

JVM 10+ reads cgroup limits (-XX:+UseContainerSupport default). Go 1.19+ has GOMEMLIMIT. Node uses --max-old-space-size. Without these settings runtimes assume host memory (e.g. 64GB) and OOM quickly.

4. OverlayFS — the secret of image layers

4.1 Why layers

A Docker image is a stack of read-only layers:

Layer 4 (10KB): app code
Layer 3 (50MB): npm install output
Layer 2 (80MB): apt packages
Layer 1 (70MB): ubuntu base

Each layer is independent and immutable; images share base layers; only the changed top layer needs push/pull.

4.2 OverlayFS structure

┌─────────────────────┐
upperdir (RW)      │  ← container writes
├─────────────────────┤
│  merged view        │  ← what the app sees
├─────────────────────┤
lowerdir (RO) ×N   │  ← image layers
└─────────────────────┘

Reads fall through to lowerdir; writes go to upperdir; modifications trigger copy-up (copy the original to upperdir before editing).

4.3 Copy-up cost

Editing 1 byte of a 100MB file copies 100MB. That is why large DB files do not belong inside a container — use volume mounts.

4.4 Whiteouts — deletion

rm does not actually remove from lowerdir. Instead a special whiteout file is placed in upperdir so the merged view hides it — but the original still occupies the layer. That is why RUN rm in a later Dockerfile step does not shrink the image; delete in the same RUN that created the file.

4.5 Storage driver evolution

  • aufs (2013-): early, not upstream.
  • devicemapper (2014-): thin provisioning, slow.
  • btrfs, zfs: filesystem dependent.
  • overlay (2014): upstream, fast.
  • overlay2 (2016-): current default, multiple lowerdirs.

Podman also offers fuse-overlayfs for rootless mode.

5. Container runtimes — runc and friends

5.1 Docker's internal layers

docker CLI
dockerd (daemon)
containerd (container lifecycle)
containerd-shim (one per container)
runc (OCI runtime, actually creates containers)
Linux kernel (namespace, cgroup, ...)

5.2 runc's job

Implements the OCI Runtime Specification. Input: rootfs + config.json. Output: a running container. Written in Go/C, it calls syscalls directly: clone() with namespace flags, setns(), cgroup setup, rootfs mount, execve().

5.3 Alternative runtimes

  • crun: C implementation, faster than runc (no Go runtime).
  • youki: Rust implementation.
  • Kata Containers: each container in a microVM — VM-grade isolation.
  • gVisor: Google userspace kernel that intercepts syscalls.
  • Firecracker: AWS Lambda's microVM.

Multi-tenant platforms (Lambda, Fargate) use VM-based runtimes because kernel sharing is too risky.

5.4 The shim

containerd-shim exists per container. It keeps the container alive even if containerd crashes, collects stdout/stderr, and records exit codes. The chain is: kubelet → CRI → containerd → shim → runc.

6. Security — why containers are not "real" isolation

6.1 Shared-kernel risk

All containers share one kernel → a kernel CVE is a mass container escape. Real examples:

  • Dirty COW (CVE-2016-5195): privilege escalation, container escape.
  • runc CVE-2019-5736: overwriting the runc binary to own the host.
  • Leaky Vessels (2024): multiple runc/containerd CVEs.

Mitigations: patched kernels, AppArmor/SELinux, seccomp profiles.

6.2 Linux Capabilities

Traditional Unix root (UID 0) has total power — too dangerous for containers. Capabilities split root into pieces:

  • CAP_NET_ADMIN: network config.
  • CAP_SYS_ADMIN: most dangerous operations.
  • CAP_NET_BIND_SERVICE: bind ports below 1024.

Docker default: a restricted cap set (least privilege). Tune with --cap-add / --cap-drop. --privileged grants all caps — effectively host root.

6.3 seccomp — syscall filter

"Only allow this container to call these syscalls":

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

Docker's default profile allows ~300 syscalls and blocks keyctl, ptrace, mount, etc.

6.4 AppArmor / SELinux

Filesystem-level MAC. Ubuntu uses AppArmor, RHEL uses SELinux.

6.5 Rootless containers

Run the container daemon as a non-privileged user, mapping UIDs via user namespace. Podman's default. Docker also supports rootless. Much stronger security, with limitations (no ports below 1024, etc).

6.6 Security checklist

  • Patched kernel.
  • Keep the default seccomp profile (avoid --security-opt seccomp=unconfined).
  • Minimize capabilities (--cap-drop ALL + add only what is needed).
  • Run as non-root user (USER directive).
  • Read-only rootfs (--read-only with tmpfs volumes).
  • Image scanning: Trivy, Grype, Docker Scout.
  • Runtime monitoring: Falco.

7. Building images well

7.1 Layer optimization

# Bad: each RUN creates a new layer, cache remains in layer 1
FROM node:20
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
RUN rm -rf /tmp/cache
# Good: clean up in the same 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

FROM node:20 AS builder
WORKDIR /app
COPY . .
RUN npm install && npm run build

FROM node:20-alpine
COPY --from=builder /app/dist /app
COPY --from=builder /app/node_modules /app/node_modules
CMD ["node", "/app/index.js"]

Build tools (gcc, webpack) never reach the final image — 5x+ size reduction.

7.3 Distroless images

Google's minimal images (no shell):

FROM gcr.io/distroless/nodejs20
COPY --from=builder /app /app
CMD ["/app/index.js"]

Tens of MB, minimal attack surface — but harder to debug (no exec shell).

7.4 Alpine vs Debian

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

Alpine gotcha: musl compatibility (DNS resolver, pthread quirks), extra tooling for Python C extensions.

7.5 BuildKit

DOCKER_BUILDKIT=1 docker build .

Parallel stages, registry cache export/import (--cache-to=type=registry,ref=...), build-time secrets (--secret), containerd native.

8. Networking — from docker0 to CNI

8.1 Docker default bridge

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 pairs (one end on host, one in container), NAT via iptables MASQUERADE, port forwarding via iptables DNAT (-p 80:8080).

8.2 Network modes

  • bridge (default).
  • host: share host network (no isolation, fastest).
  • none: no network.
  • container: share another container's network (pod-like).

8.3 Kubernetes CNI

Kubernetes mandates flat pod IPs. Docker bridge alone is not enough.

  • Flannel: VXLAN tunneling, simple.
  • Calico: BGP-based, eBPF support, network policy.
  • Cilium: eBPF native, rich observability/security.
  • AWS VPC CNI: ENI assigned directly to pods.

CNI (Container Network Interface) is the standard plugin contract.

9. Integration with Kubernetes

9.1 Why Kubernetes dropped Docker (2020)

Kubernetes 1.20 deprecated dockershim; 1.24 (2022) removed it. Reasons: Docker Engine contained features kubelet does not use (image builds, swarm); a shim was needed for CRI; containerd was already Docker's core. Result: kubelet → CRI → containerd direct. OCI images still work the same way.

9.2 Pod = containers sharing namespaces

A Pod is a group of containers sharing network, IPC, UTS namespaces. Mount and PID are independent by default.

pause container (owns the namespaces)
  ├── app container
  └── sidecar container

pause is a tiny no-op process that keeps the namespaces alive.

9.3 Init and sidecar containers

  • Init containers: run sequentially before main.
  • Sidecar (native since 2023): run alongside app with independent restart.

Envoy (Istio), Fluent Bit, etc. run as sidecars.

9.4 What orchestration adds

Scheduling, health checks and auto-restart, rolling updates, service discovery and load balancing, storage/config/secrets, horizontal autoscaling — all declarative YAML.

10. Practical tips

10.1 Debugging toolkit

docker ps -a
docker logs -f <container>
docker exec -it <container> sh
docker inspect <container>
docker stats
docker events

Kubernetes:

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

10.2 Debugging distroless

No shell, so exec won't work. Options:

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

Share pid/net namespaces to attach a debug container.

10.3 Image size analysis

  • docker history <image>: per-layer sizes.
  • dive tool: explore layer contents.
  • docker image ls --format="...".

10.4 Performance observation

docker stats --no-stream

cat /sys/fs/cgroup/memory.current
cat /sys/fs/cgroup/cpu.stat

docker run --cap-add SYS_ADMIN --pid host ...

11. Closing — what one docker run really does

A single docker run nginx performs:

  1. Download nginx image layers from Docker Hub.
  2. Assemble the layer stack with OverlayFS.
  3. containerd creates a containerd-shim.
  4. The shim launches runc.
  5. runc sets up 7 namespaces, cgroup, and rootfs.
  6. Applies seccomp/AppArmor profile.
  7. execve("nginx") starts the process.
  8. Container network (veth + bridge) is configured.
  9. iptables port forwarding is added.

Done in under 50ms. Compared with the minutes of a 2000s-era VM boot this is miraculous, resting on a decade of engineering: 2006 cgroups, 2008 namespaces, 2013 Docker, 2015 OCI, 2016 overlay2, 2020 CRI.

The price of this convenience is shared-kernel risk — the reason Lambda and Fargate use microVMs. For multi-tenant workloads, do not trust "container = isolation" blindly.

Next: Kubernetes internals — etcd's Raft consensus, scheduler filter/score, controller patterns, custom resources, and the CRI/CNI/CSI plugin system.

References

  • Jérôme Petazzoni — "Anatomy of a Container" (LinuxCon 2015).
  • Michael Kerrisk — "Understanding Linux Namespaces" (LWN series).
  • OCI Image/Runtime Specifications (GitHub).
  • Julia Evans — Container Networking series.
  • Liz Rice — "Container Security" (O'Reilly, 2020).
  • Kubernetes official docs — Pods, CNI, CRI.
  • Red Hat crun blog.
  • Firecracker paper (NSDI 2020).
  • Google gVisor paper.
  • "The Kubernetes Book" — Nigel Poulton.