컨테이너를 만드는 리눅스 커널 기능: namespace, cgroup, OverlayFS

같은 서버의 프로세스인데, 컨테이너 안에서는 왜 다른 컴퓨터처럼 보일까요? namespace, cgroup, OverlayFS가 각각 무엇을 하는지, 그리고 배포하다 만나는 에러가 이 셋과 어떻게 이어지는지 따라가 봤습니다.

목차

컨테이너에 들어가 보면 다른 컴퓨터처럼 보입니다. ps를 치면 프로세스가 몇 개뿐이고, hostname도 IP도 서버와 다르고, / 아래 파일도 다르죠.

그런데 컨테이너는 VM처럼 따로 부팅한 컴퓨터가 아닙니다. 서버에서 ps를 치면 컨테이너 안의 프로세스도 그냥 목록에 보이는, 평범한 프로세스 묶음이에요. (Docker는 거들 뿐에서 직접 확인해 봤습니다.)

같은 서버의 프로세스가 왜 안에서는 다른 세상을 보게 될까요? 답은 리눅스 커널에 있습니다.

커널은 요청을 처리하는 창구

프로세스를 뜻하는 로봇들이 요청서를 들고 줄을 서 있고, 창구 직원이 CPU, 메모리, 디스크, 네트워크 선반 앞에서 요청을 받는 그림

프로그램은 CPU, 메모리, 파일, 네트워크를 직접 만지지 못합니다. "이 파일 열어 줘", "메모리 좀 줘", "지금 떠 있는 프로세스 보여 줘" 같은 요청을 커널에 보내면, 커널이 처리해서 대답해 주죠. 이 요청을 시스템 콜이라고 합니다.

컨테이너의 비밀이 여기 있어요. 커널이 누가 물어보느냐에 따라 다르게 대답해 주는 겁니다.

프로세스가 묻는 것 호스트 프로세스가 받는 대답 컨테이너 프로세스가 받는 대답 대답을 바꾸는 커널 기능
지금 떠 있는 프로세스는? 수백 개 자기 것 몇 개 namespace (PID)
내 컴퓨터 이름은? ip-10-0-1-23 3f2a9c1e5b7d namespace (UTS)
내 IP와 localhost는? 서버의 것 자기만의 것 namespace (NET)
/ 아래 파일은? 서버의 파일 이미지의 파일 OverlayFS + namespace (MNT)
메모리를 얼마나 쓸 수 있나? 서버 전체 정해 둔 만큼 cgroup

정리하면 컨테이너 = namespace + cgroup + OverlayFS입니다.

  • namespace: 무엇이 보이는가 (보이는 범위)
  • cgroup: 얼마나 쓸 수 있는가 (쓰는 양)
  • OverlayFS: / 아래에 어떤 파일을 놓는가 (이미지의 파일)

엄밀히 말하면 격리를 하는 건 namespace와 cgroup이에요. OverlayFS는 이미지의 파일을 준비하는 쪽이고, 그 파일만 /로 보이게 가두는 건 mount namespace가 맡습니다.

namespace: 보이는 범위를 나눈다

namespace는 '이 프로세스에게는 이것만 보여 줘'라고 정해 두는 기능입니다. 다른 세상 하나를 통째로 만드는 게 아니라, 보이는 것의 종류별로 따로 나눠요. 그래서 종류가 8가지나 됩니다.

종류 따로 보이게 하는 것
PID 프로세스 번호와 목록
NET 네트워크 카드, IP, 포트, localhost
MNT 마운트(폴더 연결) 목록
UTS hostname
USER 사용자 번호 (안에서는 root, 밖에서는 일반 사용자로)
IPC 프로세스끼리 메모리를 나눠 쓰는 통로
CGROUP cgroup 정보가 보이는 범위
TIME 부팅 후 흐른 시간 시계

표에서 CGROUP을 보고 "cgroup도 namespace였어?" 싶었다면, 저도 그랬습니다. 이름만 같고, cgroup 자체는 namespace가 아니에요. 아래에서 볼 cgroup은 쓰는 양을 제한하는 기능이고, CGROUP namespace는 그 cgroup 정보가 컨테이너 안에서 어디까지 보이는지만 나눕니다.

어떤 프로세스가 어느 namespace에 들어 있는지는 /proc/[PID]/ns에서 볼 수 있어요. 서버에서 4321번으로 보이는 nginx 컨테이너의 예입니다. (이 글의 출력은 모두 예시라 번호나 경로는 서버마다 다릅니다.)

