Docker 멀티스테이지 빌드 최적화 | 이미지 크기 90% 감소 실전 가이드
이 글의 핵심
Docker 멀티스테이지 빌드로 이미지 크기 90% 감소. 빌드 도구 분리, 레이어 캐싱 최적화, distroless 이미지 활용. Node.js, Go, Rust 실전 예제.
들어가며
Docker 멀티스테이지 빌드는 빌드 환경과 런타임 환경을 분리하여 이미지 크기를 대폭 줄이는 기법입니다. 컴파일러, 빌드 도구, 개발 의존성은 빌드 단계에만 필요하고, 최종 이미지에는 실행 파일과 런타임 의존성만 포함합니다.
비유로 말씀드리면, 일반 빌드는 주방 도구와 재료를 모두 식탁에 올리는 것이고, 멀티스테이지 빌드는 주방에서 요리하고 완성된 음식만 식탁에 올리는 것입니다.
이 글을 읽으면
- 멀티스테이지 빌드의 개념과 장점을 이해합니다
- Node.js, Go, Rust 실전 예제를 확인합니다
- 레이어 캐싱 최적화 기법을 배웁니다
- 이미지 크기를 90% 줄이는 방법을 익힙니다
목차
개념 설명
일반 빌드 vs 멀티스테이지 빌드
일반 빌드 (단일 스테이지):
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/index.js"]
문제점:
node_modules전체 포함 (수백 MB)- 빌드 도구 포함 (TypeScript 컴파일러 등)
- 소스 코드 포함 (불필요)
- 이미지 크기: ~1GB
멀티스테이지 빌드:
# 빌드 스테이지
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 런타임 스테이지
FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
장점:
- 빌드 도구 제외
- 소스 코드 제외
- 이미지 크기: ~200MB (80% 감소)
멀티스테이지 빌드 구조
# 스테이지 1: 빌드
FROM <빌드 이미지> AS <스테이지 이름>
# 빌드 작업
# 스테이지 2: 런타임
FROM <런타임 이미지>
COPY --from=<스테이지 이름> <빌드 결과물> <목적지>
# 실행 명령
실전 구현
Node.js 애플리케이션
기본 멀티스테이지:
# 빌드 스테이지
FROM node:20 AS builder
WORKDIR /app
# 의존성 설치 (캐싱 최적화)
COPY package*.json ./
RUN npm ci --only=production
# 소스 복사 및 빌드
COPY . .
RUN npm run build
# 런타임 스테이지
FROM node:20-slim
WORKDIR /app
# 빌드 결과물만 복사
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
# 비root 사용자
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nodejs
USER nodejs
EXPOSE 3000
CMD ["node", "dist/index.js"]
이미지 크기:
- 일반 빌드: ~1.2GB
- 멀티스테이지: ~200MB (83% 감소)
Go 애플리케이션
최적화된 멀티스테이지:
# 빌드 스테이지
FROM golang:1.22 AS builder
WORKDIR /app
# 의존성 다운로드 (캐싱)
COPY go.mod go.sum ./
RUN go mod download
# 소스 복사 및 빌드
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# 런타임 스테이지 (scratch 사용)
FROM scratch
WORKDIR /app
# 빌드된 바이너리만 복사
COPY --from=builder /app/main .
# CA 인증서 복사 (HTTPS 통신용)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
CMD ["./main"]
이미지 크기:
- 일반 빌드: ~800MB
- 멀티스테이지: ~10MB (99% 감소)
Rust 애플리케이션
최적화된 멀티스테이지:
# 빌드 스테이지
FROM rust:1.77 AS builder
WORKDIR /app
# 의존성 캐싱 (더미 프로젝트)
RUN cargo new --bin dummy
WORKDIR /app/dummy
COPY Cargo.toml Cargo.lock ./
RUN cargo build --release
RUN rm src/*.rs
# 실제 소스 빌드
COPY src ./src
RUN touch src/main.rs
RUN cargo build --release
# 런타임 스테이지
FROM debian:bookworm-slim
WORKDIR /app
# 런타임 의존성
RUN apt-get update && \
apt-get install -y ca-certificates && \
rm -rf /var/lib/apt/lists/*
# 빌드된 바이너리 복사
COPY --from=builder /app/dummy/target/release/myapp .
# 비root 사용자
RUN useradd -m -u 1001 appuser
USER appuser
EXPOSE 8080
CMD ["./myapp"]
이미지 크기:
- 일반 빌드: ~2GB
- 멀티스테이지: ~80MB (96% 감소)
Python 애플리케이션
최적화된 멀티스테이지:
# 빌드 스테이지
FROM python:3.12 AS builder
WORKDIR /app
# 의존성 설치
COPY requirements.txt ./
RUN pip install --user --no-cache-dir -r requirements.txt
# 런타임 스테이지
FROM python:3.12-slim
WORKDIR /app
# 설치된 패키지 복사
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
# 소스 복사
COPY . .
# 비root 사용자
RUN useradd -m -u 1001 appuser && \
chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["python", "main.py"]
이미지 크기:
- 일반 빌드: ~1GB
- 멀티스테이지: ~200MB (80% 감소)
고급 최적화
레이어 캐싱 최적화
나쁜 예:
FROM node:20 AS builder
WORKDIR /app
# 소스 먼저 복사 (매번 캐시 무효화)
COPY . .
RUN npm install
RUN npm run build
좋은 예:
FROM node:20 AS builder
WORKDIR /app
# 의존성 파일만 먼저 복사
COPY package*.json ./
RUN npm ci
# 소스는 나중에 복사
COPY . .
RUN npm run build
이유:
package.json변경 시에만npm ci재실행- 소스 코드 변경 시 빌드만 재실행
- CI/CD 빌드 시간 대폭 단축
distroless 이미지
Google distroless 이미지 사용:
# 빌드 스테이지
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o main .
# 런타임 스테이지 (distroless)
FROM gcr.io/distroless/static-debian12
WORKDIR /app
COPY --from=builder /app/main .
CMD ["./main"]
장점:
- 셸, 패키지 매니저 없음 (보안 강화)
- 최소한의 파일만 포함
- 이미지 크기 최소화
Alpine Linux
Alpine 기반 이미지:
# 빌드 스테이지
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 런타임 스테이지
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
주의사항:
- Alpine은
musl libc사용 (glibc와 호환성 문제 가능) - 네이티브 모듈 빌드 시 추가 패키지 필요
BuildKit 캐시 마운트
BuildKit 활성화:
export DOCKER_BUILDKIT=1
캐시 마운트 사용:
# syntax=docker/dockerfile:1.4
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN npm run build
장점:
- npm 캐시를 빌드 간 공유
- 의존성 다운로드 시간 단축
성능 비교
이미지 크기 비교
| 언어 | 일반 빌드 | 멀티스테이지 | 감소율 |
|---|---|---|---|
| Node.js | 1.2GB | 200MB | 83% |
| Go | 800MB | 10MB | 99% |
| Rust | 2GB | 80MB | 96% |
| Python | 1GB | 200MB | 80% |
빌드 시간 비교
레이어 캐싱 미적용:
첫 빌드: 5분
소스 수정 후 재빌드: 5분 (전체 재빌드)
레이어 캐싱 적용:
첫 빌드: 5분
소스 수정 후 재빌드: 30초 (빌드 단계만)
실제 벤치마크 (Node.js 앱)
일반 빌드:
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/index.js"]
이미지 크기: 1.2GB
빌드 시간: 3분 30초
레이어 수: 12
멀티스테이지 + 최적화:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
이미지 크기: 150MB
빌드 시간: 1분 10초 (캐시 히트 시 20초)
레이어 수: 8
실무 사례
사례 1: Next.js 애플리케이션
# 의존성 설치 스테이지
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# 빌드 스테이지
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# 런타임 스테이지
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
결과:
- 일반 빌드: 1.5GB
- 멀티스테이지: 180MB (88% 감소)
사례 2: Go 마이크로서비스
# 빌드 스테이지
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="-w -s" -o main .
# 런타임 스테이지 (scratch)
FROM scratch
COPY --from=builder /app/main /main
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/main"]
결과:
- 일반 빌드: 800MB
- 멀티스테이지: 8MB (99% 감소)
사례 3: Rust 웹 서버
# 빌드 스테이지
FROM rust:1.77 AS builder
WORKDIR /app
# 의존성 캐싱
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm -rf src
# 실제 빌드
COPY src ./src
RUN touch src/main.rs
RUN cargo build --release
# 런타임 스테이지
FROM debian:bookworm-slim
WORKDIR /app
RUN apt-get update && \
apt-get install -y ca-certificates && \
rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/myapp .
RUN useradd -m -u 1001 appuser
USER appuser
EXPOSE 8080
CMD ["./myapp"]
결과:
- 일반 빌드: 2.1GB
- 멀티스테이지: 85MB (96% 감소)
트러블슈팅
COPY —from이 실패함
문제:
COPY --from=builder /app/dist ./dist
# Error: COPY failed: file not found
원인:
- 빌드 스테이지에서 파일이 생성되지 않음
- 경로가 잘못됨
해결:
# 빌드 스테이지에서 확인
FROM node:20 AS builder
WORKDIR /app
COPY . .
RUN npm run build && ls -la dist/
# 런타임 스테이지
FROM node:20-slim
COPY --from=builder /app/dist ./dist
캐시가 무효화됨
문제:
- 소스 코드 한 줄 수정해도 의존성 재설치
원인:
- COPY 순서가 잘못됨
해결:
# 나쁜 예
COPY . .
RUN npm install
# 좋은 예
COPY package*.json ./
RUN npm ci
COPY . .
이미지 크기가 여전히 큼
문제:
- 멀티스테이지인데도 이미지가 큼
원인:
- 불필요한 파일 복사
- 베이스 이미지가 큼
해결:
# .dockerignore 파일 생성
node_modules
.git
.env
*.md
tests
# slim 또는 alpine 이미지 사용
FROM node:20-slim # 또는 node:20-alpine
런타임 의존성 누락
문제:
- 애플리케이션 실행 시 라이브러리 누락 에러
원인:
- 런타임 의존성이 빌드 스테이지에만 있음
해결:
# 런타임 스테이지에 의존성 설치
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get install -y \
ca-certificates \
libssl3 \
&& rm -rf /var/lib/apt/lists/*
마무리
Docker 멀티스테이지 빌드는 이미지 크기를 대폭 줄이고 보안을 강화하는 필수 기법입니다.
핵심 원칙:
- 빌드 환경과 런타임 환경 분리
- 레이어 캐싱 최적화 (의존성 먼저, 소스 나중)
- 최소한의 베이스 이미지 (slim, alpine, distroless, scratch)
- 불필요한 파일 제외 (.dockerignore)
- 비root 사용자 사용 (보안)
효과:
- 이미지 크기: 80-99% 감소
- 빌드 시간: 50-80% 단축 (캐싱)
- 보안: 공격 표면 감소
- 비용: 저장소 및 전송 비용 절감
Kubernetes 배포나 CI/CD 파이프라인과 함께 사용하면 더욱 효과적입니다.