← 가이드 목록으로 ← Back to guides

Docker 멀티스테이지 빌드와 최적화 완벽 가이드 Docker Multi-Stage Build & Optimization Guide

Docker 이미지가 1GB를 넘어가면 배포 시간이 길어지고 보안 취약점도 늘어납니다. 멀티스테이지 빌드를 활용하면 이미지 크기를 90% 이상 줄일 수 있습니다.

When Docker images exceed 1GB, deployment takes longer and security vulnerabilities increase. Using multi-stage builds can reduce image size by over 90%.

멀티스테이지 빌드란? What is Multi-Stage Build?

하나의 Dockerfile에서 여러 FROM을 사용하여 빌드 환경과 실행 환경을 분리합니다. 빌드에 필요한 도구들은 최종 이미지에 포함되지 않습니다.

Use multiple FROM statements in a single Dockerfile to separate build and runtime environments. Build tools are not included in the final image.

# ❌ 기존 방식: 모든 것이 포함됨
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/index.js"]
# 결과: ~1.2GB 이미지 (node_modules, 빌드 도구 모두 포함)
# ❌ Traditional approach: Everything included
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/index.js"]
# Result: ~1.2GB image (includes node_modules, build tools)
# ✅ 멀티스테이지: 빌드와 실행 분리

# Stage 1: 빌드 환경
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: 실행 환경 (최소한만 포함)
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
# 결과: ~150MB 이미지!
# ✅ Multi-stage: Separate build and runtime

# Stage 1: Build environment
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Runtime environment (minimal)
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
# Result: ~150MB image!

레이어 캐싱 최적화 Layer Caching Optimization

Dockerfile의 각 명령어는 레이어를 생성합니다. 변경되지 않은 레이어는 캐시됩니다. 자주 변경되는 내용은 아래에, 변경이 적은 내용은 위에 배치하세요.

Each Dockerfile instruction creates a layer. Unchanged layers are cached. Put frequently changing content at the bottom, rarely changing content at the top.

# ❌ 나쁜 예: COPY . . 이후 모든 레이어가 무효화됨
FROM node:18
WORKDIR /app
COPY . .
RUN npm install # 코드 변경 시마다 재실행
# ✅ 좋은 예: 의존성 먼저 설치
FROM node:18
WORKDIR /app
COPY package*.json ./ # 의존성 파일만 먼저
RUN npm ci # 캐시됨 (package.json 변경 없으면)
COPY . . # 소스 코드
RUN npm run build
# ❌ Bad: All layers invalidated after COPY . .
FROM node:18
WORKDIR /app
COPY . .
RUN npm install # Re-runs on every code change
# ✅ Good: Install dependencies first
FROM node:18
WORKDIR /app
COPY package*.json ./ # Dependency files first
RUN npm ci # Cached if package.json unchanged
COPY . . # Source code
RUN npm run build

베이스 이미지 선택 Choosing Base Images

이미지 크기 용도
node:18 ~1GB 개발 환경, 빌드 스테이지
node:18-slim ~200MB 프로덕션 (일반)
node:18-alpine ~50MB 프로덕션 (최적)
Image Size Use Case
node:18 ~1GB Development, build stage
node:18-slim ~200MB Production (general)
node:18-alpine ~50MB Production (optimal)

⚠️ Alpine 주의사항 ⚠️ Alpine Caveats

Alpine은 musl libc를 사용합니다. 일부 npm 패키지가 호환되지 않을 수 있습니다. bcrypt, sharp 같은 네이티브 바이너리 패키지 사용 시 테스트 필요.

Alpine uses musl libc. Some npm packages may not be compatible. Test when using native binary packages like bcrypt, sharp.

프로덕션 Dockerfile 모범 사례 Production Dockerfile Best Practices

# 프로덕션 최적화 Dockerfile

# 1. 빌드 스테이지
FROM node:18-alpine AS builder
WORKDIR /app

# 의존성 레이어 캐싱
COPY package*.json ./
RUN npm ci --only=production

# 소스 복사 및 빌드
COPY . .
RUN npm run build

# 2. 프로덕션 스테이지
FROM node:18-alpine
WORKDIR /app

# 보안: non-root 사용자
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# 필요한 파일만 복사
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules

# 환경 변수
ENV NODE_ENV=production
ENV PORT=3000

# 사용자 전환
USER nextjs

# 헬스체크
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget -q --spider http://localhost:3000/health || exit 1

EXPOSE 3000
CMD ["node", "dist/index.js"]
# Production-optimized Dockerfile

# 1. Build stage
FROM node:18-alpine AS builder
WORKDIR /app

# Dependency layer caching
COPY package*.json ./
RUN npm ci --only=production

# Copy source and build
COPY . .
RUN npm run build

# 2. Production stage
FROM node:18-alpine
WORKDIR /app

# Security: non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# Copy only required files
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules

# Environment variables
ENV NODE_ENV=production
ENV PORT=3000

# Switch user
USER nextjs

# Health check
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget -q --spider http://localhost:3000/health || exit 1

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

.dockerignore 활용 Using .dockerignore

# .dockerignore
node_modules
npm-debug.log
.git
.env.local
.DS_Store
Dockerfile*
docker-compose*
README.md
.github
coverage
dist

💡 .dockerignore의 중요성 💡 Importance of .dockerignore

.dockerignore가 없으면 node_modules가 빌드 컨텍스트에 포함되어 빌드 속도가 느려집니다. 또한 .git 폴더도 이미지에 들어가 크기가 불필요하게 커집니다.

Without .dockerignore, node_modules is included in the build context, slowing down builds. Also, .git folder gets into the image, unnecessarily increasing size.

이미지 크기 분석 Image Size Analysis

# 이미지 레이어 크기 확인
$ docker history my-app:latest

# 상세 분석 (dive 도구)
$ dive my-app:latest
# Check image layer sizes
$ docker history my-app:latest

# Detailed analysis (dive tool)
$ dive my-app:latest