bash
sudo ls -l /proc/4321/ns
text
lrwxrwxrwx 1 root root 0 Oct 19 09:20 mnt -> 'mnt:[4026532408]'
lrwxrwxrwx 1 root root 0 Oct 19 09:20 net -> 'net:[4026532413]'
lrwxrwxrwx 1 root root 0 Oct 19 09:20 pid -> 'pid:[4026532412]'
lrwxrwxrwx 1 root root 0 Oct 19 09:20 user -> 'user:[4026531837]'
...

호스트 프로세스와 번호가 다른 종류가 그 컨테이너가 따로 받은 namespace입니다. 위에서 user는 호스트와 같은 걸 함께 쓰고 있죠.

PID namespace: 번호표가 두 장

서버에서 4321번인 nginx가 컨테이너 안에서는 1번으로 보이는 건, 커널이 컨테이너용 PID namespace를 하나 더 만들고 그 안에서 처음 시작한 프로세스에게 1번을 줬기 때문입니다. 여기엔 규칙이 두 가지 있어요.

  • 바깥은 안을 보지만, 안은 바깥을 못 본다. 호스트에서는 컨테이너 안의 프로세스가 전부 보이지만, 컨테이너 안에서는 호스트의 프로세스가 보이지 않습니다.
  • 1번이 끝나면 전부 끝난다. namespace의 1번 프로세스가 종료되면 커널이 나머지 프로세스도 모두 종료시킵니다. 컨테이너가 주 프로그램과 운명을 같이하는 거죠.

배포할 때: 띄우자마자 종료되는 컨테이너

컨테이너가 뜨자마자 Exited (0)으로 끝나 버린다면, CMD로 실행한 주 프로그램(1번)이 끝났기 때문입니다. 금방 끝나는 스크립트를 넣었거나, 서버를 백그라운드로 띄워 놓고 자기는 끝나 버린 경우예요. 서버를 포그라운드로 실행하는 명령을 CMD에 넣어야 합니다. nginx 공식 이미지가 nginx -g "daemon off;"로 실행되는 것도 이 때문이고요.

dockerfile
# 서버를 백그라운드로 띄우고 셸이 끝나 버림
CMD ["sh", "-c", "node server.js &"]

# 서버가 1번 프로세스로 계속 돎
CMD ["node", "server.js"]

network namespace: localhost도 따로

컨테이너 안의 nginx가 80번 포트를 열었는데, 서버 주소의 80번으로는 왜 안 들어가질까요? network namespace를 따로 받은 컨테이너는 랜카드(eth0), IP, 포트, localhost까지 자기 것을 갖거든요. nginx가 연 80번은 그 컨테이너 세상 안에만 있는 80번입니다. 밖과 이어 주는 문이 따로 필요한데, 그게 docker run -p 8080:80이에요.

브라우저가 서버 8080번으로 보낸 요청을 커널이 규칙에 따라 컨테이너의 80번으로 넘기는 경로

  1. 브라우저가 서버 주소의 8080번으로 요청을 보냅니다.
  2. 서버의 커널이 "8080번으로 온 건 172.17.0.2의 80번으로"라는 규칙에 따라 목적지를 바꿔 컨테이너로 넘깁니다. 이 규칙은 Docker가 커널의 패킷 처리 기능(iptables 또는 nftables)에 등록해 둔 거예요.
  3. nginx가 80번에서 받아 응답하고, 응답은 같은 길로 돌아갑니다. 브라우저는 끝까지 서버의 8080번과 대화한 줄 알죠.

흔히 "Docker가 포트를 넘겨준다"고 하지만, 엄밀히 말하면 넘겨주는 건 커널이고 Docker는 규칙을 등록했을 뿐이에요.

회사 전화로 치면 서버 주소와 8080번은 대표번호, 컨테이너의 80번은 내선번호입니다. 커널은 대표번호로 온 전화를 내선으로 돌려 주는 교환원이고, Docker는 그 연결 규칙을 등록해 준 사람이죠. 내선번호는 사무실마다 따로라서 컨테이너 세 개가 모두 80번을 써도 충돌하지 않아요. 대표번호만 -p 8080:80, -p 8081:80처럼 다르게 정하면 됩니다.

같은 이유로 localhost도 각자의 것입니다. 컨테이너 안에서 localhost는 그 컨테이너 자기 자신이에요.

