본문으로 건너뛰기
Previous
Next
Docker 멀티스테이지 빌드 최적화 | 이미지 크기 90% 감소 실전 가이드

Docker 멀티스테이지 빌드 최적화 | 이미지 크기 90% 감소 실전 가이드

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.js1.2GB200MB83%
Go800MB10MB99%
Rust2GB80MB96%
Python1GB200MB80%

빌드 시간 비교

레이어 캐싱 미적용:

첫 빌드: 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 멀티스테이지 빌드이미지 크기를 대폭 줄이고 보안을 강화하는 필수 기법입니다. 핵심 원칙:

  1. 빌드 환경과 런타임 환경 분리
  2. 레이어 캐싱 최적화 (의존성 먼저, 소스 나중)
  3. 최소한의 베이스 이미지 (slim, alpine, distroless, scratch)
  4. 불필요한 파일 제외 (.dockerignore)
  5. 비root 사용자 사용 (보안) 효과:
  • 이미지 크기: 80-99% 감소
  • 빌드 시간: 50-80% 단축 (캐싱)
  • 보안: 공격 표면 감소
  • 비용: 저장소 및 전송 비용 절감 Kubernetes 배포나 CI/CD 파이프라인과 함께 사용하면 더욱 효과적입니다.

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「Docker 멀티스테이지 빌드 최적화 | 이미지 크기 90% 감소 실전 가이드」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「Docker 멀티스테이지 빌드 최적화 | 이미지 크기 90% 감소 실전 가이드」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Docker 멀티스테이지 빌드로 이미지 크기를 90% 줄이는 방법. 빌드 도구 분리, 레이어 캐싱, distroless 이미지 활용. Node.js, Go, Rust 실전 예제. Docker·multistage bu… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Docker, multistage build, 최적화, DevOps, 컨테이너, CI/CD 등으로 검색하시면 이 글이 도움이 됩니다.