🐳 Docker
🐳 Docker란? — "내 PC에선 되는데요" 문제의 종결자
Docker는 애플리케이션을 코드·라이브러리·런타임·OS 의존성까지 하나의 이식 가능한 단위(=이미지)로 포장하고, 그 이미지로부터 가볍고 격리된 프로세스(=컨테이너)를 빠르게 실행하는 도구다. 가상머신(VM)처럼 하드웨어를 통째로 흉내내지 않고, 호스트 리눅스 커널의 namespace·cgroup을 빌려 써서 초 단위 기동·MB 단위 오버헤드를 달성한다.
- 이미지 = 읽기 전용 레이어 묶음 (OS 루트 + 앱 + 설정)
- 컨테이너 = 이미지로부터 생성된 격리 프로세스 (쓰기 레이어 + 네트워크 ns + cgroup)
- 레지스트리 = 이미지를 저장·배포하는 원격 저장소 (Docker Hub, ECR, GCR, GHCR)
컨테이너 vs 가상머신 — 계층 비교
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 전환·강한 격리·레거시 |
🏗️ 아키텍처 — CLI부터 리눅스 커널까지
Docker는 단일 바이너리가 아니다. 클라이언트-서버 구조로, docker CLI가 REST API로 dockerd(데몬)에 명령을 보내면, dockerd는 containerd(표준 런타임)에 위임하고, containerd는 runc(OCI 런타임)를 호출해 리눅스 커널의 격리 기능(namespace·cgroup)으로 컨테이너를 만든다.
pid · net · mnt · uts · ipc · user"] CG["cgroup v2
cpu · memory · io · pids"] end CT1 -.-> NS CT2 -.-> NS CT3 -.-> CG
docker CLI를 쓰는 건 변함없음.
🧱 이미지 & 레이어 — Copy-on-Write 스택
Docker 이미지는 읽기 전용 레이어의 스택이다. 각 Dockerfile 명령(FROM · RUN · COPY · ADD)이 하나의 레이어가 되고, 레이어는 SHA-256 해시로 식별되며 여러 이미지가 공유한다. 컨테이너 실행 시 맨 위에 쓰기 가능한 레이어가 얹혀 Copy-on-Write로 동작한다.
로그 · 세션 · 캐시"] 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 | 호스트 → 이미지 파일 복사 | ✅ 레이어 |
| ADD | COPY + 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 → 이미지 → 컨테이너
.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 흐름을 그림으로 보자.
(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하느냐에 따라 외부 접근 방식이 달라진다.
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 — 기본값 · 단일 호스트
같은 브릿지의 컨테이너끼리는 서로 컨테이너 이름으로 DNS 해석된다(사용자 정의 브릿지일 때). 기본 docker0에는 이 DNS가 없으므로 docker network create app-net으로 따로 만들어 쓰자.
host — 성능 · 격리 트레이드오프
네트워크 오버헤드가 0에 가깝지만 포트 충돌·보안 경계 약함. 주로 관제·성능 테스트에서 사용.
overlay — 다중 호스트 · Swarm/K8s
여러 호스트의 컨테이너를 동일 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 시 쓰기 레이어 소멸). 영속 데이터는 반드시 볼륨이나 바인드 마운트로 컨테이너 밖에 저장해야 한다.
/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 한 방에 띄우는 도구. 개발 환경의 표준.
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 덤프
db:5432·cache:6379로 바로 접근되는 이유.
📦 Registry — 이미지 저장·배포
Registry는 이미지를 저장·배포하는 HTTP 기반 저장소다. 공개: Docker Hub(기본값), 비공개: ECR · GCR · ACR · GHCR · Harbor(사내).
# 로그인 & 태그
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
myapp:1.0 같은 태그는 언제든 다른 이미지로 바뀔 수 있다. 완전 재현을 원하면 myapp@sha256:ab12… digest를 고정하라. K8s 프로덕션에서는 digest 핀닝이 표준.
🔒 보안 · 최적화 — "작고, non-root, 서명된 이미지"
이미지 크기 줄이기 — 멀티스테이지 빌드
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로 핀닝 |
| 2 | non-root 실행 | USER app 명시. root 기본값 = CVE 에스컬레이션 1순위 |
| 3 | 시크릿 미포함 | ENV로 하드코딩 금지 → BuildKit --secret 또는 볼륨 주입 |
| 4 | 스캐닝 | Trivy·Grype·Snyk로 CVE 스캔 → CI 게이트에 넣기 |
| 5 | readOnlyRootFilesystem | --read-only + 필요한 경로만 tmpfs 마운트 |
| 6 | caps 최소화 | --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 와 함께 탭으로 전환하며 연습하세요.
🎯 핵심 정리 — 면접/실전 포인트
- 컨테이너 ≠ VM: 커널 공유 · namespace(격리) + cgroup(제한) · MB·초 단위
- dockerd → containerd → runc: K8s는 1.24부터 dockershim 제거, containerd 직결
- 이미지는 레이어 스택: SHA-256 해시 · 공유·CoW ·
docker history로 진단 - Dockerfile 캐시 법칙: 자주 안 바뀌는 명령 위로,
COPY package*.json먼저 - 멀티스테이지 빌드: 빌드 도구는 최종 이미지에 없음 → 10× 작고 안전
- 네트워크 4종: bridge(기본) · host(성능) · overlay(멀티호스트) · none(격리)
- 사용자 정의 브리지: 컨테이너 이름 DNS 자동 →
docker network create필수 - 볼륨 > 바인드 마운트: 운영 데이터는 Named Volume, 개발 소스는 Bind Mount
- Compose DNS: 서비스 이름이 그대로 호스트명 (
db:5432) - 태그 vs digest: 운영은 digest 핀닝 (
@sha256:…) - non-root 실행:
USER app,--cap-drop=ALL,--read-only - 종료 코드: 137=OOM/SIGKILL, 139=SEGV, 143=SIGTERM
- restart 정책: 일반 서비스는
unless-stopped권장 - docker system prune: 주기적으로 돌려 디스크 누수 방지
- 보안 스캔: Trivy/Grype를 CI 게이트로 · HIGH/CRITICAL 차단