배포할 때: 컨테이너 안에서 localhost로 DB에 접속이 안 된다

백엔드와 DB를 각각 컨테이너로 띄우고 백엔드가 localhost:5432로 접속하면, 백엔드 컨테이너 자기 자신의 5432번을 찾다가 연결이 거절됩니다. 같은 네트워크에 있는 DB 컨테이너의 이름으로 접속해야 해요. docker compose는 서비스들을 같은 네트워크에 넣고, 서비스 이름을 주소처럼 쓸 수 있게 해 줍니다.

yaml
services:
  api:
    build: .
    environment:
      DATABASE_URL: postgres://app:app@db:5432/app   # localhost가 아니라 db
  db:
    image: postgres:17
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app

mount namespace와 UTS namespace

  • mount namespace는 파일시스템 연결 목록을 프로세스마다 따로 갖게 합니다. 컨테이너가 자기만의 /를 갖는 바탕이고, 아래 OverlayFS에서 이어집니다.
  • UTS namespace는 hostname을 따로 갖게 합니다. 컨테이너 안에서 hostname을 치면 컨테이너 ID 같은 이름이 나오는 게 이 덕분이에요.

cgroup: 쓸 수 있는 양을 정한다

보이는 범위를 나눴으니 남은 건 쓰는 양입니다. 컨테이너 하나가 서버 메모리를 다 먹어 버리면 곤란하겠죠. 이걸 막는 게 cgroup(control group)이에요. 프로세스들을 한 그룹으로 묶고, 그룹 전체에 사용량 상한을 겁니다. 한 달 10GB 데이터 요금제를 쓰면 앱을 몇 개 쓰든 합쳐서 10GB까지인 것처럼요.

우리가 치는 Docker 옵션은 결국 커널의 cgroup 파일에 숫자를 적는 일입니다.

Docker 옵션 커널 파일에 적히는 값 뜻
--memory 256m memory.max = 268435456 메모리 256MB(바이트 단위)까지
--cpus 0.5 cpu.max = 50000 100000 0.1초마다 CPU 0.05초 (평균 CPU 반 개)
--pids-limit 100 pids.max = 100 동시에 띄울 수 있는 프로세스(스레드 포함) 100개까지

옵션을 안 주면 값은 max, 즉 제한 없음이에요.

cgroup은 폴더 트리다

이 '커널 파일'은 리눅스 서버의 /sys/fs/cgroup 아래에 폴더 트리로 보입니다. 폴더 하나가 그룹 하나, 폴더 안의 파일이 그 그룹의 설정과 상태예요.

text
/sys/fs/cgroup/
└── system.slice/
    ├── docker.service/              ← dockerd
    └── docker-3f2a9c1e….scope/      ← web 컨테이너의 그룹
        ├── memory.max               ← 한도 (설정)
        ├── memory.current           ← 지금 쓰는 양 (상태)
        ├── cpu.max
        ├── cpu.stat
        └── cgroup.procs             ← 이 그룹에 속한 PID 목록

파일이라고 했지만 디스크 어디에도 없습니다. 커널이 파일인 척 열어 둔 창구거든요. 읽으면 커널이 그 순간의 값을 만들어 주고, 숫자를 쓰면 바로 적용합니다. ps가 읽는 /proc도 같은 종류예요.

컨테이너 안에서 /sys/fs/cgroup을 열면 자기 그룹 폴더가 맨 위처럼 보이고(앞에서 본 CGROUP namespace 덕분입니다), 읽기 전용이라 스스로 한도를 올릴 수는 없습니다.

bash
docker run -d --name m --memory 256m nginx:alpine
docker exec m cat /sys/fs/cgroup/memory.max
text
268435456

--memory 256m이 268435456(256 × 1024 × 1024)이라는 숫자로 적혀 있죠. Docker는 숫자를 적었을 뿐이고, 지키게 만드는 건 커널입니다.

참고로 컨테이너 안에서 free -m을 치면 서버 전체 메모리가 나와서 한도가 안 걸린 것처럼 보입니다. free가 읽는 정보가 namespace로 나뉘지 않아서일 뿐이에요. 진짜 한도와 사용량은 memory.max와 memory.current에 있고, docker stats도 여기서 값을 가져옵니다.

메모리는 넘으면 죽고, CPU는 넘으면 기다린다

한도를 넘으면 메모리와 CPU는 결과가 정반대예요.

