Docker Basics
분류: Layer 5 - 플랫폼 엔지니어링 & 자동화 | 작성일: 2026-03-21
1. 한 줄 정의
섹션 제목: “1. 한 줄 정의”Docker는 애플리케이션을 실행 환경째 패키징해서, 어디서든 동일하게 실행할 수 있게 해주는 컨테이너 기술이다.
2. 왜 중요한가
섹션 제목: “2. 왜 중요한가”“내 컴퓨터에서는 되는데 서버에서는 안 돼요” 문제를 해결한다. BackOps에서 배포, 로컬 개발환경, CI/CD 파이프라인 모두 Docker 위에서 돌아갈 가능성이 높다. 컨테이너를 이해하지 못하면 배포 구조를 이해할 수 없고, 장애 대응도 어렵다.
3. 핵심 개념
섹션 제목: “3. 핵심 개념”컨테이너 vs 가상머신(VM) — 왜 컨테이너가 가벼운가
비유하자면, VM은 “아파트 건물 전체를 통째로 짓는 것”이고 컨테이너는 “같은 건물 안에 칸막이를 치는 것”이다.
VM은 하이퍼바이저 위에 각자 완전한 OS(커널 포함)를 띄운다. 반면 컨테이너는 Host OS의 리눅스 커널을 공유하고, 두 가지 기술로 격리만 한다:
- Namespace: 프로세스 격리. 컨테이너 안에서는 자기 프로세스만 보인다 (PID, 네트워크, 파일시스템 등 분리)
- cgroups: 자원 제한. 컨테이너별로 CPU/메모리 사용량을 제한할 수 있다
결과적으로 컨테이너는 OS 부팅 없이 수십 ms 만에 시작되고, 같은 서버에 수십 개를 띄울 수 있다.
📖 더 보기: How Docker Containers Work Under the Hood — Namespace와 cgroups 개념을 다이어그램으로 설명 (입문)
OverlayFS — 이미지 레이어가 실제로 디스크에서 어떻게 합쳐지는가
Docker가 이미지를 레이어로 관리할 수 있는 핵심 기술이 **OverlayFS(Overlay Filesystem)**다. OverlayFS는 Linux 커널에 내장된 Union 파일시스템으로, 여러 디렉토리를 하나의 디렉토리처럼 보이게 합쳐주는 기술이다.
비유: 투명한 OHP 필름을 여러 장 쌓아 올리는 것이다. 아래 필름(베이스 이미지 레이어)은 고정되어 있고, 위에 쌓인 필름(컨테이너 쓰기 레이어)에만 변경 내용을 기록한다. 밑에 있는 원본은 건드리지 않는다.
OverlayFS의 4가지 핵심 디렉토리:
| 디렉토리 | 역할 | 특성 |
|---|---|---|
| lowerdir | 읽기 전용 이미지 레이어들 (여러 개 가능) | Dockerfile의 각 명령이 하나의 lowerdir 레이어가 됨 |
| upperdir | 쓰기 가능한 컨테이너 레이어 (1개) | 컨테이너가 파일을 수정/생성하면 여기에 기록됨 |
| workdir | OverlayFS 내부 작업용 (사용자 비공개) | 파일 이동 등 원자적 작업에 사용 |
| merged | lowerdir + upperdir를 합쳐 보여주는 뷰 | 컨테이너 안에서 실제로 보이는 파일시스템 |
Copy-on-Write(CoW) 메커니즘 — 파일 수정 시 무슨 일이 생기나:
# 컨테이너가 /etc/nginx/nginx.conf를 수정하는 경우
1. 읽기 요청: merged에서 파일을 읽음 → lowerdir(이미지 레이어)에서 파일 제공 (복사 없음)
2. 쓰기 요청: 최초 1회 "copy up" 발생 → lowerdir의 파일을 upperdir로 복사 → 이후 upperdir의 복사본을 수정 → lowerdir의 원본은 그대로 유지 (다른 컨테이너가 공유 가능)
3. 파일 삭제: upperdir에 "whiteout" 파일 생성 → 실제 lowerdir 파일은 삭제 안 됨 → whiteout이 "이 파일은 없다"고 표시해서 merged에서 숨김# 실제로 Docker가 사용하는 overlay2 레이어 구조 확인docker inspect my-nestjs-app --format='{{json .GraphDriver.Data}}'
# 예상 출력:# {# "LowerDir": "/var/lib/docker/overlay2/abc123.../diff:# /var/lib/docker/overlay2/def456.../diff", ← 이미지 레이어들# "MergedDir": "/var/lib/docker/overlay2/xyz789.../merged", ← 컨테이너 뷰# "UpperDir": "/var/lib/docker/overlay2/xyz789.../diff", ← 쓰기 레이어# "WorkDir": "/var/lib/docker/overlay2/xyz789.../work"# }
# overlay2 드라이버가 지원하는 최대 레이어 수: 128개# → Dockerfile 명령어가 너무 많으면 레이어 수 초과 가능 → RUN 명령 묶기 권장이 구조가 주는 실용적 이점:
- 레이어 공유:
node:20-alpine이미지를 10개 서비스가 쓰면, lowerdir는 1개만 디스크에 저장 - 빠른 시작: 컨테이너 시작 시 파일을 복사하지 않고 lowerdir를 그대로 참조
- ECR 효율: 이미지 푸시/풀 시 변경된 레이어만 전송 → 배포 속도 개선
📖 더 보기: OverlayFS storage driver — Docker Docs — overlay2 드라이버 동작 원리, 레이어 관리, 성능 특성 공식 설명 (중급)
CoW “쓰기 시 복사” — 다른 도메인으로의 전이 원리
OverlayFS의 copy-up은 도메인 고유 기술이 아니다. “원본을 건드리지 않고, 쓸 때만 새 복사본을 만든다”는 원리는 Git·DB MVCC·가상 메모리 등 여러 분야에서 동일하게 등장한다. 이 공통 원리를 이해하면 낯선 시스템의 동작 방식을 추론할 수 있다.
| 도메인 | CoW 구현 단위 | ”원본” (읽기 전용) | “쓰기 레이어” (변경 격리) | 공통 효과 |
|---|---|---|---|---|
| OverlayFS (Docker) | 파일(copy-up) | lowerdir 이미지 레이어 | upperdir 컨테이너 레이어 | 이미지 공유·빠른 시작 |
| Git 커밋 DAG | 파일 tree 객체 | 부모 커밋 tree | 새 커밋 tree (변경된 blob만) | 브랜치 생성이 O(1) |
| DB MVCC (PostgreSQL) | 행(tuple) 버전 | 이전 tuple 버전 (pg_undo) | 새 tuple 버전 (heap) | 읽기/쓰기 비차단 |
| OS fork() | 메모리 페이지 | 부모 프로세스 페이지 | 자식 프로세스 페이지(쓰기 시) | fork가 O(1) |
핵심 패턴: “변경 전까지는 공유, 변경 시점에만 분기” — 이 패턴이 보이면 CoW 계열 설계임을 인식할 수 있다. Git의 브랜치가 가볍고, PostgreSQL 읽기가 쓰기를 차단하지 않으며, Docker 이미지 레이어가 공유되는 이유 모두 같은 원리다.
📖 참고: Copy-on-write — Wikipedia | Overlay Filesystem — Linux Kernel Docs | MVCC Deep Dive — CelerData
Docker 네트워크 내부 원리 — veth pair와 docker0 브리지
docker run -p 3000:3000 my-app을 실행하면 “컨테이너가 네트워크에 연결된다”는 결과가 나타난다. 내부에서는 Linux 네트워크 스택이 이 과정을 처리한다.
비유: 컨테이너와 호스트 사이에 “가상 랜선(veth pair)“을 꽂는다. 한쪽 끝은 컨테이너 안(eth0), 다른 쪽 끝은 호스트의 가상 스위치(docker0)에 꽂혀 있다. docker0는 모든 컨테이너를 연결하는 가상 허브 역할을 한다.
Bridge 네트워크 내부 동작 흐름:
# docker run 시 Docker 데몬이 자동으로 수행하는 작업
1. veth pair 생성 (가상 랜선 한 쌍) → vethXXXXXX (호스트 끝) → eth0 (컨테이너 끝, 새 Network Namespace 안에 배치)
2. vethXXXXXX를 docker0 브리지에 연결 (호스트 측) $ brctl show docker0 bridge name bridge id interfaces docker0 8000.abc123 veth1a2b3c ← 컨테이너1 veth4d5e6f ← 컨테이너2
3. 컨테이너 eth0에 IP 할당 (172.17.0.x 대역) $ docker exec my-app ip addr show eth0 # 예상 출력: # 3: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> # inet 172.17.0.2/16 brd 172.17.255.255
4. 포트 매핑: iptables NAT 규칙 추가 # 호스트의 3000 포트 → 컨테이너 172.17.0.2:3000으로 DNAT $ sudo iptables -t nat -L DOCKER --line-numbers # num target prot opt source destination # 1 DNAT tcp -- any any tcp dpt:3000 to:172.17.0.2:3000# 실행 중인 컨테이너의 네트워크 정보 확인docker network inspect bridge
# 예상 출력 (축약):# {# "Name": "bridge",# "Driver": "bridge",# "IPAM": {"Config": [{"Subnet": "172.17.0.0/16", "Gateway": "172.17.0.1"}]},# "Containers": {# "a3f1c8d2...": {# "Name": "my-app-1",# "IPv4Address": "172.17.0.2/16",# "MacAddress": "02:42:ac:11:00:02"# }# }# }Docker Compose가 서비스명으로 통신 가능한 이유: Compose는 별도의 사용자 정의 브리지 네트워크를 생성하고, 내장 DNS 서버가 서비스명을 해당 컨테이너 IP로 자동 변환한다. DATABASE_URL: postgresql://user:pass@db:5432/mydb에서 db가 컨테이너 IP로 해석되는 이유가 바로 이것이다.
📖 더 보기: Bridge network driver — Docker Docs — bridge 네트워크 생성, 사용자 정의 브리지, 포트 매핑 메커니즘 공식 설명 (중급)
이미지(Image)
컨테이너를 만들기 위한 설계도. 읽기 전용 템플릿이다. “Node.js 18 + 내 앱 코드 + 필요한 라이브러리”를 하나의 이미지로 만든다.
컨테이너(Container)
이미지를 실행한 인스턴스. 이미지가 클래스라면 컨테이너는 객체. 시작/중지/삭제가 빠르다.
# 컨테이너 상태 확인docker ps
# 예상 출력CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESa3f1c8d2e941 my-app:latest "node server.js" 2 minutes ago Up 2 minutes 0.0.0.0:3000->3000/tcp my-app-1
# 컨테이너 로그 확인docker logs my-app-1
# 예상 출력[Nest] 1 - 03/29/2026, 10:00:00 AM LOG [NestFactory] Starting Nest application...[Nest] 1 - 03/29/2026, 10:00:01 AM LOG [NestApplication] Nest application successfully startedListening on port 3000Dockerfile
이미지를 만드는 레시피 파일.
FROM node:18-alpineWORKDIR /appCOPY package*.json ./RUN npm installCOPY . .EXPOSE 3000CMD ["node", "server.js"]이미지 레이어와 캐시 — 왜 순서가 중요한가
Dockerfile의 각 명령은 레이어를 만든다. 변경되지 않은 레이어는 캐시된다. 아래처럼 설계해야 한다:
# ✅ 좋은 순서: 변경이 적은 것 먼저FROM node:18-alpineCOPY package*.json ./ # 의존성 파일 (자주 안 바뀜)RUN npm install # 캐시 활용COPY . . # 소스 코드 (자주 바뀜)
# ❌ 나쁜 순서: 코드 바뀔 때마다 npm install 재실행FROM node:18-alpineCOPY . . # 소스 코드 먼저RUN npm install # 코드 바뀌면 캐시 무효화레이어 순서 설계가 CI/CD 빌드 속도에 직접 영향을 준다. 코드만 바뀐 경우 npm install 단계를 캐시로 건너뛰면 수 분이 절약된다.
멀티스테이지 빌드 — 왜 프로덕션 이미지를 작게 만들어야 하는가
비유하자면, 멀티스테이지 빌드는 “공장(빌드 환경)과 진열대(실행 환경)를 분리”하는 것이다. 앱을 만드는 데 필요한 공구(TypeScript 컴파일러, 개발 의존성 등)는 공장에만 있으면 되고, 진열대(프로덕션 컨테이너)에는 완성된 제품만 넣는다.
단일 스테이지 NestJS 이미지는 1GB 이상이 될 수 있다. 멀티스테이지 빌드와 Alpine 베이스 이미지를 조합하면 150~200MB로 줄일 수 있다. 이미지가 작으면 ECR 푸시/풀 시간이 줄고, ECS 배포 속도도 빨라진다.
# NestJS 프로덕션용 멀티스테이지 Dockerfile# ---- Stage 1: 빌드 (빌더 환경) ----FROM node:20-alpine AS builder
WORKDIR /appCOPY package*.json ./RUN npm ci # 개발 의존성 포함 설치COPY . .RUN npm run build # TypeScript → JavaScript 컴파일 (dist/ 생성)
# ---- Stage 2: 실행 (프로덕션 환경) ----FROM node:20-alpine AS production
# 보안: root 대신 비권한 사용자로 실행RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /appCOPY package*.json ./RUN npm ci --only=production # 프로덕션 의존성만 설치 (devDependencies 제외)
COPY --from=builder /app/dist ./dist # 빌더에서 컴파일된 JS만 복사
# 헬스체크: 컨테이너가 실제로 동작하는지 확인HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ CMD wget --spider -q http://localhost:3000/health || exit 1
USER appuser # root 대신 일반 사용자로 실행 (보안)EXPOSE 3000CMD ["node", "dist/main.js"]# 멀티스테이지 빌드 전후 이미지 크기 비교docker images
# 예상 출력REPOSITORY TAG IMAGE ID SIZEmy-nestjs single-stage abc123def456 987MB ← 단일 스테이지 (node_modules + ts 포함)my-nestjs multi-stage 789ghi012jkl 178MB ← 멀티스테이지 (dist + prod deps만)--from=builder는 Stage 1에서 만들어진 파일시스템에서 파일을 복사하는 명령이다. Stage 1에서 설치된 TypeScript, ts-node, 기타 개발 도구는 Stage 2에 전혀 포함되지 않는다.
📖 더 보기: Docker Multi-stage Builds 공식 문서 — 멀티스테이지 빌드 공식 가이드 (입문)
HEALTHCHECK — 컨테이너가 살아있는지 확인하는 방법
컨테이너가 실행 중이어도 앱이 실제로 응답하는지는 별개다. HEALTHCHECK는 주기적으로 앱의 상태를 확인하고, 비정상이면 오케스트레이터(ECS, Kubernetes)가 자동으로 재시작할 수 있도록 신호를 보낸다.
# 컨테이너 헬스체크 상태 확인docker inspect --format='{{json .State.Health}}' my-app-1
# 예상 출력 (정상):# {"Status":"healthy","FailingStreak":0,"Log":[...마지막 5회 결과...]}
# 예상 출력 (비정상):# {"Status":"unhealthy","FailingStreak":3,"Log":[...]}NestJS 앱에는 반드시 /health 엔드포인트를 만들어야 ECS ALB 헬스체크와 HEALTHCHECK가 동작한다:
@Controller("health")export class HealthController { @Get() check() { return { status: "ok", timestamp: new Date().toISOString() }; }}프로덕션 보안 체크리스트
2025년 기준 프로덕션 Docker 환경에서 지켜야 할 핵심 보안 원칙:
- 비root 사용자 실행:
USER appuser— root로 컨테이너가 탈취되면 호스트까지 위험 - 베이스 이미지 최신 유지:
node:20-alpine처럼 latest 대신 버전 고정, 주기적 업데이트 - 이미지 취약점 스캔:
docker scout cves my-app:latest또는 Trivy로 정기 스캔 - 읽기 전용 파일시스템:
docker run --read-only(필요한 경로만 volume으로 쓰기 허용) - .dockerignore 작성:
.env,node_modules,.git등 민감한 파일이 이미지에 포함되지 않게
# .dockerignore 예시node_modules.env.env.local.gitdist*.logDistroless 이미지 — 보안을 한 단계 더
Alpine보다 더 공격 표면을 줄이고 싶다면 Google의 Distroless 이미지를 사용한다. Distroless는 쉘(sh, bash)과 패키지 매니저(apk, apt)가 완전히 제거된 이미지다. 컨테이너가 탈취당해도 공격자가 쉘을 실행하거나 추가 도구를 설치할 수 없다.
# 멀티스테이지 빌드 + Distroless 조합FROM node:20-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build
# Distroless Node.js 이미지 사용 (쉘 없음, 패키지 매니저 없음)FROM gcr.io/distroless/nodejs20-debian12 AS productionWORKDIR /appCOPY --from=builder /app/dist ./distCOPY --from=builder /app/node_modules ./node_modulesEXPOSE 3000CMD ["dist/main.js"] # node 커맨드 없이 경로만 지정# 베이스 이미지 비교node:20 ~1.1GB (개발 도구 전체 포함)node:20-alpine ~180MB (Alpine, 쉘 포함)distroless/nodejs20 ~120MB (쉘 없음, 최소 공격 표면)단점: 쉘이 없어서 docker exec -it <id> sh로 컨테이너 내부에 들어갈 수 없다. 디버깅이 필요할 때 불편하므로, 로컬 개발은 Alpine, 프로덕션은 Distroless를 분리 사용하는 것이 일반적이다.
컨테이너 보안 — 2025년 기준 공격 벡터와 방어 설계
2025년 Sysdig Cloud-Native Security Report에 따르면 컨테이너 관련 보안 사고가 전년 대비 47% 증가했다. 주요 공격 벡터는 취약한 베이스 이미지(32%), root 실행 컨테이너(28%), 하드코딩된 시크릿(12%) 순이다.
왜 컨테이너를 root로 실행하면 위험한가: 컨테이너 탈출 취약점(Container Escape)이 발견되면 root 권한의 컨테이너는 호스트 OS에 root로 접근할 수 있다. 비권한 사용자(UID 1000+)로 실행하면 탈출 성공 후에도 호스트에서 권한이 제한된다.
# ❌ 위험: root로 실행 (기본값)FROM node:20-alpineCMD ["node", "dist/main.js"]# 공격자가 컨테이너 탈출 시 호스트에 root 접근 가능
# ✅ 안전: 비권한 사용자 실행FROM node:20-alpineRUN addgroup -S appgroup && adduser -S appuser -G appgroupUSER appuser # UID 1000으로 실행CMD ["node", "dist/main.js"]# 컨테이너 실행 사용자 확인docker run --rm my-nestjs-app whoami# 안전한 경우: appuser# 위험한 경우: root
# 실행 중인 컨테이너의 사용자 확인docker inspect --format='{{.Config.User}}' my-app-1# 빈 문자열이면 root로 실행 중 → 수정 필요Linux Capabilities 제한 — —cap-drop으로 공격 표면 최소화
컨테이너는 기본적으로 14가지 Linux Capability를 갖는다. 대부분의 웹 앱은 이 중 하나도 필요하지 않다.
# 모든 Capability 제거 후 실행 (웹 서버에 권장)docker run --cap-drop ALL my-nestjs-app
# Compose에서 Capability 제한services: app: cap_drop: - ALL # 모든 권한 제거 cap_add: - NET_BIND_SERVICE # 1024 이하 포트 바인딩이 필요한 경우만 security_opt: - no-new-privileges:true # setuid 바이너리로 권한 상승 차단# Capability 제한 전/후 비교제한 전: 컨테이너가 네트워크 패킷 스니핑, 커널 모듈 로드, 파일시스템 마운트 등 가능제한 후: Node.js 앱 실행에 필요한 것 외에 모든 시스템 콜 거부→ 취약점 악용 시 공격자가 할 수 있는 행동이 대폭 제한됨Docker Compose
여러 컨테이너를 한 번에 정의하고 실행하는 도구. “웹 서버 + DB + Redis를 한 번에 띄워라”를 docker-compose.yml 하나로 관리한다.
# NestJS 앱 + PostgreSQL 로컬 개발 환경 예시services: app: build: . ports: - "3000:3000" environment: DATABASE_URL: postgresql://user:pass@db:5432/mydb depends_on: db: condition: service_healthy # DB가 healthy 상태일 때만 앱 시작 mem_limit: 512m # 메모리 제한 — OOM killer 방지 db: image: postgres:15 environment: POSTGRES_USER: user POSTGRES_PASSWORD: pass POSTGRES_DB: mydb volumes: - db-data:/var/lib/postgresql/data # 데이터 영속화 healthcheck: test: ["CMD-SHELL", "pg_isready -U user -d mydb"] interval: 10s timeout: 5s retries: 5
volumes: db-data:docker-compose up -d # 백그라운드로 전체 서비스 실행# 예상 출력:# [+] Running 2/2# ✔ Container my-project-db-1 Started# ✔ Container my-project-app-1 StartedDocker 네트워크 모드 — Compose에서 서비스명으로 통신이 되는 이유
Docker 컨테이너의 기본 네트워크 모드는 bridge다. bridge 모드에서 컨테이너는 가상 네트워크(docker0)에 연결되어 독립적인 IP를 할당받고, 컨테이너끼리 통신하려면 포트 매핑이나 네트워크 연결이 필요하다.
Docker Compose는 실행 시 자동으로 프로젝트 전용 default network를 생성하고, 모든 서비스를 여기에 연결한다. 같은 Compose 파일 안의 서비스들은 이 네트워크 안에서 서비스명(예: db, redis)을 hostname으로 사용해 직접 통신할 수 있다. 위 예시에서 DATABASE_URL에 @db:5432가 동작하는 이유가 바로 이 때문이다. Docker의 내장 DNS가 서비스명을 해당 컨테이너 IP로 자동 변환해준다.
Docker 네트워크 모드 비교├── bridge (기본) ← 컨테이너별 격리. 포트 매핑으로 호스트와 통신├── host ← 컨테이너가 호스트 네트워크를 그대로 사용. 격리 없음, 포트 충돌 주의└── none ← 네트워크 완전 비활성화. 보안 민감 작업에만 사용host 모드는 컨테이너가 호스트 네트워크 인터페이스를 그대로 공유하므로 포트 매핑 없이 호스트 IP로 직접 접근된다. 성능 상 유리하지만 컨테이너 간 격리가 없어 프로덕션에서는 잘 쓰지 않는다.
컨테이너 데이터 휘발성
컨테이너가 종료·삭제되면 내부에 저장된 데이터도 함께 사라진다. DB나 로그처럼 유지해야 할 데이터는 **볼륨(Volume)**을 마운트해서 컨테이너 밖에 저장해야 한다.
docker run -v /host/data:/app/data myapp # 호스트 경로를 컨테이너에 마운트레지스트리(Registry)
이미지를 저장하고 공유하는 저장소. Docker Hub(공개), ECR(AWS 전용), 사내 레지스트리 등이 있다.
BuildKit — 왜 현대 Docker 빌드는 BuildKit을 쓰는가
Docker 18.09부터 도입된 BuildKit은 기존 빌더 대비 빌드 성능과 캐시 효율이 크게 개선됐다. 2023년부터 Docker Desktop에서 기본 활성화됐다.
핵심 차이점: 기존 빌더는 레이어를 순서대로 직렬 실행하지만, BuildKit은 독립적인 빌드 단계를 병렬 실행한다. 멀티스테이지 빌드에서 두 개의 독립적인 스테이지가 동시에 실행되므로 전체 빌드 시간이 줄어든다.
# BuildKit 활성화 (환경변수)DOCKER_BUILDKIT=1 docker build -t my-app .
# Docker Compose에서 BuildKit 사용COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose build
# 또는 ~/.docker/daemon.json에 영구 설정{ "features": { "buildkit": true }}# BuildKit 전용 시크릿 마운트 — .env 파일이 이미지 레이어에 남지 않음# ❌ 위험: ARG/ENV로 시크릿 전달 시 이미지 레이어에 영구 저장됨# ARG NPM_TOKEN# RUN npm config set //registry.npmjs.org/:_authToken=${NPM_TOKEN}
# ✅ 안전: BuildKit 시크릿 마운트 (빌드 후 레이어에서 사라짐)# syntax=docker/dockerfile:1FROM node:20-alpine AS builderRUN --mount=type=secret,id=npm_token \ NPM_TOKEN=$(cat /run/secrets/npm_token) \ npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN \ && npm ci# BuildKit 시크릿 전달docker build --secret id=npm_token,src=.npmrc -t my-app .# .npmrc 파일 내용이 빌드 중에만 사용되고 이미지에는 포함되지 않음Docker가 부적합한 상황 — Anti-Pattern 판단 기준
Docker는 대부분의 웹 서비스에 적합하지만, 아래 상황에서는 오버헤드가 이점을 앞선다. 도입 전 체크리스트로 활용한다.
| 상황 | 문제 원인 | 판단 기준 | 권장 대안 |
|---|---|---|---|
| GPU 집약적 HPC / ML 학습 | Docker 스케줄러가 GPU 격리를 지원하지 않아 “rogue job”이 GPU 전체 선점 가능. InfiniBand 네트워크와 호환성 낮음 | 멀티 GPU MPI 병렬 작업, GPU 할당 보장이 필요한 경우 | Singularity(Apptainer) — HPC 잡 스케줄러(SLURM) 통합, 비루트 실행, InfiniBand 직접 접근 지원 |
| 실시간 커널 요구 (RT Linux) | Docker는 호스트 커널을 공유하지만 컨테이너 내부에서 PREEMPT_RT 패치를 적용할 수 없음. 스케줄링 지연(jitter)이 증가 | 마이크로초 단위 결정론적 응답이 필요한 제어 시스템, 로봇, 오디오 | 베어 메탈 RT 커널 — CONFIG_PREEMPT_RT 패치 적용, CPU affinity + memory locking 직접 설정 |
| 극단적 저지연 네트워크 (DPDK/RDMA) | 컨테이너 네트워크 스택(veth pair + iptables NAT)이 추가 레이어를 도입. 패킷당 1–10 µs 추가 지연 발생 가능 | 고빈도 트레이딩, RDMA 기반 스토리지, 패킷 처리 전용 워크로드 | host 네트워크 모드(격리 포기) 또는 Unikernel — 부팅 12 ms, 컨테이너 대비 커널 스택 오버헤드 제거 |
| 루트리스 불가 HPC 클러스터 | Docker 데몬은 root 권한 필요. 공유 슈퍼컴퓨터 환경에서 보안 정책 위반 | 다중 사용자 공유 클러스터, 시스템 관리자가 Docker 루트 허용 불가인 경우 | Charliecloud / Shifter — NERSC 개발, 비루트 언프리빌리지드 컨테이너, HPC 전용 |
| IoT 엣지 / 초저전력 디바이스 | Docker 데몬 상시 실행 오버헤드 (~100 MB RAM), 수십 ms 컨테이너 시작 지연이 자원 제약 기기에서 문제 | 임베디드 리눅스, MCU 수준 메모리, 배터리 제약 엣지 기기 | Unikernel (OSv, MirageOS) 또는 경량 런타임 (containerd + runc 직접 사용) |
📖 참고: Docker Compatibility with Singularity for HPC — NVIDIA Blog | Unikernels vs Containers — arXiv 2509.07891
4. 실무에서 어디에 쓰이나
섹션 제목: “4. 실무에서 어디에 쓰이나”- 로컬 개발환경 통일 (모든 개발자가 같은 환경에서 개발)
- CI/CD 파이프라인 (빌드 → 이미지 생성 → 배포)
- 마이크로서비스 배포 (서비스별로 독립적인 컨테이너)
- ECS, Kubernetes 같은 오케스트레이션 도구의 기본 단위
5. 현재 내 업무와 연결점
섹션 제목: “5. 현재 내 업무와 연결점”- 배포 시 Docker 이미지가 어떻게 빌드되고 푸시되는지 이해해야 함
- 서비스 장애 시 컨테이너 로그를 확인해야 함
- 로컬에서 서비스를 띄워볼 때 Docker Compose 사용
- ECS에서 돌아가는 서비스를 이해하려면 컨테이너 개념이 선수지식
6. 자주 헷갈리는 개념 비교
섹션 제목: “6. 자주 헷갈리는 개념 비교”| 개념 A | 개념 B | 차이점 |
|---|---|---|
| 이미지 | 컨테이너 | 이미지 = 설계도(읽기전용), 컨테이너 = 실행된 인스턴스 |
| Docker | Docker Compose | Docker = 단일 컨테이너 관리, Compose = 여러 컨테이너를 한 번에 |
| 컨테이너 | VM | 컨테이너는 OS 커널 공유(가벼움), VM은 OS 전체 가상화(무거움) |
| EXPOSE | 포트 바인딩(-p) | EXPOSE는 문서화 용도, 실제 외부 접근은 -p 3000:3000으로 매핑해야 함 |
| 단일스테이지 | 멀티스테이지 | 단일스테이지는 빌드 도구가 이미지에 포함(크고 느림), 멀티스테이지는 실행에 필요한 것만 포함(작고 빠름) |
| HEALTHCHECK | docker ps | HEALTHCHECK는 앱 내부 동작 확인, docker ps는 프로세스 실행 여부만 확인 |
6.5. 트러블슈팅
섹션 제목: “6.5. 트러블슈팅”🔧 “port is already allocated” — 포트 충돌
섹션 제목: “🔧 “port is already allocated” — 포트 충돌”증상: docker run -p 3000:3000 ... 실행 시 Error: bind: address already in use 또는 port is already allocated
원인: 로컬 3000번 포트를 이미 다른 프로세스(또는 이전에 멈춘 컨테이너)가 점유 중
해결:
# 어떤 프로세스가 포트를 쓰는지 확인lsof -i :3000
# 이전에 멈춘 컨테이너가 원인이라면docker ps -a # 중지된 컨테이너 포함 목록 확인docker rm <container_id> # 해당 컨테이너 삭제 후 재실행🔧 컨테이너가 바로 종료됨 — 로그 확인이 핵심
섹션 제목: “🔧 컨테이너가 바로 종료됨 — 로그 확인이 핵심”증상: docker run 후 컨테이너가 즉시 종료. docker ps에서 보이지 않음
원인: 앱 자체가 에러로 크래시하거나, Dockerfile의 CMD가 잘못 설정됨 (예: 백그라운드 프로세스로 실행해서 메인 프로세스가 없음)
해결:
# 종료된 컨테이너 로그 확인 (-a로 멈춘 것도 포함)docker ps -adocker logs <container_id>
# 예상 출력 (NestJS 앱이 DB 연결 실패로 크래시한 경우)Error: connect ECONNREFUSED 127.0.0.1:5432 at TCPConnectWrap.afterConnectdocker logs가 빈 경우: CMD가 잘못돼서 아예 실행이 안 된 것. Dockerfile의 CMD/ENTRYPOINT를 확인할 것.
🔧 이미지 빌드 시 변경사항이 반영 안 됨
섹션 제목: “🔧 이미지 빌드 시 변경사항이 반영 안 됨”증상: 코드를 수정했는데 컨테이너 안에서 이전 코드가 실행됨
원인: 이전에 빌드한 이미지를 그대로 사용 중. docker run my-app은 로컬에서 가장 최근 태그를 쓰지만, 이미 실행 중인 컨테이너는 재빌드가 필요 없다고 착각할 수 있음
해결:
# 이미지 재빌드 후 컨테이너 재시작docker build -t my-app .docker-compose up --build # Compose 사용 시 --build 플래그 필수
# 캐시를 완전히 무시하고 재빌드docker build --no-cache -t my-app .🔧 컨테이너가 OOM(Out of Memory)으로 강제 종료됨
섹션 제목: “🔧 컨테이너가 OOM(Out of Memory)으로 강제 종료됨”증상: 컨테이너가 갑자기 종료되고 docker inspect에서 OOMKilled: true 확인됨. ECS에서는 태스크가 STOPPED되고 Stopped Reason에 “OutOfMemoryError: Container killed due to memory usage” 표시
원인: 컨테이너에 메모리 제한이 설정되어 있는데 앱이 그 이상을 사용하려 함. Node.js의 기본 힙 사이즈(약 1.5GB)가 컨테이너 메모리 제한보다 크거나, 메모리 누수가 있는 경우
해결:
# 컨테이너 OOM 여부 확인docker inspect <container_id> | grep OOMKilled# "OOMKilled": true 이면 OOM으로 종료된 것
# Node.js 힙 사이즈를 컨테이너 메모리에 맞게 제한CMD ["node", "--max-old-space-size=400", "dist/main.js"]# 512MB 컨테이너라면 400MB로 힙 제한
# docker-compose에서 메모리 제한 + 예약 설정services: app: mem_limit: 512m # 최대 사용량 mem_reservation: 256m # 보장 사용량🔧 HEALTHCHECK가 항상 unhealthy — curl 없는 Alpine 이미지
섹션 제목: “🔧 HEALTHCHECK가 항상 unhealthy — curl 없는 Alpine 이미지”증상: Dockerfile에 HEALTHCHECK CMD curl -f http://localhost:3000/health를 추가했는데 컨테이너가 unhealthy 상태
원인: Alpine 기반 이미지에는 curl이 기본 포함되어 있지 않음. curl: not found 에러로 헬스체크가 실패
해결: curl 대신 wget 사용하거나 curl을 명시적으로 설치
# ✅ Alpine에서 wget 사용 (기본 포함)HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ CMD wget --spider -q http://localhost:3000/health || exit 1
# 또는 curl 설치RUN apk add --no-cache curlHEALTHCHECK CMD curl -f http://localhost:3000/health || exit 17. 체크리스트
섹션 제목: “7. 체크리스트”- 컨테이너와 VM의 차이를 설명할 수 있다
- 이미지와 컨테이너의 관계를 설명할 수 있다
- Dockerfile의 기본 명령어(FROM, COPY, RUN, CMD)를 이해한다
-
docker run,docker ps,docker logs명령어를 사용할 수 있다 - Docker Compose로 여러 서비스를 동시에 띄울 수 있다
- 멀티스테이지 빌드가 왜 프로덕션 이미지 크기를 줄이는지 설명할 수 있다
- HEALTHCHECK를 추가하고 상태를 확인할 수 있다
8. 추가 학습 키워드
섹션 제목: “8. 추가 학습 키워드”Docker 네트워크, Docker 볼륨, .dockerignore, Alpine vs Debian 이미지, Docker 보안, ECR 이미지 스캔, BuildKit, Docker Scout
8.5. 추천 리소스
섹션 제목: “8.5. 추천 리소스”📚 추천 리소스
섹션 제목: “📚 추천 리소스”- 📖 Docker 공식 문서 — Get Started — 이미지 빌드부터 컨테이너 실행까지 공식 튜토리얼, 실습 중심 (입문)
- 🎬 Docker in 100 Seconds — Fireship — Docker 핵심 개념을 100초로 요약한 영상 (입문)
- 📖 Docker Multi-stage Builds 공식 문서 — 멀티스테이지 빌드를 공식으로 설명하는 가이드, NestJS 예시 적용 가능 (입문)
- 📖 NestJS Docker 이미지 최적화 — oneuptime 블로그 — NestJS 앱을 멀티스테이지 빌드로 최적화하는 실전 가이드 (중급)
- 📖 Docker Production Best Practices 2025 — Mykola Aleksandrov — 보안, 최적화, 모니터링을 포함한 프로덕션 Docker 운영 가이드 (중급)
9. 내가 직접 확인해볼 것
섹션 제목: “9. 내가 직접 확인해볼 것”-
docker run -it ubuntu bash로 Ubuntu 컨테이너 안에 들어가보기
docker run -it ubuntu bash# 예상 출력: root@a3f1c8d2:/# (컨테이너 안 쉘)# 호스트와 완전히 격리된 환경임을 확인# exit로 나오면 컨테이너 종료- 간단한 Dockerfile 작성 → 이미지 빌드 → 컨테이너 실행
docker build -t my-nestjs-app .# 예상 출력:# [+] Building 23.4s (10/10) FINISHED# => [builder 1/5] FROM node:20-alpine# => [builder 2/5] COPY package*.json ./# => [builder 3/5] RUN npm ci# => [production 1/3] COPY --from=builder /app/dist ./dist# => exporting to image
docker run -p 3000:3000 my-nestjs-app# 예상 출력:# [Nest] LOG [NestApplication] Nest application successfully started +1ms-
docker ps로 실행 중인 컨테이너 확인,docker logs로 로그 보기 - 팀 서비스의 Dockerfile을 열어보고 각 줄이 뭘 하는지 읽어보기
-
docker-compose up으로 로컬 개발환경 띄워보기
# 이미지 보안 취약점 스캔 (Docker Scout 사용)docker scout cves my-nestjs-app:latest# 예상 출력:# ✓ SBOM of image already cached, 234 packages indexed# ✓ No vulnerable packages detected# (또는) ! 3 packages with critical vulnerabilities foundGraceful Shutdown — 컨테이너가 종료될 때 요청을 잃지 않는 방법
ECS가 컨테이너를 교체할 때, Docker는 먼저 SIGTERM 신호를 보내고 일정 시간(기본 30초) 후에 강제 종료(SIGKILL)한다. NestJS 앱이 SIGTERM을 받으면 처리 중인 요청을 완료하고 우아하게 종료해야 한다. 이를 Graceful Shutdown이라고 한다.
// main.ts — NestJS Graceful Shutdown 설정async function bootstrap() { const app = await NestFactory.create(AppModule);
// SIGTERM 신호 수신 시 graceful shutdown 활성화 app.enableShutdownHooks();
await app.listen(3000);}# Dockerfile — Node.js가 SIGTERM을 직접 받도록 exec 형식 사용# ❌ shell 형식 (SIGTERM이 sh에 전달됨, Node.js에 전달 안 됨)CMD node dist/main.js
# ✅ exec 형식 (SIGTERM이 Node.js 프로세스에 직접 전달됨)CMD ["node", "dist/main.js"]Graceful Shutdown이 없으면 배포 중에 처리 중이던 요청(결제, DB 트랜잭션 등)이 중간에 끊겨 데이터 불일치가 발생할 수 있다.
10. 요약 — 이것만 기억해도 된다
섹션 제목: “10. 요약 — 이것만 기억해도 된다”빠른 판단 기준
섹션 제목: “빠른 판단 기준”| 상황 | 대응 |
|---|---|
| 배포 후 서비스가 응답 안 함 | docker logs <id> → 앱 크래시 원인 확인 |
| 컨테이너가 갑자기 죽음 | docker inspect OOMKilled 확인 → 메모리 제한 조정 |
| 이미지가 너무 커서 ECR 배포 느림 | 멀티스테이지 빌드 + Alpine 이미지 적용 |
| 로컬에서만 되고 컨테이너에서 안 됨 | .dockerignore 확인, 환경변수 주입 여부 확인 |
| 헬스체크가 unhealthy | Alpine이면 curl 없음 → wget 사용 |
| 배포 중 요청이 끊김 | Graceful Shutdown + CMD ["node", ...] exec 형식 |
5줄 핵심
섹션 제목: “5줄 핵심”- Docker는 앱을 환경째 패키징해서 어디서든 동일하게 실행하는 기술이다
- 이미지(설계도)를 만들고, 그걸 실행하면 컨테이너(인스턴스)가 된다
- Dockerfile로 이미지 레시피를 작성하고, Docker Compose로 여러 컨테이너를 관리한다
- 멀티스테이지 빌드로 프로덕션 이미지를 1GB → 180MB로 줄여 배포 속도를 개선한다
- ECS, Kubernetes 같은 오케스트레이션을 이해하려면 Docker가 선수지식이다