Skip to content

Split View: Docker 완전 정복 2025: 컨테이너 기초부터 멀티스테이지 빌드, Docker Compose, 프로덕션 배포까지

✨ Learn with Quiz
|

Docker 완전 정복 2025: 컨테이너 기초부터 멀티스테이지 빌드, Docker Compose, 프로덕션 배포까지

들어가며

컨테이너 기술은 현대 소프트웨어 개발의 근간이 되었습니다. "내 로컬에서는 되는데?"라는 전설적인 변명을 없앤 것이 바로 Docker입니다. 2013년 등장 이후 Docker는 개발, 테스트, 배포의 패러다임을 완전히 바꾸어 놓았고, 2025년 현재는 Kubernetes, 서버리스, AI 워크로드까지 컨테이너가 기본 단위로 자리 잡았습니다.

이 글에서는 컨테이너의 근본 원리부터 Dockerfile 최적화, 멀티스테이지 빌드, Docker Compose 실전, 보안, CI/CD 파이프라인, 프로덕션 운영, 그리고 Docker 대안과 AI 워크로드까지 체계적으로 다룹니다.


1. 컨테이너란 무엇인가 — VM과의 차이

컨테이너의 정의

컨테이너는 애플리케이션과 그 의존성을 하나의 패키지로 묶어 격리된 환경에서 실행하는 기술입니다. 가상 머신(VM)과 달리 OS 커널을 공유하기 때문에 훨씬 가볍고 빠릅니다.

Linux의 3가지 핵심 기술

컨테이너는 Linux 커널의 세 가지 기능을 조합한 것입니다.

1) Namespaces — 격리

프로세스가 볼 수 있는 시스템 자원의 범위를 제한합니다.

Namespace격리 대상
PID프로세스 ID
NET네트워크 인터페이스, 라우팅
MNT파일시스템 마운트 포인트
UTS호스트명, 도메인명
IPC프로세스 간 통신
USER사용자 및 그룹 ID
CGROUPcgroup 루트 디렉토리

2) Cgroups — 자원 제한

CPU, 메모리, 디스크 I/O, 네트워크 대역폭 등 자원 사용량을 제한합니다.

# cgroup으로 메모리를 256MB로 제한하는 예시
sudo cgcreate -g memory:mycontainer
echo 268435456 | sudo tee /sys/fs/cgroup/memory/mycontainer/memory.limit_in_bytes

3) Union Filesystem — 레이어 구조

여러 파일시스템 레이어를 하나로 합쳐서 보여주는 기술입니다. Docker는 OverlayFS를 사용합니다.

  • 읽기 전용(Read-Only) 레이어: 베이스 이미지, 패키지 설치 등
  • 쓰기 가능(Read-Write) 레이어: 컨테이너 실행 시 변경 사항

컨테이너 vs VM 비교

┌─────────────────────────────────────────────────────────────┐
│                   컨테이너 아키텍처                           │
├──────────┬──────────┬──────────┬──────────┐                 │
App AApp BApp CApp D   │                 │
├──────────┼──────────┼──────────┼──────────┤                 │
Bins/Bins/Bins/Bins/   │                 │
LibsLibsLibsLibs    │                 │
├──────────┴──────────┴──────────┴──────────┤                 │
Container Runtime                │                 │
├────────────────────────────────────────────┤                 │
Host OS Kernel                │                 │
├────────────────────────────────────────────┤                 │
Infrastructure                │                 │
└────────────────────────────────────────────┘                 │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
VM 아키텍처                             │
├──────────┬──────────┬──────────┬──────────┐                 │
App AApp BApp CApp D   │                 │
├──────────┼──────────┼──────────┼──────────┤                 │
Bins/Bins/Bins/Bins/   │                 │
LibsLibsLibsLibs    │                 │
├──────────┼──────────┼──────────┼──────────┤                 │
Guest OSGuest OSGuest OSGuest OS │                 │
├──────────┴──────────┴──────────┴──────────┤                 │
Hypervisor                    │                 │
├────────────────────────────────────────────┤                 │
Host OS                       │                 │
├────────────────────────────────────────────┤                 │
Infrastructure                │                 │
└────────────────────────────────────────────┘                 │
└─────────────────────────────────────────────────────────────┘
항목컨테이너VM
시작 시간밀리초수 분
이미지 크기MB 단위GB 단위
성능 오버헤드거의 없음10~20%
격리 수준프로세스 수준하드웨어 수준
OS 지원호스트 커널 공유독립 OS 가능

OCI 표준

OCI(Open Container Initiative)는 컨테이너 이미지와 런타임의 개방형 표준을 정의합니다.

  • Runtime Specification: 컨테이너 실행 방법 표준화
  • Image Specification: 컨테이너 이미지 포맷 표준화
  • Distribution Specification: 이미지 배포 방법 표준화

Docker, Podman, containerd 모두 OCI 표준을 따르므로 이미지가 호환됩니다.


2. Docker 아키텍처

Client-Server 모델

Docker는 클라이언트-서버 아키텍처를 사용합니다.

┌─────────────┐     REST API     ┌─────────────────────┐
Docker     │ ──────────────>Docker DaemonCLI   (dockerd)│              │                 │                      │
│  docker run  │                 │  ┌─────────────────┐ │
│  docker build│                 │  │   containerd    │ │
│  docker pull │                 │  │                 │ │
└─────────────┘                 │  │  ┌───────────┐  │ │
                                │  │  │   runc    │  │ │
                                │  │  └───────────┘  │ │
                                │  └─────────────────┘ │
                                └─────────────────────┘
  • Docker CLI: 사용자가 명령을 입력하는 클라이언트
  • Docker Daemon (dockerd): 컨테이너 라이프사이클을 관리하는 서버 프로세스
  • containerd: OCI 호환 컨테이너 런타임 관리자
  • runc: 실제로 컨테이너를 생성하고 실행하는 저수준 런타임

핵심 개념 4가지

1) 이미지 (Image)

읽기 전용 템플릿으로, 컨테이너를 만드는 청사진입니다. 레이어 구조로 되어 있어 효율적으로 저장하고 전송합니다.

# 이미지 관리 명령어
docker images                    # 로컬 이미지 목록
docker pull nginx:1.25-alpine    # 이미지 다운로드
docker image inspect nginx       # 이미지 상세 정보
docker image prune               # 사용하지 않는 이미지 정리

2) 컨테이너 (Container)

이미지의 실행 인스턴스입니다. 이미지 위에 쓰기 가능한 레이어가 추가됩니다.

# 컨테이너 실행 및 관리
docker run -d --name my-nginx -p 8080:80 nginx:1.25-alpine
docker ps                        # 실행 중인 컨테이너 목록
docker logs my-nginx             # 로그 확인
docker exec -it my-nginx /bin/sh # 컨테이너 내부 접속
docker stop my-nginx             # 정지
docker rm my-nginx               # 삭제

3) 볼륨 (Volume)

컨테이너의 데이터를 영구적으로 저장하는 메커니즘입니다.

# 볼륨 생성 및 사용
docker volume create my-data
docker run -d -v my-data:/app/data my-app
docker run -d -v /host/path:/container/path my-app  # 바인드 마운트