메모리는 기다린다고 해결되지 않으니, 커널이 그룹 안의 프로세스를 강제 종료합니다(OOM Kill). 컨테이너는 Exited (137)로 끝나요. 137은 128 + 9, SIGKILL로 종료됐다는 뜻입니다.

배포할 때: 컨테이너가 Exited (137)로 죽는다

137을 보면 바로 메모리 부족을 떠올리기 쉽지만, 137은 SIGKILL로 끝났다는 뜻일 뿐이에요. docker stop이 제한 시간 안에 안 끝나는 프로세스를 강제로 끝낼 때도 137이 나옵니다. 메모리 때문인지는 따로 확인하세요.

bash
docker inspect -f '{{.State.OOMKilled}}' api

true면 메모리 초과입니다. 컨테이너가 아직 떠 있다면 memory.events의 oom_kill 숫자로도 볼 수 있어요. 한도를 늘리거나 사용량을 줄여야 하고, 서버 전체 메모리를 믿고 힙을 크게 잡는 프로그램은 특히 조심해야 합니다. 요즘 Java(11 이상)는 cgroup 한도를 읽어서 기본 힙 크기를 정하죠.

CPU는 시간이라 기다리게 하면 됩니다. 그런데 --cpus 0.5면 CPU가 반 속도로 도는 걸까요? 저도 처음엔 그렇게 생각했는데, 실제로는 전속력으로 돌다가 멈추기를 반복합니다.

0.1초 구간마다 0.05초 실행하고 0.05초 대기하는 CPU 제한 타임라인

커널은 0.1초를 한 구간으로 보고, 그 안에서 0.05초를 다 쓰면 컨테이너의 프로세스들을 멈춰 세웁니다. 다음 0.1초가 시작되면 다시 0.05초가 충전되고요. 이걸 1초에 열 번 반복하니 평균하면 CPU 반 개처럼 보이는 거예요. cpu.max의 50000 100000이 바로 '0.1초(100000µs) 중 0.05초(50000µs)'라는 뜻입니다. 이 시간은 모든 코어에서 쓴 시간을 합친 값이라, 스레드가 많을수록 빨리 바닥나요.

배포할 때: 에러는 없는데 API가 이상하게 느리다

CPU 한도에 걸린 컨테이너는 죽지 않고 느려집니다. 스레드가 여러 개인 프로그램은 여러 코어에서 동시에 돌면서 0.05초를 순식간에 다 써 버리고, 그 구간의 남은 시간 내내 멈춰 있거든요. 평균 CPU 사용률은 낮은데 응답이 가끔 0.1초 가까이 튄다면 의심해 볼 만합니다. cpu.stat의 nr_throttled(멈춘 횟수)와 throttled_usec(멈춘 시간)이 계속 늘고 있으면 CPU 한도에 걸리고 있는 거예요. --cpus를 늘리거나, 스레드와 워커 수를 한도에 맞게 줄이면 됩니다.

bash
docker exec api cat /sys/fs/cgroup/cpu.stat
text
usage_usec 81234567
nr_periods 52000
nr_throttled 18400
throttled_usec 912345678

쿠버네티스에서 파드에 거는 CPU limit(resources.limits.cpu)도 결국 같은 cpu.max로 적히고, 흔히 보는 스로틀링 지표도 이 cpu.stat에서 나옵니다. 실제 운영에서도 나오는 일이에요. 당근 SRE 밋업 4회의 「당근에서 Loki 1년간 운영기」 발표(발표 자료)에서는 Loki의 CPU 사용률이 평균 35%로 낮았는데도 CPU 스로틀링이 높게 나타났다고 합니다. 로그 유실 문제는 전송 배치 크기를 키우면서 Loki의 CPU limit도 없애서 풀었고, 조회가 느린 문제에서도 CPU limit 제거로 스로틀링을 해결했습니다. 다만 limit을 없앨 때는 같은 노드의 다른 파드에 영향이 가지 않도록 CPU request를 적당히 잡고 사용 패턴을 먼저 이해해야 한다는 점도 함께 짚었습니다.

OverlayFS: 이미지 레이어로 /를 만든다

컨테이너 안의 /에는 왜 서버의 파일이 아니라 이미지의 파일이 보일까요?

