🐳 Docker

Notes / 컨테이너 / 이미지 · 네트워크 · Compose · Registry 진행 중

🐳 Docker란? — "내 PC에선 되는데요" 문제의 종결자

Docker는 애플리케이션을 코드·라이브러리·런타임·OS 의존성까지 하나의 이식 가능한 단위(=이미지)로 포장하고, 그 이미지로부터 가볍고 격리된 프로세스(=컨테이너)를 빠르게 실행하는 도구다. 가상머신(VM)처럼 하드웨어를 통째로 흉내내지 않고, 호스트 리눅스 커널의 namespace·cgroup을 빌려 써서 초 단위 기동·MB 단위 오버헤드를 달성한다.

3줄 요약:
  • 이미지 = 읽기 전용 레이어 묶음 (OS 루트 + 앱 + 설정)
  • 컨테이너 = 이미지로부터 생성된 격리 프로세스 (쓰기 레이어 + 네트워크 ns + cgroup)
  • 레지스트리 = 이미지를 저장·배포하는 원격 저장소 (Docker Hub, ECR, GCR, GHCR)

컨테이너 vs 가상머신 — 계층 비교

flowchart TB subgraph VM["🖥️ 가상머신 (VM)"] direction BT A1[하드웨어 Hardware] A2[Host OS] A3[Hypervisor] A4["Guest OS 1
Bins/Libs + App"] A5["Guest OS 2
Bins/Libs + App"] A1 --> A2 --> A3 A3 --> A4 A3 --> A5 end subgraph DK["🐳 Docker"] direction BT B1[하드웨어 Hardware] B2[Host OS · Linux Kernel] B3[Docker Engine] B4["Container 1
Bins/Libs + App"] B5["Container 2
Bins/Libs + App"] B6["Container 3
Bins/Libs + App"] B1 --> B2 --> B3 B3 --> B4 B3 --> B5 B3 --> B6 end
항목컨테이너VM
기동 시간수백 ms ~ 수 초수십 초 ~ 수 분
이미지 크기수 MB ~ 수백 MB수 GB
격리 수준프로세스 격리 (kernel 공유)하드웨어 격리 (다른 OS 가능)
오버헤드거의 없음 (native 속도)하이퍼바이저 비용
보안 경계커널 공격면 존재 → runc/gVisor 보강강한 경계 (커널 분리)
주요 용도마이크로서비스·CI·배포OS 전환·강한 격리·레거시
언제 Docker? 언제 VM? 같은 리눅스 커널에서 실행할 앱 = Docker 우선. Windows·macOS 앱 또는 멀티테넌트 강격리 = VM. 실전에서는 VM 위에 Docker를 얹는 2단 구성이 가장 흔하다 (EKS/GKE worker node = EC2/GCE VM + containerd).

🏗️ 아키텍처 — CLI부터 리눅스 커널까지

Docker는 단일 바이너리가 아니다. 클라이언트-서버 구조로, docker CLI가 REST API로 dockerd(데몬)에 명령을 보내면, dockerd는 containerd(표준 런타임)에 위임하고, containerd는 runc(OCI 런타임)를 호출해 리눅스 커널의 격리 기능(namespace·cgroup)으로 컨테이너를 만든다.

flowchart TB CLI["docker CLI"] -->|"REST /var/run/docker.sock"| D[dockerd] D --> C[containerd] C --> R1[runc] C --> R2[runc] C --> R3[runc] R1 --> CT1["Container A"] R2 --> CT2["Container B"] R3 --> CT3["Container C (priv)"] subgraph K["Linux Kernel"] NS["namespaces
pid · net · mnt · uts · ipc · user"] CG["cgroup v2
cpu · memory · io · pids"] end CT1 -.-> NS CT2 -.-> NS CT3 -.-> CG
왜 K8s는 dockerd를 버렸나? K8s 1.24부터 dockershim 제거. dockerd는 K8s가 필요하지 않은 기능(빌드·이미지 push 등)까지 끼워놓은 구조라, containerd·CRI-O로 직접 가면 중간 홉·버그·메모리가 줄어든다. 일반 개발자가 docker CLI를 쓰는 건 변함없음.

🧱 이미지 & 레이어 — Copy-on-Write 스택

Docker 이미지는 읽기 전용 레이어의 스택이다. 각 Dockerfile 명령(FROM · RUN · COPY · ADD)이 하나의 레이어가 되고, 레이어는 SHA-256 해시로 식별되며 여러 이미지가 공유한다. 컨테이너 실행 시 맨 위에 쓰기 가능한 레이어가 얹혀 Copy-on-Write로 동작한다.

flowchart BT subgraph IA["📦 nginx:1.25"] direction BT A1["FROM debian:bookworm-slim · 74MB"] A2["RUN apt install nginx · 58MB"] A3["COPY nginx.conf · 2KB"] A4["EXPOSE 80 · 0B"] A5["CMD nginx -g 'daemon off;'"] A1 --> A2 --> A3 --> A4 --> A5 end subgraph IB["📦 my-app"] direction BT B1["FROM debian (공유)"] B2["RUN apt install nginx (공유)"] B3["COPY ./dist /app · 18MB"] B4["CMD ./server"] B1 --> B2 --> B3 --> B4 end subgraph RC["🟢 Running Container"] direction BT C1["모든 image layers (R/O)"] C2["Writable Layer (R/W)
로그 · 세션 · 캐시"] C1 --> C2 end A1 -.공유.-> B1 A2 -.공유.-> B2 B4 -.run.-> C1
# 이미지 탐색
docker images                       # 로컬 이미지 목록
docker image ls --digests           # SHA-256 포함
docker image inspect nginx:1.25     # 전체 메타데이터 (레이어·CMD·Env)
docker history nginx:1.25           # 레이어별 명령 + 크기 ← 가장 자주 씀
docker image prune -a               # 안 쓰는 이미지 일괄 정리

# 태그 이동 (같은 이미지에 새 이름)
docker tag my-app:latest registry.example.com/me/my-app:v1.2.0

📝 Dockerfile — "재현 가능한 빌드 레시피"

Dockerfile은 이미지를 만드는 선언형 스크립트다. 각 명령이 레이어가 되므로 순서·캐시 전략이 곧 빌드 성능이 된다.

필수 명령어 13종

명령용도레이어 생성
FROM베이스 이미지 지정 — 모든 Dockerfile 첫 줄Base layer
RUN빌드 시 명령 실행 (패키지 설치 등)✅ 레이어
COPY호스트 → 이미지 파일 복사✅ 레이어
ADDCOPY + URL/tar 자동 해제 (가급적 COPY 권장)✅ 레이어
WORKDIR작업 디렉토리 설정 (cd 대용)메타
ENV환경변수 설정 (이후 레이어/런타임 모두 적용)메타
ARG빌드 시 변수 (--build-arg, 런타임엔 미반영)메타
EXPOSE문서화용 포트 (실제 발행은 -p)메타
CMD컨테이너 기본 실행 명령 (런타임에 덮어쓰기 가능)메타
ENTRYPOINT실행 엔트리 고정 (CMD는 인자로)메타
VOLUME익명 볼륨 선언 — 가급적 compose/플래그로메타
USER실행 UID (root 금지!)메타
HEALTHCHECK컨테이너 자가 헬스체크 명령메타

예제 — Node.js 앱 (Production 품질)

# syntax=docker/dockerfile:1.6

# ---------- 빌드 스테이지 ----------
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build

# ---------- 런타임 스테이지 ----------
FROM node:20-alpine
ENV NODE_ENV=production TZ=Asia/Seoul
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
COPY --from=builder --chown=app:app /app/dist ./dist
USER app                                 # ⚠ root 금지
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]

빌드 흐름 — Dockerfile → 이미지 → 컨테이너

flowchart LR DF[Dockerfile] --> BC["Build Context
.dockerignore 제외"] BC --> BK[BuildKit] BK -->|step 1| L1["Layer 1
FROM"] BK -->|step 2| L2["Layer 2
COPY · RUN"] BK -->|step N| LN["Layer N
CMD · ENTRYPOINT"] L1 --> IMG["Image
SHA-256"] L2 --> IMG LN --> IMG IMG --> REG[("Registry
Docker Hub · ECR · GHCR")]
# 기본 빌드
docker build -t myapp:1.0 .

# 멀티플랫폼 빌드 (ARM + x86) — buildx
docker buildx build --platform linux/amd64,linux/arm64 \
  -t myorg/myapp:1.0 --push .

# 빌드 아규먼트
docker build --build-arg NODE_ENV=production -t myapp:prod .

# 레이어 캐시 진단
docker build --progress=plain --no-cache .   # 캐시 없이 재빌드
docker history myapp:1.0                     # 어떤 명령이 몇 MB?
안티패턴: RUN apt-get update만 단독 실행하면 캐시 된 상태에서 apt-get install이 오래된 패키지 목록을 참조 → 항상 같은 RUN 안에 && apt-get install -y … && rm -rf /var/lib/apt/lists/* 까지 묶을 것.

▶️ 컨테이너 실행 — 상태 머신과 수명 주기

컨테이너는 단순히 "떠 있다"가 아니라, 명령에 따라 여러 상태를 오가는 유한 상태 머신이다. create → start → pause → stop → rm 흐름을 그림으로 보자.

stateDiagram-v2 [*] --> Created: docker create Created --> Running: docker start Running --> Paused: docker pause Paused --> Running: docker unpause Running --> Stopped: docker stop
(SIGTERM → SIGKILL) Running --> Exited: 프로세스 종료
(exit code) Stopped --> Running: docker start Stopped --> Removed: docker rm Exited --> Removed: docker rm Removed --> [*]
# 가장 흔한 run 플래그
docker run -d \                       # detach (백그라운드)
  --name web \                        # 이름 지정
  --restart=unless-stopped \          # 재시작 정책
  -p 8080:80 \                        # 호스트:컨테이너 포트
  -v $(pwd)/html:/usr/share/nginx/html:ro \   # 바인드 마운트 (읽기 전용)
  -e TZ=Asia/Seoul \
  --memory=512m --cpus=1 \            # 리소스 제한 (cgroup)
  --health-cmd "curl -f http://localhost/ || exit 1" \
  nginx:1.25

# 상태 확인
docker ps                            # 실행 중만
docker ps -a                         # 전체 (exited 포함)
docker logs -f --tail 100 web        # 로그 tail
docker stats web                     # 실시간 CPU/메모리
docker exec -it web sh               # 컨테이너 안으로 진입
docker inspect web | jq '.[0].State' # 상태 JSON

# 정리
docker stop web && docker rm web
docker container prune               # stopped 일괄 삭제

--restart 정책 4종

정책동작사용처
no기본값 — 재시작 없음일회성 작업
on-failure[:N]비정상 종료 시만, 최대 N회배치·시드
always무조건 재시작 (데몬 재시작 시에도)영구 서비스
unless-stopped수동 stop 아닌 이상 재시작⭐ 일반 서비스 권장

🌐 네트워크 드라이버 — bridge · host · overlay · none · macvlan

컨테이너는 기본적으로 별도의 네트워크 네임스페이스를 가진다. 어떤 드라이버에 attach하느냐에 따라 외부 접근 방식이 달라진다.

flowchart TB subgraph BR["🌉 bridge (기본)"] H1[Host eth0] --> B0["docker0
172.17.0.1"] B0 --> BC1["Container1
172.17.0.2"] B0 --> BC2["Container2
172.17.0.3"] end subgraph HO["🖥️ host"] HH[Host Network] --- HC["Container
호스트 NIC 직접 사용"] end subgraph OV["🌐 overlay (멀티호스트)"] N1["Node1
VXLAN"] --- N2["Node2
VXLAN"] N1 --> OC1["Container A"] N2 --> OC2["Container B"] end subgraph NN["🚫 none"] NC["Container
lo 만 존재"] end

bridge — 기본값 · 단일 호스트

사설 서브넷(172.17.0.0/16) · iptables NAT · -p 포트 발행 필요

같은 브릿지의 컨테이너끼리는 서로 컨테이너 이름으로 DNS 해석된다(사용자 정의 브릿지일 때). 기본 docker0에는 이 DNS가 없으므로 docker network create app-net으로 따로 만들어 쓰자.

host — 성능 · 격리 트레이드오프

호스트 net ns 공유 · -p 불필요 · Linux 전용 (Docker Desktop은 제한적)

네트워크 오버헤드가 0에 가깝지만 포트 충돌·보안 경계 약함. 주로 관제·성능 테스트에서 사용.

overlay — 다중 호스트 · Swarm/K8s

VXLAN 터널 · 컨트롤 플레인 필요 · mTLS 옵션

여러 호스트의 컨테이너를 동일 L2 세그먼트처럼 연결한다. K8s의 기본 CNI(Calico·Cilium)도 이 아이디어의 변주.

# 사용자 정의 브리지 — 컨테이너 이름 DNS 자동
docker network create app-net
docker run -d --name db --network app-net postgres:16
docker run -d --name web --network app-net -p 8080:8080 myapp:1.0
# 웹 컨테이너 안에서 "db"라는 호스트명이 바로 된다

# 확인
docker network ls                       # 네트워크 목록
docker network inspect app-net          # 어떤 컨테이너가 붙어있나
docker port web                          # 발행된 포트
docker exec web cat /etc/resolv.conf     # Docker DNS (127.0.0.11) 사용

💾 볼륨 vs 바인드 마운트 vs tmpfs

컨테이너는 기본적으로 휘발성이다 (docker rm 시 쓰기 레이어 소멸). 영속 데이터는 반드시 볼륨이나 바인드 마운트로 컨테이너 밖에 저장해야 한다.

flowchart LR C([Container]) --> V1["/var/lib/docker/volumes/"] C --> B1["호스트 경로
/home/user/src"] C --> T1["메모리 tmpfs"] subgraph V["📦 Volume (권장)"] V1 --> V2["Docker 관리
이식성·백업 용이"] end subgraph B["🔗 Bind Mount"] B1 --> B2["개발 중 소스 공유"] end subgraph T["⚡ tmpfs"] T1 --> T2["시크릿·임시 캐시
재시작 시 소멸"] end
# 볼륨 수명주기
docker volume create pg-data
docker volume ls
docker volume inspect pg-data
docker volume prune               # 참조되지 않는 볼륨 정리

# 사용
docker run -d --name db \
  -v pg-data:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=secret postgres:16

# 바인드 마운트 (개발)
docker run --rm -it \
  -v $(pwd):/app \
  -w /app node:20 npm run dev

# tmpfs (민감 데이터)
docker run --tmpfs /app/cache:size=100m,mode=1770 myapp

# 백업/복구 (데이터 전용 컨테이너 패턴)
docker run --rm \
  -v pg-data:/src \
  -v $(pwd):/dst \
  alpine tar czf /dst/pg-data.tgz -C /src .

🧩 Docker Compose — "스택을 YAML 한 장으로"

여러 컨테이너(웹·DB·캐시·워커)를 하나의 YAML로 정의하고 docker compose up 한 방에 띄우는 도구. 개발 환경의 표준.

flowchart LR USER([사용자]) -->|:80| WEB subgraph NET["app-network (bridge)"] WEB["web
nginx:alpine
:80"] API["api
node:20
:3000"] DB[("db
postgres:16
:5432")] CACHE[("cache
redis:7
:6379")] WEB -->|proxy_pass| API API -->|db:5432| DB API -->|cache:6379| CACHE end VOL1[(pg-data)] -.volume.-> DB VOL2[(redis-data)] -.volume.-> CACHE

docker-compose.yml 예제 — 위 토폴로지 그대로

services:
  web:
    image: nginx:alpine
    ports: ["8080:80"]
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      app:
        condition: service_healthy
    restart: unless-stopped

  app:
    build: .
    environment:
      DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
    deploy:
      replicas: 3
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 30s
      timeout: 3s
      retries: 3

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: app
    volumes:
      - pg-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
    restart: unless-stopped

  cache:
    image: redis:7-alpine
    volumes: ["redis-data:/data"]
    restart: unless-stopped

volumes:
  pg-data:
  redis-data:
# 자주 쓰는 명령
docker compose up -d              # 백그라운드로 전체 스택 기동
docker compose ps                 # 서비스 상태
docker compose logs -f app        # 특정 서비스 로그 tail
docker compose exec db psql -U postgres
docker compose restart web        # 특정 서비스 재시작
docker compose down               # 컨테이너·네트워크 제거 (볼륨 유지)
docker compose down -v            # 볼륨까지 제거 (DB 날아감 주의)
docker compose pull && docker compose up -d   # 이미지 업데이트
docker compose config             # 최종 병합된 YAML 덤프
서비스 이름이 곧 DNS: Compose는 자동으로 프로젝트별 사용자 정의 브릿지를 만들어 서비스 이름을 호스트명으로 쓰게 해준다. 위 예시에서 app 컨테이너가 db:5432·cache:6379로 바로 접근되는 이유.

📦 Registry — 이미지 저장·배포

Registry는 이미지를 저장·배포하는 HTTP 기반 저장소다. 공개: Docker Hub(기본값), 비공개: ECR · GCR · ACR · GHCR · Harbor(사내).

sequenceDiagram actor Dev as Developer participant CLI as docker CLI participant D as dockerd participant R as Registry participant S as Object Storage Dev->>CLI: docker push myapp:1.0 CLI->>D: push 요청 D->>R: POST /v2/auth R-->>D: Bearer token D->>R: HEAD 각 layer digest R-->>D: 존재 여부 (있으면 skip) D->>R: PUT 새 layer blob R->>S: 저장 D->>R: PUT manifest R-->>D: 201 Created Note over R,S: pull 시 역방향 흐름
# 로그인 & 태그
docker login                                 # Hub (또는 ghcr.io, 123456789.dkr.ecr.ap-northeast-2.amazonaws.com)
docker tag myapp:1.0 myorg/myapp:1.0

# 푸시
docker push myorg/myapp:1.0

# 다른 머신에서 pull & run
docker pull myorg/myapp:1.0
docker run -d -p 8080:3000 myorg/myapp:1.0

# 내 사설 레지스트리 띄우기 (테스트용)
docker run -d -p 5000:5000 --name registry registry:2
docker tag myapp:1.0 localhost:5000/myapp:1.0
docker push localhost:5000/myapp:1.0

# ECR (AWS)
aws ecr get-login-password --region ap-northeast-2 | \
  docker login --username AWS --password-stdin 123456789.dkr.ecr.ap-northeast-2.amazonaws.com
태그 vs digest: myapp:1.0 같은 태그는 언제든 다른 이미지로 바뀔 수 있다. 완전 재현을 원하면 myapp@sha256:ab12… digest를 고정하라. K8s 프로덕션에서는 digest 핀닝이 표준.

🔒 보안 · 최적화 — "작고, non-root, 서명된 이미지"

이미지 크기 줄이기 — 멀티스테이지 빌드

flowchart LR subgraph BEF["❌ Before (단일 스테이지)"] B1["node:20
SDK + 소스 + node_modules
+ 빌드 도구"] B2["최종 이미지 ≈ 1.2 GB"] B1 --> B2 end subgraph AFT["✅ After (멀티스테이지)"] A1["builder: node:20
빌드 전담"] A2["runner: node:20-alpine
--from=builder /app/dist"] A3["최종 이미지 ≈ 120 MB"] A1 --> A2 --> A3 end

보안 체크리스트 (8개)

#항목설명
1베이스 이미지 최소화alpine·distroless·scratch 선호. 태그는 digest로 핀닝
2non-root 실행USER app 명시. root 기본값 = CVE 에스컬레이션 1순위
3시크릿 미포함ENV로 하드코딩 금지 → BuildKit --secret 또는 볼륨 주입
4스캐닝Trivy·Grype·Snyk로 CVE 스캔 → CI 게이트에 넣기
5readOnlyRootFilesystem--read-only + 필요한 경로만 tmpfs 마운트
6caps 최소화--cap-drop=ALL --cap-add=NET_BIND_SERVICE
7서명·검증Sigstore cosign · Notation으로 공급망 검증
8리소스 제한--memory · --cpus · --pids-limit로 DoS 완화
# CVE 스캐닝 (Trivy)
trivy image myorg/myapp:1.0 --severity HIGH,CRITICAL

# non-root + read-only + cap drop 실행
docker run -d \
  --read-only --tmpfs /tmp \
  --user 1000:1000 \
  --cap-drop=ALL --cap-add=NET_BIND_SERVICE \
  --memory=512m --cpus=1 --pids-limit=200 \
  --security-opt=no-new-privileges \
  myorg/myapp:1.0

# Docker Content Trust (서명 강제)
export DOCKER_CONTENT_TRUST=1
docker push myorg/myapp:1.0      # 서명 필수

# 이미지 서명 (cosign)
cosign sign --key cosign.key myorg/myapp:1.0
cosign verify --key cosign.pub myorg/myapp:1.0
절대 금지: docker run --privileged · -v /:/host · -v /var/run/docker.sock:/var/run/docker.sock (컨테이너 안에서 호스트 탈옥 가능). CI 러너에서만 제한적으로, 그 외는 피해야 한다.

운영 팁 — 디스크 누수 방지

# Docker가 디스크를 얼마나 쓰나
docker system df
docker system df -v                # 상세

# 한꺼번에 정리 (사용 중이지 않은 것만)
docker system prune -a --volumes   # 이미지·컨테이너·네트워크·볼륨 모두
# 단, 실수로 필요한 볼륨 날릴 수 있으니 -a --volumes는 신중히

# 특정만
docker image prune -a              # 태그 없는 이미지 + 참조 없는 것
docker container prune             # exited 컨테이너
docker volume prune                # 참조 없는 볼륨
docker builder prune               # BuildKit 캐시

🕹️ 실습 터미널

브라우저에서 바로 돌아가는 Docker 시뮬레이터를 별도의 실습 페이지로 분리했습니다. Linux · Cisco IOS · Kubernetes 와 함께 탭으로 전환하며 연습하세요.

→ Docker 실습 터미널 열기

🎯 핵심 정리 — 면접/실전 포인트

  1. 컨테이너 ≠ VM: 커널 공유 · namespace(격리) + cgroup(제한) · MB·초 단위
  2. dockerd → containerd → runc: K8s는 1.24부터 dockershim 제거, containerd 직결
  3. 이미지는 레이어 스택: SHA-256 해시 · 공유·CoW · docker history로 진단
  4. Dockerfile 캐시 법칙: 자주 안 바뀌는 명령 위로, COPY package*.json 먼저
  5. 멀티스테이지 빌드: 빌드 도구는 최종 이미지에 없음 → 10× 작고 안전
  6. 네트워크 4종: bridge(기본) · host(성능) · overlay(멀티호스트) · none(격리)
  7. 사용자 정의 브리지: 컨테이너 이름 DNS 자동 → docker network create 필수
  8. 볼륨 > 바인드 마운트: 운영 데이터는 Named Volume, 개발 소스는 Bind Mount
  9. Compose DNS: 서비스 이름이 그대로 호스트명 (db:5432)
  10. 태그 vs digest: 운영은 digest 핀닝 (@sha256:…)
  11. non-root 실행: USER app, --cap-drop=ALL, --read-only
  12. 종료 코드: 137=OOM/SIGKILL, 139=SEGV, 143=SIGTERM
  13. restart 정책: 일반 서비스는 unless-stopped 권장
  14. docker system prune: 주기적으로 돌려 디스크 누수 방지
  15. 보안 스캔: Trivy/Grype를 CI 게이트로 · HIGH/CRITICAL 차단