4) 네트워크 (Network)

컨테이너 간 통신을 관리합니다.

# 네트워크 생성 및 관리
docker network create my-network
docker run -d --network my-network --name api my-api
docker run -d --network my-network --name db postgres
# api 컨테이너에서 db:5432로 접근 가능

3. Dockerfile 마스터 클래스

주요 명령어 해설

# 베이스 이미지 선택
FROM node:20-alpine

# 메타데이터 추가
LABEL maintainer="dev@example.com"
LABEL version="1.0"

# 빌드 시 변수 (빌드 시에만 사용)
ARG NODE_ENV=production

# 환경 변수 (런타임에도 사용)
ENV NODE_ENV=$NODE_ENV
ENV PORT=3000

# 작업 디렉토리 설정
WORKDIR /app

# 파일 복사 (COPY 권장)
COPY package*.json ./

# 명령어 실행
RUN npm ci --only=production

# 소스 코드 복사
COPY . .

# 포트 문서화 (실제 포트 열지는 않음)
EXPOSE 3000

# 컨테이너 상태 체크
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

# 실행할 사용자 변경 (보안)
USER node

# 컨테이너 시작 시 실행할 명령
ENTRYPOINT ["node"]
CMD ["server.js"]

COPY vs ADD 차이

기능COPYADD
로컬 파일 복사OO
URL 다운로드XO
tar 자동 압축 해제XO
권장도권장특수한 경우만

결론: 대부분의 경우 COPY를 사용하세요. ADD는 tar 자동 해제가 필요할 때만 사용합니다.

CMD vs ENTRYPOINT 차이

# CMD만 사용 - docker run 시 완전 대체 가능
CMD ["python", "app.py"]
# docker run my-image python test.py  (CMD가 test.py로 대체됨)

# ENTRYPOINT만 사용 - 항상 실행됨
ENTRYPOINT ["python"]
# docker run my-image app.py  (python app.py로 실행)

# ENTRYPOINT + CMD 조합 (가장 유연)
ENTRYPOINT ["python"]
CMD ["app.py"]
# docker run my-image         -> python app.py
# docker run my-image test.py -> python test.py

ARG vs ENV

# ARG: 빌드 시에만 존재 (런타임에는 없음)
ARG BUILD_VERSION=1.0
RUN echo "Building version: $BUILD_VERSION"

# ENV: 빌드 시 + 런타임 모두 존재
ENV APP_VERSION=1.0
# 컨테이너 실행 시에도 APP_VERSION 사용 가능

Dockerfile 최적화 베스트 프랙티스

1) 레이어 순서가 캐시를 결정한다

# 나쁜 예: 소스 변경 시 npm install 재실행
COPY . .
RUN npm install

# 좋은 예: package.json 변경 시에만 npm install 재실행
COPY package*.json ./
RUN npm ci
COPY . .

2) RUN 명령을 합치기

# 나쁜 예: 3개의 레이어 생성
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# 좋은 예: 1개의 레이어 생성 + 캐시 정리
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

3) .dockerignore 활용

# .dockerignore
node_modules
.git
.env
*.md
Dockerfile
docker-compose*.yml
.github
coverage
dist

4) 작은 베이스 이미지 선택

이미지크기용도
ubuntu:22.04~77MB풀 OS 필요 시
node:20-slim~200MB대부분의 Node.js 앱
node:20-alpine~130MB경량 Node.js 앱
alpine:3.19~7MB최소 베이스
distroless~2MB최소한의 런타임만
scratch0MB정적 바이너리 전용

4. 멀티스테이지 빌드 — 이미지 90% 줄이기

멀티스테이지 빌드는 빌드 도구와 최종 실행 환경을 분리하여 이미지 크기를 극적으로 줄이는 기법입니다.

Node.js 예제: 1.2GB에서 120MB로

# Stage 1: 빌드 환경
FROM node:20 AS builder
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# Stage 2: 프로덕션 환경
FROM node:20-alpine AS production
WORKDIR /app

# 프로덕션 의존성만 설치
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# 빌드 결과물만 복사
COPY --from=builder /app/dist ./dist

# 보안: non-root 사용자
USER node

EXPOSE 3000
CMD ["node", "dist/server.js"]

결과: node:20 풀 이미지(~1.1GB) 대신 Alpine 기반(~130MB) + 빌드 도구 제외로 최종 120MB

Go 예제: 800MB에서 15MB로

# Stage 1: 빌드
FROM golang:1.22-alpine AS builder
WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server .

# Stage 2: 최소 이미지
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server

EXPOSE 8080
ENTRYPOINT ["/server"]

결과: Go 빌드 환경(~800MB)에서 정적 바이너리만 추출하여 15MB 이하

Java/Spring Boot 예제: 400MB에서 80MB로

# Stage 1: 빌드
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app

COPY gradlew build.gradle settings.gradle ./
COPY gradle ./gradle
RUN ./gradlew dependencies --no-daemon

COPY src ./src
RUN ./gradlew bootJar --no-daemon

# Stage 2: 런타임
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app

