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

- Name
- Youngju Kim
- @fjvbn20031
0. "컨테이너는 VM 이 아니다" 가 의미하는 것
많은 개발자가 컨테이너를 "가벼운 VM" 으로 이해한다. 실제로는 완전히 다르다:
| 항목 | VM | Container |
|---|---|---|
| 격리 단위 | 하드웨어 | OS 네임스페이스 |
| 커널 | VM 마다 독립 | 호스트와 공유 |
| 부팅 시간 | 수 초 ~ 수 분 | 수십 ms |
| 메모리 오버헤드 | 수백 MB | 수 MB |
| 디스크 오버헤드 | GB | MB |
| 격리 강도 | 강력 | 약함 |
컨테이너 = 프로세스 + 커널 기능들 (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 /app/dist /app
COPY /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 /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>: 레이어별 크기.divetool: 레이어 내용 탐색.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 한 줄이 실제로 하는 일:
- Docker Hub 에서 nginx 이미지 레이어 다운로드.
- OverlayFS 로 레이어 스택 조립.
- containerd 가 containerd-shim 생성.
- shim 이 runc 실행.
- runc 가 7개 네임스페이스 + cgroup + rootfs 설정.
- seccomp/AppArmor 프로필 적용.
execve("nginx")로 프로세스 시작.- 컨테이너 네트워크 (veth + bridge) 구성.
- 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.