리눅스의 /는 모든 파일이 시작되는 최상위 폴더예요. 프로세스에게 /로 보이는 파일 전체를 루트 파일시스템(root filesystem, rootfs)이라고 부릅니다. docker run --read-only가 읽기 전용으로 만드는 게 바로 이거예요. 컨테이너의 루트 파일시스템에 이미지의 파일이 보이는 건 커널 기능 두 가지가 함께 쓰이기 때문입니다.

  1. OverlayFS가 이미지의 레이어들을 겹쳐 폴더 하나로 조립합니다. 컨테이너를 띄우기 전에 Docker 쪽(dockerd나 containerd)이 요청해 둡니다.
  2. mount namespace와 pivot_root가 그 폴더를 컨테이너의 /로 바꿔 끼우고, 서버의 나머지 파일은 안 보이게 합니다. 프로세스를 시작하면서 runc가 요청합니다.

근데 컨테이너를 띄울 때마다 이미지를 통째로 복사한다면 디스크가 버틸까요? 그래서 복사하는 대신 레이어를 겹칩니다.

읽기 전용 이미지 레이어들 위에 쓰기 레이어가 쌓여 하나의 '/'로 보이는 구조

  • 이미지는 읽기 전용 레이어를 쌓은 것입니다. Dockerfile의 FROM, RUN, COPY 같은 줄마다 레이어가 하나씩 생겨요.
  • 컨테이너를 실행하면 맨 위에 그 컨테이너 전용 쓰기 레이어가 하나 더 생깁니다.
  • OverlayFS는 위 레이어에 같은 파일이 있으면 위의 것을 보여 주고, 아래 레이어의 파일을 고치면 쓰기 레이어로 복사한 뒤 고칩니다(copy-on-write). 그래서 컨테이너를 여러 개 띄워도 이미지 레이어는 한 벌만 있으면 되죠.

컨테이너 안에서 /가 어떻게 붙어 있는지 보면 이 구조가 그대로 드러납니다.

bash
docker exec web cat /proc/mounts | head -1
text
overlay / overlay rw,relatime,lowerdir=/var/lib/docker/overlay2/l/3KX…:/var/lib/docker/overlay2/l/QZ7…,upperdir=/var/lib/docker/overlay2/9f1…/diff,workdir=/var/lib/docker/overlay2/9f1…/work 0 0

/가 overlay 타입으로 붙어 있고, 콜론(:)으로 이어진 lowerdir가 그림의 읽기 전용 이미지 레이어, upperdir가 이 컨테이너의 쓰기 레이어입니다(workdir는 OverlayFS의 작업용 폴더예요). 정확히는 OverlayFS가 Docker의 기본 저장 방식(overlay2)일 뿐이라, 설정에 따라 경로가 /var/lib/containerd/… 아래로 나오거나 다른 방식을 쓰기도 합니다.

이 구조를 알면 두 가지가 자연스럽게 설명됩니다.

  • 쓰기 레이어는 컨테이너와 함께 지워집니다. docker rm으로 컨테이너를 지우면 그 안에서 만든 파일도 사라져요. 이미지 레이어는 여러 컨테이너가 함께 읽기만 하니 그대로 남고요.
  • 이미지에는 커널이 없습니다. 이미지에 들어 있는 건 파일과 기본 설정(실행할 명령, 환경 변수 등)뿐입니다. Ubuntu 이미지에도 Ubuntu 커널은 없어요.

배포할 때: 컨테이너를 지웠더니 DB 데이터가 사라졌다

DB가 저장한 파일도 그 컨테이너의 쓰기 레이어에 있었기 때문입니다. docker rm이나 docker compose down으로 컨테이너가 지워지면 함께 사라져요. DB 데이터 폴더를 볼륨으로 연결하면, 볼륨은 쓰기 레이어 바깥에 있어서 컨테이너를 지워도 남습니다. 데이터 폴더 경로는 이미지마다, 버전마다 다를 수 있으니 이미지 문서에서 확인하세요.

bash
docker run -d --name db \
  -e POSTGRES_PASSWORD=app \
  -v pgdata:/var/lib/postgresql/data \
  postgres:17

이미지에 정말 커널이 없는지는 바로 확인할 수 있습니다.

bash
uname -r
docker run --rm alpine uname -r
docker run --rm alpine cat /etc/os-release
text
6.8.0-1015-aws
6.8.0-1015-aws
NAME="Alpine Linux"
...

uname -r은 커널 버전을 보여 주는 명령이에요. 파일은 Alpine인데 커널 버전은 서버와 똑같죠. 컨테이너는 서버의 커널을 그대로 함께 씁니다.

VM과는 무엇이 다른가