COPY --from=builder /app/build/libs/*.jar app.jar

RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Python 예제

# Stage 1: 의존성 빌드
FROM python:3.12-slim AS builder
WORKDIR /app

RUN pip install --no-cache-dir poetry
COPY pyproject.toml poetry.lock ./
RUN poetry export -f requirements.txt -o requirements.txt --without-hashes
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# Stage 2: 런타임
FROM python:3.12-slim
WORKDIR /app

COPY --from=builder /install /usr/local
COPY . .

RUN useradd --create-home appuser
USER appuser

EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app.main:app"]

빌드 결과 비교 요약

언어싱글 스테이지멀티스테이지절감률
Node.js1.2GB120MB90%
Go800MB15MB98%
Java400MB80MB80%
Python900MB150MB83%

5. Docker Compose 실전

docker-compose.yml 기본 구조

Docker Compose는 여러 컨테이너를 YAML 파일 하나로 정의하고 관리합니다.

# docker-compose.yml
version: '3.9'

services:
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    ports:
      - '3000:3000'
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    healthcheck:
      test: ['CMD', 'wget', '--spider', 'http://localhost:3000/health']
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    restart: unless-stopped
    networks:
      - app-network

  db:
    image: postgres:16-alpine
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./init-scripts:/docker-entrypoint-initdb.d
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
    ports:
      - '5432:5432'
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres']
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --maxmemory 256mb
    volumes:
      - redis-data:/data
    ports:
      - '6379:6379'
    networks:
      - app-network

  nginx:
    image: nginx:1.25-alpine
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - api
    networks:
      - app-network

volumes:
  postgres-data:
    driver: local
  redis-data:
    driver: local

networks:
  app-network:
    driver: bridge

환경 변수와 Secrets 관리

# .env 파일 사용
services:
  api:
    env_file:
      - .env
      - .env.production

# Docker Secrets (Swarm 모드)
services:
  api:
    secrets:
      - db_password
      - api_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    external: true

Compose 주요 명령어

# 서비스 시작 (백그라운드)
docker compose up -d

# 서비스 로그 확인
docker compose logs -f api

# 서비스 스케일링
docker compose up -d --scale api=3

# 서비스 재빌드 + 재시작
docker compose up -d --build

# 서비스 중지 및 리소스 정리
docker compose down

# 볼륨까지 모두 삭제
docker compose down -v

# 특정 서비스만 재시작
docker compose restart api

# 실행 중인 서비스 상태 확인
docker compose ps

개발 환경과 프로덕션 환경 분리

# docker-compose.yml (공통)
services:
  api:
    build: ./api

# docker-compose.override.yml (개발 환경 - 자동 로드)
services:
  api:
    volumes:
      - ./api:/app    # 핫 리로드용 마운트
    environment:
      - DEBUG=true
    command: npm run dev

# docker-compose.prod.yml (프로덕션)
services:
  api:
    environment:
      - NODE_ENV=production
    deploy:
      replicas: 3
      resources:
        limits:
          memory: 512M
          cpus: "0.5"
# 개발 환경 (자동으로 override 파일 로드)
docker compose up

# 프로덕션 환경
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

6. 네트워킹 심화

Docker 네트워크 드라이버

드라이버설명사용 사례
bridge기본 드라이버, 동일 호스트 내 컨테이너 통신단일 호스트 개발 환경
host호스트 네트워크 스택 직접 사용성능이 중요한 경우
overlay여러 Docker 호스트 간 통신Swarm, 멀티 호스트
macvlan컨테이너에 실제 MAC 주소 할당레거시 네트워크 통합
none네트워크 비활성화보안 격리

컨테이너 DNS 해석

같은 Docker 네트워크에 있는 컨테이너들은 서비스 이름으로 서로를 찾을 수 있습니다.

# 커스텀 네트워크 생성
docker network create my-app

# 같은 네트워크에서 컨테이너 실행
docker run -d --name api --network my-app my-api-image
docker run -d --name db --network my-app postgres

# api 컨테이너 내부에서 db 컨테이너에 접근
# 호스트명 "db"가 자동으로 해석됨
curl http://db:5432

포트 매핑 전략

# 특정 포트 매핑
docker run -p 8080:80 nginx

# 특정 인터페이스에만 바인딩
docker run -p 127.0.0.1:8080:80 nginx

# 랜덤 호스트 포트 할당
docker run -p 80 nginx

# 여러 포트 매핑
docker run -p 80:80 -p 443:443 nginx

서비스 디스커버리 패턴

# Docker Compose에서 자동 서비스 디스커버리
services:
  api-gateway:
    image: nginx
    depends_on:
      - user-service
      - order-service

  user-service:
    image: my-user-service
    # api-gateway에서 http://user-service:3000 으로 접근 가능

  order-service:
    image: my-order-service
    # api-gateway에서 http://order-service:3000 으로 접근 가능

7. 보안 베스트 프랙티스

1) Non-root 사용자로 실행

# Node.js - 내장 node 사용자 활용
FROM node:20-alpine
WORKDIR /app
COPY --chown=node:node . .
RUN npm ci --only=production
USER node
CMD ["node", "server.js"]

# 커스텀 사용자 생성
FROM python:3.12-slim
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
CMD ["python", "app.py"]

2) 읽기 전용 파일시스템

docker run --read-only --tmpfs /tmp --tmpfs /var/run my-app
# Docker Compose에서
services:
  api:
    read_only: true
    tmpfs:
      - /tmp
      - /var/run

3) 이미지 보안 스캐닝

# Trivy로 취약점 스캔
trivy image my-app:latest

# 심각도 기준 필터링
trivy image --severity HIGH,CRITICAL my-app:latest

# Docker Scout (Docker Desktop 내장)
docker scout cves my-app:latest
docker scout recommendations my-app:latest

# Snyk
snyk container test my-app:latest

4) Secrets 관리 (절대 Dockerfile에 넣지 말 것)

# 절대 하지 마세요
ENV API_KEY=sk-1234567890
COPY .env /app/.env

# 올바른 방법: 빌드 시 시크릿 마운트 (BuildKit)
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
# 빌드 시 시크릿 전달
DOCKER_BUILDKIT=1 docker build --secret id=npmrc,src=.npmrc -t my-app .

5) Rootless Docker

# Rootless 모드로 Docker 설치
dockerd-rootless-setuptool.sh install

# 확인
docker info | grep -i rootless
# Security Options: rootless

6) Content Trust와 이미지 서명

# Docker Content Trust 활성화
export DOCKER_CONTENT_TRUST=1

# 서명된 이미지만 Pull 가능
docker pull my-registry/my-app:latest  # 서명 없으면 실패

# Cosign으로 이미지 서명 (Sigstore)
cosign sign my-registry/my-app:latest
cosign verify my-registry/my-app:latest

7) 보안 체크리스트

[보안 체크리스트]
- Non-root 사용자로 실행 (USER 지시어)
- 최소 베이스 이미지 사용 (Alpine, distroless, scratch)
- 이미지 취약점 정기 스캔 (Trivy, Snyk, Scout)
- Dockerfile에 시크릿/자격 증명 절대 포함 금지
- 읽기 전용 파일시스템 사용
- 네트워크 노출 최소화
- 리소스 제한 설정 (메모리, CPU)
- Docker Content Trust 활성화
- 정기적으로 베이스 이미지 업데이트
- 불필요한 패키지/도구 제거

8. CI/CD 파이프라인

GitHub Actions + Docker 빌드/푸시

# .github/workflows/docker-build.yml
name: Docker Build and Push

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: my-org/my-app

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: my-username
          password: my-token

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/my-org/my-app
          tags: |
            type=ref,event=branch
            type=sha,prefix=
            type=semver,pattern=v{{version}}

      - name: Build and Push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: steps.meta.outputs.tags
          labels: steps.meta.outputs.labels
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64

멀티 플랫폼 빌드 (amd64 + arm64)

# Buildx 빌더 생성
docker buildx create --name multiarch --use

# 멀티 플랫폼 빌드 및 푸시
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag my-registry/my-app:latest \
  --push .

CI에서 레이어 캐싱

# GitHub Actions Cache
- name: Build with cache
  uses: docker/build-push-action@v5
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

# Registry 기반 캐시
- name: Build with registry cache
  uses: docker/build-push-action@v5
  with:
    cache-from: type=registry,ref=ghcr.io/my-org/my-app:buildcache
    cache-to: type=registry,ref=ghcr.io/my-org/my-app:buildcache,mode=max

주요 컨테이너 레지스트리 비교

레지스트리무료 티어특징
Docker Hub1개 프라이빗 리포가장 큰 퍼블릭 레지스트리
GitHub Container Registry (GHCR)무제한 퍼블릭GitHub Actions 통합
Amazon ECR500MB 프리 티어AWS 서비스 통합
Google Artifact Registry500MB 프리 티어GCP 서비스 통합
Azure Container Registry없음Azure 서비스 통합

9. 프로덕션 운영

로깅 전략

# 컨테이너 로그 드라이버 설정
docker run -d \
  --log-driver=json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  my-app

# syslog 드라이버
docker run -d \
  --log-driver=syslog \
  --log-opt syslog-address=tcp://logserver:514 \
  my-app
# Docker Compose에서 로깅 설정
services:
  api:
    logging:
      driver: json-file
      options:
        max-size: '10m'
        max-file: '3'

로깅 베스트 프랙티스: 애플리케이션은 항상 stdout/stderr로 로그를 출력하세요. 파일에 직접 쓰지 마세요. Docker가 로그 드라이버를 통해 수집합니다.

모니터링: Prometheus + cAdvisor

# docker-compose.monitoring.yml
services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - '9090:9090'

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    ports:
      - '8080:8080'

  grafana:
    image: grafana/grafana:latest
    volumes:
      - grafana-data:/var/lib/grafana
    ports:
      - '3000:3000'
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

volumes:
  grafana-data:

리소스 제한

# 메모리 제한: 512MB (초과 시 OOM Kill)
docker run -m 512m my-app

# CPU 제한: 1.5 코어
docker run --cpus="1.5" my-app

# 조합
docker run -m 512m --cpus="1.5" --memory-swap=1g my-app
# Docker Compose에서
services:
  api:
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1.5'
        reservations:
          memory: 256M
          cpus: '0.5'

Graceful Shutdown (SIGTERM 처리)

// Node.js 예시
process.on('SIGTERM', async () => {
  console.log('SIGTERM received. Graceful shutdown...')

  // 새 요청 거부
  server.close(async () => {
    // DB 연결 종료
    await db.close()
    // Redis 연결 종료
    await redis.quit()

    console.log('All connections closed. Exiting.')
    process.exit(0)
  })

  // 30초 후 강제 종료
  setTimeout(() => {
    console.error('Forced shutdown after timeout')
    process.exit(1)
  }, 30000)
})
# Dockerfile에서 올바른 ENTRYPOINT 형식
# exec 형식 사용 (SIGTERM이 직접 앱에 전달됨)
ENTRYPOINT ["node", "server.js"]

# shell 형식은 사용 금지 (SIGTERM이 sh에만 전달됨)
# ENTRYPOINT node server.js  # 이렇게 하지 마세요

Health Check 패턴

# Dockerfile에서 헬스체크
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1
# Docker Compose에서 헬스체크
services:
  api:
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:3000/health']
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  db:
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres']
      interval: 10s
      timeout: 5s
      retries: 5

10. Docker 대안: Podman, containerd, nerdctl

Podman — 루트리스, 데몬리스

Podman은 Red Hat이 개발한 Docker 호환 컨테이너 엔진입니다.

# Docker와 거의 동일한 CLI
podman run -d -p 8080:80 nginx
podman ps
podman images

# Docker와의 호환성을 위한 별칭
alias docker=podman
특징DockerPodman
아키텍처데몬 기반 (dockerd)데몬 없음 (포크 모델)
루트 필요기본적으로 필요기본 루트리스
Compose 지원docker composepodman-compose
시스템 통합독자적systemd 통합
Pod 지원XO (Kubernetes Pod 호환)

containerd — Kubernetes 기본 런타임

containerd는 Docker에서 분리된 컨테이너 런타임으로 Kubernetes의 기본 런타임입니다.

# ctr - containerd 네이티브 CLI (저수준)
ctr images pull docker.io/library/nginx:latest
ctr run docker.io/library/nginx:latest my-nginx

nerdctl — containerd용 Docker 호환 CLI

# Docker와 동일한 UX
nerdctl run -d -p 8080:80 nginx
nerdctl compose up -d
nerdctl build -t my-app .

# 추가 기능: 이미지 암호화
nerdctl image encrypt --recipient jwe:public-key.pem my-app encrypted-app

도구 선택 가이드

시나리오추천 도구
로컬 개발Docker Desktop 또는 Podman
CI/CD 파이프라인Docker (BuildKit)
Kubernetes 런타임containerd
보안 중시 환경Podman (루트리스)
Docker 호환 + containerdnerdctl

11. Docker + AI 워크로드

NVIDIA Container Toolkit

AI/ML 워크로드에서 GPU를 컨테이너에 전달하려면 NVIDIA Container Toolkit이 필요합니다.

# NVIDIA Container Toolkit 설치
distribution=ubuntu22.04
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \
  sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg

# 패키지 설치
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit

# Docker 런타임 설정
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

GPU 패스스루

# 모든 GPU 사용
docker run --gpus all nvidia/cuda:12.3-base nvidia-smi

# 특정 GPU만 사용
docker run --gpus '"device=0,1"' my-ml-app

# Docker Compose에서
services:
  ml-training:
    image: my-ml-app
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 2
              capabilities: [gpu]

ML 모델 서빙 컨테이너

# PyTorch 모델 서빙 Dockerfile
FROM pytorch/pytorch:2.2.0-cuda12.1-cudnn8-runtime

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY model/ ./model/
COPY serve.py .

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:8000/health || exit 1

CMD ["python", "serve.py"]
# TensorFlow Serving
services:
  tf-serving:
    image: tensorflow/serving:latest-gpu
    ports:
      - '8501:8501'
    volumes:
      - ./models:/models
    environment:
      - MODEL_NAME=my_model
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]

  triton-server:
    image: nvcr.io/nvidia/tritonserver:24.01-py3
    ports:
      - '8000:8000'
      - '8001:8001'
      - '8002:8002'
    volumes:
      - ./model-repository:/models
    command: tritonserver --model-repository=/models
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]

주요 ML 프레임워크 컨테이너

프레임워크공식 이미지GPU 지원
PyTorchpytorch/pytorchO
TensorFlowtensorflow/tensorflowO
NVIDIA Tritonnvcr.io/nvidia/tritonserverO
Hugging Face TGIghcr.io/huggingface/text-generation-inferenceO
vLLMvllm/vllm-openaiO

퀴즈

Docker와 컨테이너에 대한 이해를 확인해 보세요.

Q1: 컨테이너가 VM보다 가벼운 근본적인 이유는?

정답: 컨테이너는 호스트 OS의 커널을 공유하기 때문입니다. VM은 각각 독립된 게스트 OS를 가지고 있어 수 GB의 오버헤드가 있지만, 컨테이너는 Linux namespaces와 cgroups를 사용하여 프로세스 수준에서 격리하므로 MB 단위의 크기와 밀리초 단위의 시작 시간을 가집니다.

Q2: Dockerfile에서 COPY와 ADD의 차이, 그리고 왜 COPY를 권장하는지 설명하세요.

정답: COPY는 로컬 파일/디렉토리를 이미지로 복사하는 단순한 기능만 제공합니다. ADD는 추가로 URL 다운로드와 tar 자동 압축 해제 기능이 있습니다. COPY를 권장하는 이유는 (1) 동작이 명시적이고 예측 가능하며, (2) ADD의 URL 다운로드는 캐시 무효화를 일으킬 수 있고, (3) 의도치 않은 tar 해제를 방지하기 위해서입니다. tar 자동 해제가 필요한 경우에만 ADD를 사용하세요.

Q3: 멀티스테이지 빌드에서 Go 이미지를 800MB에서 15MB로 줄일 수 있는 이유는?

정답: Go는 정적 바이너리(static binary)를 컴파일할 수 있기 때문입니다. CGO_ENABLED=0으로 빌드하면 외부 C 라이브러리 의존성이 없는 독립 실행 파일이 만들어지며, 이를 scratch(빈 이미지) 위에 복사하면 바이너리 크기 그대로가 최종 이미지 크기가 됩니다. 빌드 도구(컴파일러, 패키지 매니저 등)가 모두 제거되므로 크기가 극적으로 줄어듭니다.

Q4: Dockerfile의 레이어 순서가 빌드 캐시에 중요한 이유를 예시로 설명하세요.

정답: Docker는 각 레이어를 캐시하고, 한 레이어가 변경되면 그 이후의 모든 레이어를 재빌드합니다. 예를 들어 Node.js 앱에서 COPY . . 후에 RUN npm install을 하면, 소스 코드가 한 글자만 바뀌어도 npm install이 재실행됩니다. 대신 package.json만 먼저 COPY하고 npm install을 실행한 뒤 소스를 복사하면, package.json이 변경되지 않는 한 의존성 설치 레이어가 캐시에서 재사용되어 빌드 시간이 크게 단축됩니다.

Q5: Docker 보안에서 non-root 사용자로 실행해야 하는 이유와 방법은?

정답: 컨테이너가 root로 실행되면, 컨테이너 탈출(container escape) 취약점 발생 시 공격자가 호스트 시스템의 root 권한을 획득할 수 있습니다. Dockerfile에서 USER 지시어를 사용하여 non-root 사용자를 지정해야 합니다. Node.js 이미지는 내장 node 사용자가 있고, 다른 이미지는 RUN groupadd/useradd로 사용자를 생성한 뒤 USER 지시어로 전환합니다. 파일 소유권은 COPY --chown 옵션으로 설정합니다.


참고 자료

  1. Docker 공식 문서 — 가장 신뢰할 수 있는 레퍼런스
  2. Dockerfile Best Practices — 공식 최적화 가이드
  3. Docker Compose 스펙 — Compose 파일 레퍼런스
  4. OCI 스펙 — 컨테이너 표준 사양
  5. NVIDIA Container Toolkit — GPU 컨테이너 가이드
  6. Trivy 보안 스캐너 — 컨테이너 취약점 스캐닝
  7. Podman 공식 문서 — Docker 대안 컨테이너 엔진
  8. containerd 공식 문서 — 산업 표준 컨테이너 런타임
  9. Docker BuildKit 가이드 — 고급 빌드 기능
  10. Sigstore/Cosign — 컨테이너 이미지 서명
  11. cAdvisor — 컨테이너 리소스 모니터링
  12. Distroless 이미지 — 최소 컨테이너 이미지
  13. Docker Security Cheat Sheet — OWASP 보안 체크리스트
  14. Multi-platform Docker Builds — 크로스 플랫폼 빌드 가이드
  15. Docker + AI/ML 가이드 — Docker GenAI 스택
  16. Hugging Face TGI — LLM 모델 서빙
  17. vLLM 프로젝트 — 고성능 LLM 추론 엔진

Docker Complete Guide 2025: From Container Basics to Multi-stage Builds, Compose, and Production Deployment

Introduction

Container technology has become the backbone of modern software development. Docker is what eliminated the legendary excuse, "But it works on my machine!" Since its introduction in 2013, Docker has completely transformed the paradigm of development, testing, and deployment. As of 2025, containers serve as the fundamental unit for Kubernetes, serverless, and even AI workloads.

This guide systematically covers everything from the fundamental principles of containers, Dockerfile optimization, multi-stage builds, Docker Compose in practice, security, CI/CD pipelines, production operations, Docker alternatives, and AI workloads.


1. What Is a Container — Differences from VMs

Definition of a Container

A container packages an application and its dependencies together to run in an isolated environment. Unlike virtual machines (VMs), containers share the OS kernel, making them much lighter and faster.

Three Core Linux Technologies

Containers are a combination of three Linux kernel features:

1) Namespaces — Isolation

They limit the scope of system resources visible to a process.

NamespaceWhat It Isolates
PIDProcess IDs
NETNetwork interfaces, routing
MNTFilesystem mount points
UTSHostname, domain name
IPCInter-process communication
USERUser and group IDs
CGROUPCgroup root directory

2) Cgroups — Resource Limiting

They limit resource usage including CPU, memory, disk I/O, and network bandwidth.

# Example: limiting memory to 256MB with cgroup
sudo cgcreate -g memory:mycontainer
echo 268435456 | sudo tee /sys/fs/cgroup/memory/mycontainer/memory.limit_in_bytes

3) Union Filesystem — Layered Structure

A technology that merges multiple filesystem layers into a single view. Docker uses OverlayFS.

  • Read-Only Layers: Base image, package installations, etc.
  • Read-Write Layer: Changes made during container runtime

Container vs VM Comparison

┌─────────────────────────────────────────────────────────────┐
Container Architecture├──────────┬──────────┬──────────┬──────────┐                 │
App AApp BApp CApp D   │                 │
├──────────┼──────────┼──────────┼──────────┤                 │
Bins/Bins/Bins/Bins/   │                 │
LibsLibsLibsLibs    │                 │
├──────────┴──────────┴──────────┴──────────┤                 │
Container Runtime                │                 │
├────────────────────────────────────────────┤                 │
Host OS Kernel                │                 │
├────────────────────────────────────────────┤                 │
Infrastructure                │                 │
└────────────────────────────────────────────┘                 │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
VM Architecture├──────────┬──────────┬──────────┬──────────┐                 │
App AApp BApp CApp D   │                 │
├──────────┼──────────┼──────────┼──────────┤                 │
Bins/Bins/Bins/Bins/   │                 │
LibsLibsLibsLibs    │                 │
├──────────┼──────────┼──────────┼──────────┤                 │
Guest OSGuest OSGuest OSGuest OS │                 │
├──────────┴──────────┴──────────┴──────────┤                 │
Hypervisor                    │                 │
├────────────────────────────────────────────┤                 │
Host OS                       │                 │
├────────────────────────────────────────────┤                 │
Infrastructure                │                 │
└────────────────────────────────────────────┘                 │
└─────────────────────────────────────────────────────────────┘
AspectContainerVM
Start TimeMillisecondsMinutes
Image SizeMB rangeGB range
Performance OverheadNearly none10-20%
Isolation LevelProcess levelHardware level
OS SupportShared host kernelIndependent OS

OCI Standard

The OCI (Open Container Initiative) defines open standards for container images and runtimes.

  • Runtime Specification: Standardizes how containers are run
  • Image Specification: Standardizes container image format
  • Distribution Specification: Standardizes image distribution methods

Docker, Podman, and containerd all follow OCI standards, ensuring image compatibility.


2. Docker Architecture

Client-Server Model

Docker uses a client-server architecture.

┌─────────────┐     REST API     ┌─────────────────────┐
Docker     │ ──────────────>Docker DaemonCLI   (dockerd)│              │                 │                      │
│  docker run  │                 │  ┌─────────────────┐ │
│  docker build│                 │  │   containerd    │ │
│  docker pull │                 │  │                 │ │
└─────────────┘                 │  │  ┌───────────┐  │ │
                                │  │  │   runc    │  │ │
                                │  │  └───────────┘  │ │
                                │  └─────────────────┘ │
                                └─────────────────────┘
  • Docker CLI: The client where users enter commands
  • Docker Daemon (dockerd): The server process managing container lifecycle
  • containerd: An OCI-compliant container runtime manager
  • runc: The low-level runtime that actually creates and runs containers

Four Core Concepts

1) Image

A read-only template serving as the blueprint for creating containers. It has a layered structure for efficient storage and transfer.

# Image management commands
docker images                    # List local images
docker pull nginx:1.25-alpine    # Download an image
docker image inspect nginx       # Image details
docker image prune               # Clean unused images

2) Container

A running instance of an image. A writable layer is added on top of the image.

# Running and managing containers
docker run -d --name my-nginx -p 8080:80 nginx:1.25-alpine
docker ps                        # List running containers
docker logs my-nginx             # View logs
docker exec -it my-nginx /bin/sh # Access container shell
docker stop my-nginx             # Stop
docker rm my-nginx               # Remove

3) Volume

A mechanism for persisting container data.

# Creating and using volumes
docker volume create my-data
docker run -d -v my-data:/app/data my-app
docker run -d -v /host/path:/container/path my-app  # Bind mount

4) Network

Manages communication between containers.

# Creating and managing networks
docker network create my-network
docker run -d --network my-network --name api my-api
docker run -d --network my-network --name db postgres
# The api container can access db at db:5432

3. Dockerfile Master Class

Key Instructions Explained

# Select base image
FROM node:20-alpine

# Add metadata
LABEL maintainer="dev@example.com"
LABEL version="1.0"

# Build-time variable (only available during build)
ARG NODE_ENV=production

# Environment variable (available at runtime too)
ENV NODE_ENV=$NODE_ENV
ENV PORT=3000

# Set working directory
WORKDIR /app

# Copy files (COPY is recommended)
COPY package*.json ./

# Execute command
RUN npm ci --only=production

# Copy source code
COPY . .

# Document port (does not actually open the port)
EXPOSE 3000

# Container health check
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

# Switch to non-root user (security)
USER node

# Command to run when container starts
ENTRYPOINT ["node"]
CMD ["server.js"]

COPY vs ADD Differences

FeatureCOPYADD
Copy local filesYesYes
URL downloadNoYes
Auto-extract tarNoYes
RecommendedYesOnly special cases

Conclusion: Use COPY in most cases. Use ADD only when automatic tar extraction is needed.

CMD vs ENTRYPOINT Differences

# CMD only - can be fully overridden at docker run
CMD ["python", "app.py"]
# docker run my-image python test.py  (CMD replaced with test.py)

# ENTRYPOINT only - always executes
ENTRYPOINT ["python"]
# docker run my-image app.py  (runs python app.py)

# ENTRYPOINT + CMD combination (most flexible)
ENTRYPOINT ["python"]
CMD ["app.py"]
# docker run my-image         -> python app.py
# docker run my-image test.py -> python test.py

ARG vs ENV

# ARG: exists only during build (not at runtime)
ARG BUILD_VERSION=1.0
RUN echo "Building version: $BUILD_VERSION"

# ENV: exists during build + runtime
ENV APP_VERSION=1.0
# APP_VERSION is available when container runs

Dockerfile Optimization Best Practices

1) Layer Order Determines Cache Behavior

# Bad: npm install re-runs on every source change
COPY . .
RUN npm install

# Good: npm install only re-runs when package.json changes
COPY package*.json ./
RUN npm ci
COPY . .

2) Combine RUN Instructions

# Bad: Creates 3 layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# Good: Creates 1 layer + cleans cache
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

3) Use .dockerignore

# .dockerignore
node_modules
.git
.env
*.md
Dockerfile
docker-compose*.yml
.github
coverage
dist

4) Choose Small Base Images

ImageSizeUse Case
ubuntu:22.04~77MBWhen full OS needed
node:20-slim~200MBMost Node.js apps
node:20-alpine~130MBLightweight Node.js
alpine:3.19~7MBMinimal base
distroless~2MBMinimal runtime only
scratch0MBStatic binaries only

4. Multi-stage Builds — Reduce Image Size by 90%

Multi-stage builds separate build tools from the final runtime environment, dramatically reducing image size.

Node.js Example: 1.2GB to 120MB

# Stage 1: Build environment
FROM node:20 AS builder
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# Stage 2: Production environment
FROM node:20-alpine AS production
WORKDIR /app

# Install production dependencies only
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# Copy only build artifacts
COPY --from=builder /app/dist ./dist

# Security: non-root user
USER node

EXPOSE 3000
CMD ["node", "dist/server.js"]

Result: Instead of the full node:20 image (~1.1GB), using Alpine-based (~130MB) + excluding build tools = final 120MB

Go Example: 800MB to 15MB

# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server .

# Stage 2: Minimal image
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server

EXPOSE 8080
ENTRYPOINT ["/server"]

Result: Extract only the static binary from Go build environment (~800MB) for under 15MB

Java/Spring Boot Example: 400MB to 80MB

# Stage 1: Build
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app

COPY gradlew build.gradle settings.gradle ./
COPY gradle ./gradle
RUN ./gradlew dependencies --no-daemon

COPY src ./src
RUN ./gradlew bootJar --no-daemon

# Stage 2: Runtime
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app

COPY --from=builder /app/build/libs/*.jar app.jar

RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Python Example

# Stage 1: Dependency build
FROM python:3.12-slim AS builder
WORKDIR /app

RUN pip install --no-cache-dir poetry
COPY pyproject.toml poetry.lock ./
RUN poetry export -f requirements.txt -o requirements.txt --without-hashes
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# Stage 2: Runtime
FROM python:3.12-slim
WORKDIR /app

COPY --from=builder /install /usr/local
COPY . .

RUN useradd --create-home appuser
USER appuser

EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app.main:app"]

Build Result Summary

LanguageSingle StageMulti-stageReduction
Node.js1.2GB120MB90%
Go800MB15MB98%
Java400MB80MB80%
Python900MB150MB83%

5. Docker Compose in Practice

docker-compose.yml Basic Structure

Docker Compose defines and manages multiple containers in a single YAML file.

# docker-compose.yml
version: '3.9'

services:
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    ports:
      - '3000:3000'
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    healthcheck:
      test: ['CMD', 'wget', '--spider', 'http://localhost:3000/health']
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    restart: unless-stopped
    networks:
      - app-network

  db:
    image: postgres:16-alpine
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./init-scripts:/docker-entrypoint-initdb.d
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
    ports:
      - '5432:5432'
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres']
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes --maxmemory 256mb
    volumes:
      - redis-data:/data
    ports:
      - '6379:6379'
    networks:
      - app-network

  nginx:
    image: nginx:1.25-alpine
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - api
    networks:
      - app-network

volumes:
  postgres-data:
    driver: local
  redis-data:
    driver: local

networks:
  app-network:
    driver: bridge

Environment Variables and Secrets Management

# Using .env files
services:
  api:
    env_file:
      - .env
      - .env.production

# Docker Secrets (Swarm mode)
services:
  api:
    secrets:
      - db_password
      - api_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    external: true

Essential Compose Commands

# Start services (background)
docker compose up -d

# View service logs
docker compose logs -f api

# Scale services
docker compose up -d --scale api=3

# Rebuild + restart services
docker compose up -d --build

# Stop and clean up resources
docker compose down

# Delete everything including volumes
docker compose down -v

# Restart specific service
docker compose restart api

# Check running service status
docker compose ps

Separating Development and Production Environments

# docker-compose.yml (shared)
services:
  api:
    build: ./api

# docker-compose.override.yml (development - auto-loaded)
services:
  api:
    volumes:
      - ./api:/app    # Mount for hot reload
    environment:
      - DEBUG=true
    command: npm run dev

# docker-compose.prod.yml (production)
services:
  api:
    environment:
      - NODE_ENV=production
    deploy:
      replicas: 3
      resources:
        limits:
          memory: 512M
          cpus: "0.5"
# Development (auto-loads override file)
docker compose up

# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

6. Networking Deep Dive

Docker Network Drivers

DriverDescriptionUse Case
bridgeDefault driver, intra-host container communicationSingle-host development
hostUses host network stack directlyPerformance-critical scenarios
overlayMulti-host communicationSwarm, multi-host setups
macvlanAssigns real MAC address to containersLegacy network integration
noneDisables networkingSecurity isolation

Container DNS Resolution

Containers on the same Docker network can discover each other by service name.

# Create custom network
docker network create my-app

# Run containers on the same network
docker run -d --name api --network my-app my-api-image
docker run -d --name db --network my-app postgres

# From inside the api container, access db container
# Hostname "db" is automatically resolved
curl http://db:5432

Port Mapping Strategies

# Specific port mapping
docker run -p 8080:80 nginx

# Bind to specific interface only
docker run -p 127.0.0.1:8080:80 nginx

# Random host port assignment
docker run -p 80 nginx

# Multiple port mappings
docker run -p 80:80 -p 443:443 nginx

Service Discovery Patterns

# Automatic service discovery in Docker Compose
services:
  api-gateway:
    image: nginx
    depends_on:
      - user-service
      - order-service

  user-service:
    image: my-user-service
    # Accessible from api-gateway at http://user-service:3000

  order-service:
    image: my-order-service
    # Accessible from api-gateway at http://order-service:3000

7. Security Best Practices

1) Run as Non-root User

# Node.js - use built-in node user
FROM node:20-alpine
WORKDIR /app
COPY --chown=node:node . .
RUN npm ci --only=production
USER node
CMD ["node", "server.js"]

# Custom user creation
FROM python:3.12-slim
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
CMD ["python", "app.py"]

2) Read-only Filesystem

docker run --read-only --tmpfs /tmp --tmpfs /var/run my-app
# In Docker Compose
services:
  api:
    read_only: true
    tmpfs:
      - /tmp
      - /var/run

3) Image Security Scanning

# Scan vulnerabilities with Trivy
trivy image my-app:latest

# Filter by severity
trivy image --severity HIGH,CRITICAL my-app:latest

# Docker Scout (built into Docker Desktop)
docker scout cves my-app:latest
docker scout recommendations my-app:latest

# Snyk
snyk container test my-app:latest

4) Secrets Management (Never Put Them in Dockerfile!)

# NEVER do this
ENV API_KEY=sk-1234567890
COPY .env /app/.env

# Correct approach: mount secrets during build (BuildKit)
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
# Pass secrets during build
DOCKER_BUILDKIT=1 docker build --secret id=npmrc,src=.npmrc -t my-app .

5) Rootless Docker

# Install Docker in rootless mode
dockerd-rootless-setuptool.sh install

# Verify
docker info | grep -i rootless
# Security Options: rootless

6) Content Trust and Image Signing

# Enable Docker Content Trust
export DOCKER_CONTENT_TRUST=1

# Only signed images can be pulled
docker pull my-registry/my-app:latest  # Fails without signature

# Sign images with Cosign (Sigstore)
cosign sign my-registry/my-app:latest
cosign verify my-registry/my-app:latest

7) Security Checklist

[Security Checklist]
- Run as non-root user (USER directive)
- Use minimal base images (Alpine, distroless, scratch)
- Regularly scan images for vulnerabilities (Trivy, Snyk, Scout)
- NEVER include secrets/credentials in Dockerfile
- Use read-only filesystem
- Minimize network exposure
- Set resource limits (memory, CPU)
- Enable Docker Content Trust
- Regularly update base images
- Remove unnecessary packages/tools

8. CI/CD Pipeline

GitHub Actions + Docker Build/Push

# .github/workflows/docker-build.yml
name: Docker Build and Push

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: my-org/my-app

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: my-username
          password: my-token

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/my-org/my-app
          tags: |
            type=ref,event=branch
            type=sha,prefix=
            type=semver,pattern=v{{version}}

      - name: Build and Push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: steps.meta.outputs.tags
          labels: steps.meta.outputs.labels
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64

Multi-platform Builds (amd64 + arm64)

# Create Buildx builder
docker buildx create --name multiarch --use

# Build and push for multiple platforms
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag my-registry/my-app:latest \
  --push .

Layer Caching in CI

# GitHub Actions Cache
- name: Build with cache
  uses: docker/build-push-action@v5
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

# Registry-based cache
- name: Build with registry cache
  uses: docker/build-push-action@v5
  with:
    cache-from: type=registry,ref=ghcr.io/my-org/my-app:buildcache
    cache-to: type=registry,ref=ghcr.io/my-org/my-app:buildcache,mode=max

Container Registry Comparison

RegistryFree TierFeatures
Docker Hub1 private repoLargest public registry
GitHub Container Registry (GHCR)Unlimited publicGitHub Actions integration
Amazon ECR500MB free tierAWS service integration
Google Artifact Registry500MB free tierGCP service integration
Azure Container RegistryNoneAzure service integration

9. Production Operations

Logging Strategies

# Set container log driver
docker run -d \
  --log-driver=json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  my-app

# Syslog driver
docker run -d \
  --log-driver=syslog \
  --log-opt syslog-address=tcp://logserver:514 \
  my-app
# Logging in Docker Compose
services:
  api:
    logging:
      driver: json-file
      options:
        max-size: '10m'
        max-file: '3'

Logging Best Practice: Always output logs to stdout/stderr. Never write directly to files. Docker collects logs through the log driver.

Monitoring: Prometheus + cAdvisor

# docker-compose.monitoring.yml
services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - '9090:9090'

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    ports:
      - '8080:8080'

  grafana:
    image: grafana/grafana:latest
    volumes:
      - grafana-data:/var/lib/grafana
    ports:
      - '3000:3000'
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

volumes:
  grafana-data:

Resource Limits

# Memory limit: 512MB (OOM Kill on exceed)
docker run -m 512m my-app

# CPU limit: 1.5 cores
docker run --cpus="1.5" my-app

# Combined
docker run -m 512m --cpus="1.5" --memory-swap=1g my-app
# In Docker Compose
services:
  api:
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1.5'
        reservations:
          memory: 256M
          cpus: '0.5'

Graceful Shutdown (SIGTERM Handling)

// Node.js example
process.on('SIGTERM', async () => {
  console.log('SIGTERM received. Graceful shutdown...')

  // Reject new requests
  server.close(async () => {
    // Close DB connections
    await db.close()
    // Close Redis connections
    await redis.quit()

    console.log('All connections closed. Exiting.')
    process.exit(0)
  })

  // Force exit after 30 seconds
  setTimeout(() => {
    console.error('Forced shutdown after timeout')
    process.exit(1)
  }, 30000)
})
# Correct ENTRYPOINT format in Dockerfile
# Use exec form (SIGTERM delivered directly to app)
ENTRYPOINT ["node", "server.js"]

# Do NOT use shell form (SIGTERM only delivered to sh)
# ENTRYPOINT node server.js  # Don't do this

Health Check Patterns

# Health check in Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1
# Health check in Docker Compose
services:
  api:
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:3000/health']
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  db:
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres']
      interval: 10s
      timeout: 5s
      retries: 5

10. Docker Alternatives: Podman, containerd, nerdctl

Podman — Rootless, Daemonless

Podman is a Docker-compatible container engine developed by Red Hat.

# Nearly identical CLI to Docker
podman run -d -p 8080:80 nginx
podman ps
podman images

# Alias for Docker compatibility
alias docker=podman
FeatureDockerPodman
ArchitectureDaemon-based (dockerd)Daemonless (fork model)
Root RequiredRequired by defaultRootless by default
Compose Supportdocker composepodman-compose
System IntegrationProprietarysystemd integration
Pod SupportNoYes (Kubernetes Pod compatible)

containerd — Kubernetes Default Runtime

containerd is a container runtime extracted from Docker and is the default runtime for Kubernetes.

# ctr - containerd native CLI (low-level)
ctr images pull docker.io/library/nginx:latest
ctr run docker.io/library/nginx:latest my-nginx

nerdctl — Docker-compatible CLI for containerd

# Same UX as Docker
nerdctl run -d -p 8080:80 nginx
nerdctl compose up -d
nerdctl build -t my-app .

# Extra feature: image encryption
nerdctl image encrypt --recipient jwe:public-key.pem my-app encrypted-app

Tool Selection Guide

ScenarioRecommended Tool
Local DevelopmentDocker Desktop or Podman
CI/CD PipelinesDocker (BuildKit)
Kubernetes Runtimecontainerd
Security-focused EnvironmentsPodman (rootless)
Docker-compatible + containerdnerdctl

11. Docker + AI Workloads

NVIDIA Container Toolkit

For AI/ML workloads, the NVIDIA Container Toolkit is needed to pass GPUs to containers.

# Install NVIDIA Container Toolkit
distribution=ubuntu22.04
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \
  sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg

# Install packages
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit

# Configure Docker runtime
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

GPU Passthrough

# Use all GPUs
docker run --gpus all nvidia/cuda:12.3-base nvidia-smi

# Use specific GPUs only
docker run --gpus '"device=0,1"' my-ml-app

# In Docker Compose
services:
  ml-training:
    image: my-ml-app
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 2
              capabilities: [gpu]

ML Model Serving Containers

# PyTorch model serving Dockerfile
FROM pytorch/pytorch:2.2.0-cuda12.1-cudnn8-runtime

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY model/ ./model/
COPY serve.py .

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:8000/health || exit 1

CMD ["python", "serve.py"]
# TensorFlow Serving
services:
  tf-serving:
    image: tensorflow/serving:latest-gpu
    ports:
      - '8501:8501'
    volumes:
      - ./models:/models
    environment:
      - MODEL_NAME=my_model
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]

  triton-server:
    image: nvcr.io/nvidia/tritonserver:24.01-py3
    ports:
      - '8000:8000'
      - '8001:8001'
      - '8002:8002'
    volumes:
      - ./model-repository:/models
    command: tritonserver --model-repository=/models
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]

Key ML Framework Containers

FrameworkOfficial ImageGPU Support
PyTorchpytorch/pytorchYes
TensorFlowtensorflow/tensorflowYes
NVIDIA Tritonnvcr.io/nvidia/tritonserverYes
Hugging Face TGIghcr.io/huggingface/text-generation-inferenceYes
vLLMvllm/vllm-openaiYes

Quiz

Test your understanding of Docker and containers.

Q1: What is the fundamental reason containers are lighter than VMs?

Answer: Containers share the host OS kernel. VMs have independent guest operating systems, adding gigabytes of overhead. Containers use Linux namespaces and cgroups to isolate at the process level, resulting in MB-sized images and millisecond startup times.

Q2: Explain the difference between COPY and ADD in a Dockerfile, and why COPY is recommended.

Answer: COPY provides simple functionality for copying local files/directories to the image. ADD additionally supports URL downloads and automatic tar extraction. COPY is recommended because (1) its behavior is explicit and predictable, (2) ADD's URL download can cause cache invalidation, and (3) it prevents unintended tar extraction. Use ADD only when automatic tar extraction is needed.

Q3: Why can Go images be reduced from 800MB to 15MB with multi-stage builds?

Answer: Go can compile static binaries. Building with CGO_ENABLED=0 produces a standalone executable with no external C library dependencies. Copying this onto scratch (empty image) makes the final image size equal to the binary size alone. All build tools (compiler, package manager, etc.) are eliminated, resulting in a dramatic size reduction.

Q4: Explain with an example why layer order in a Dockerfile matters for build cache.

Answer: Docker caches each layer, and when one layer changes, all subsequent layers are rebuilt. For example, in a Node.js app, if you run COPY . . followed by RUN npm install, even a single character change in source code triggers npm install again. Instead, COPY package.json first, run npm install, then COPY the source. As long as package.json is unchanged, the dependency installation layer is reused from cache, significantly reducing build time.

Q5: Why should containers run as non-root users, and how do you implement this?

Answer: If a container runs as root and a container escape vulnerability is exploited, the attacker gains root access to the host system. Use the USER directive in the Dockerfile to specify a non-root user. Node.js images have a built-in node user, while other images require creating users with RUN groupadd/useradd and then switching with the USER directive. File ownership is set using the COPY --chown option.


References

  1. Docker Official Documentation — The most reliable reference
  2. Dockerfile Best Practices — Official optimization guide
  3. Docker Compose Spec — Compose file reference
  4. OCI Specifications — Container standard specs
  5. NVIDIA Container Toolkit — GPU container guide
  6. Trivy Security Scanner — Container vulnerability scanning
  7. Podman Official Docs — Alternative container engine
  8. containerd Official Docs — Industry-standard container runtime
  9. Docker BuildKit Guide — Advanced build features
  10. Sigstore/Cosign — Container image signing
  11. cAdvisor — Container resource monitoring
  12. Distroless Images — Minimal container images
  13. Docker Security Cheat Sheet — OWASP security checklist
  14. Multi-platform Docker Builds — Cross-platform build guide
  15. Docker + AI/ML Guide — Docker GenAI Stack
  16. Hugging Face TGI — LLM model serving
  17. vLLM Project — High-performance LLM inference engine