VM은 각자 게스트 커널을 갖고, 컨테이너는 호스트 커널 하나를 함께 쓰는 구조 비교

가상 머신(VM)은 하이퍼바이저라는 프로그램이 가짜 하드웨어를 만들고, 그 위에 VM마다 자기 커널을 따로 부팅합니다. 컨테이너는 서버의 커널 하나를 함께 쓰는 프로세스일 뿐이라 부팅 과정이 없어요. 컨테이너가 빠르게 뜨고 가벼운 건 이 때문입니다.

반대로 리눅스 컨테이너에는 리눅스 커널이 꼭 필요하다는 뜻이기도 해요. 그래서 맥의 Docker Desktop은 작은 리눅스 VM을 띄우고 그 안에서 컨테이너를 돌립니다. 맥 터미널의 ps에 컨테이너가 안 보이는 게 그래서고, 컨테이너 안에서 uname -r을 치면 6.10.14-linuxkit처럼 그 VM의 커널 버전이 나와요. 윈도우는 WSL2의 리눅스 커널을 씁니다.

격리를 보강하는 장치

namespace와 cgroup만으로 충분히 안전할까요? 커널에는 컨테이너를 더 단단히 가두는 기능이 더 있습니다.

  • capabilities: root 권한을 기능별로 쪼갠 것입니다. Docker는 기본적으로 14개만 남기고 나머지를 빼요. 그래서 컨테이너 안의 root는 서버의 root보다 할 수 있는 일이 훨씬 적습니다.
  • seccomp: 컨테이너가 보낼 수 있는 시스템 콜을 거르는 필터입니다. Docker 기본 설정은 위험한 시스템 콜 40여 개를 막아요.

--privileged 옵션은 이런 보호 장치를 대부분 끕니다. 실습이 아니라면 쓰지 않는 게 좋아요.

정리

  • namespace: 보이는 범위를 나눕니다. 프로세스 번호, 네트워크와 localhost, 마운트, hostname이 따로 보여요.
  • cgroup: 쓰는 양을 제한합니다. 메모리는 넘으면 강제 종료, CPU는 넘으면 대기.
  • OverlayFS: 이미지 레이어 위에 쓰기 레이어를 겹쳐 루트 파일시스템을 만들고, mount namespace가 그걸 /로 끼웁니다.

컨테이너에서 이상한 일이 생기면 "보이는 범위, 쓰는 양, 보이는 파일 중 무엇이 문제일까?"부터 물어보세요. 원인에 훨씬 빨리 닿습니다.

증상 커널 기능 확인할 것
띄우자마자 종료 PID namespace CMD의 주 프로그램이 계속 살아 있는지
localhost로 다른 컨테이너 접속 실패 network namespace 컨테이너 이름(서비스 이름)으로 접속하는지
free가 서버 전체 메모리를 보여 줌 namespace로 안 나뉘는 정보 memory.max, memory.current
Exited (137) cgroup (메모리) OOMKilled, memory.events
에러 없이 느림 cgroup (CPU) cpu.stat의 nr_throttled
컨테이너를 지웠더니 데이터 사라짐 OverlayFS (쓰기 레이어) 데이터 폴더를 볼륨으로 연결했는지
맥의 ps에 컨테이너가 없음 Docker Desktop의 리눅스 VM 컨테이너 안의 uname -r

직접 해 보기

Docker가 설치된 리눅스 서버(Ubuntu 22.04 이상처럼 cgroup v2를 쓰는 서버)와 sudo 권한이 필요합니다.

bash
# namespace: 컨테이너 프로세스와 내 셸의 namespace 번호 비교
docker run -d --name web nginx:alpine
PID=$(docker inspect -f '{{.State.Pid}}' web)
sudo ls -l /proc/$PID/ns
sudo ls -l /proc/self/ns

# cgroup: 한도가 파일에 적히고, CPU는 50% 근처에서 더 오르지 않음 (Ctrl+C로 빠져나오기)
docker run -d --name m --memory 256m --cpus 0.5 alpine sh -c 'while :; do :; done'
docker exec m cat /sys/fs/cgroup/memory.max
docker exec m cat /sys/fs/cgroup/cpu.max
docker stats m

# OverlayFS로 만든 루트 파일시스템: 파일은 Alpine, 커널은 서버 것
uname -r
docker run --rm alpine uname -r
docker run --rm alpine cat /etc/os-release

# 정리
docker rm -f web m