Dockerfile 최적화와 멀티스테이지 빌드
느린 빌드와 큰 이미지는 설계 문제다
docker build가 느리거나 이미지가 불필요하게 크다면 대부분 Dockerfile 설계 문제다. Node.js 앱 이미지가 1GB를 넘거나, 코드 한 줄 바꿀 때마다 npm install이 다시 실행된다면 레이어 순서와 캐시를 이해하지 못한 것이다.
이 편에서는 Docker 레이어 캐시가 어떻게 동작하는지 원리부터 이해하고, 멀티스테이지 빌드와 BuildKit 고급 기능으로 빌드를 빠르게, 이미지를 작게 만드는 방법을 다룬다.
레이어와 캐시의 원리
Dockerfile의 각 명령어는 새 레이어를 만든다. FROM, RUN, COPY, ADD가 레이어를 쌓는다. ENV, LABEL, EXPOSE, CMD 같은 메타데이터 명령어도 레이어를 만들지만 용량에는 거의 영향을 주지 않는다.
캐시 무효화 규칙: 어느 레이어 하나라도 변경되면, 그 이후의 모든 레이어가 다시 빌드된다.
.dockerignore — 빌드 컨텍스트 최소화
docker build . 실행 시 Docker 데몬은 현재 디렉터리 전체를 빌드 컨텍스트로 전송한다. .dockerignore가 없으면 node_modules, .git, 테스트 파일, 로그 등 불필요한 파일이 모두 포함되어 빌드가 느려지고 캐시가 불필요하게 무효화된다.
# .dockerignore 예시
node_modules/
.git/
.gitignore
*.log
dist/ # 빌드 아티팩트 (멀티스테이지에서 직접 생성)
.env* # 환경 변수 파일 (보안상 제외)
**/*.test.ts
coverage/
README.md기본 Dockerfile 최적화 패턴
RUN 명령어 체이닝
각 RUN은 레이어를 추가한다. 관련 명령어를 &&로 연결하고, 임시 파일은 같은 레이어에서 제거한다.
# ❌ 레이어 낭비, 캐시 패키지가 다른 레이어에 남음
RUN apt-get update
RUN apt-get install -y curl git
RUN rm -rf /var/lib/apt/lists/*
# ✅ 하나의 레이어, 캐시 파일도 같은 레이어에서 제거
RUN apt-get update && \
apt-get install -y --no-install-recommends curl git && \
rm -rf /var/lib/apt/lists/*베이스 이미지 선택
| 태그 | 크기 | 용도 |
|---|---|---|
node:20 | ~1GB | 개발·디버깅용 |
node:20-slim | ~200MB | 일반 프로덕션 |
node:20-alpine | ~50MB | 경량 프로덕션 (musl libc) |
gcr.io/distroless/nodejs20 | ~120MB | 쉘 없는 최소 런타임 |
Alpine은 작지만 glibc 대신 musl libc를 사용해 일부 네이티브 모듈에서 호환성 문제가 생길 수 있다. 프로덕션에서 문제가 발생하면 -slim 태그를 검토한다.
멀티스테이지 빌드
멀티스테이지 빌드는 하나의 Dockerfile 안에서 여러 FROM을 사용해 빌드 환경과 런타임 환경을 분리한다. 컴파일러·빌드 도구·테스트 프레임워크 등 런타임에 불필요한 것들이 최종 이미지에 들어가지 않는다.
-o /app/server ./cmd/server Go 컴파일러 (~700MB) 포함
실용적인 Node.js 멀티스테이지 예시
# syntax=docker/dockerfile:1
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json .
RUN npm ci --only=production
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# 런타임 의존성만 복사 (devDependencies 제외)
COPY --from=deps /app/node_modules ./node_modules
# 빌드 아티팩트만 복사
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json .
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]특정 스테이지만 빌드
# 개발 환경용 스테이지만 빌드
$ docker build --target builder -t myapp:dev .
# 특정 스테이지까지만 실행 (디버깅용)
$ docker build --target deps -t myapp:deps .BuildKit 고급 기능
Docker 23.0+에서 BuildKit은 기본 활성화된다. # syntax=docker/dockerfile:1 주석을 Dockerfile 첫 줄에 추가하면 최신 문법을 사용할 수 있다.
cache mount — 빌드 간 캐시 유지
--mount=type=cache는 BuildKit 전용 기능으로, 레이어 캐시와 별도로 빌드 간 영속적인 캐시 디렉터리를 제공한다.
# syntax=docker/dockerfile:1
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY go.mod go.sum .
# go 모듈 다운로드 결과를 빌드 간 캐시에 보관
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
# 컴파일 캐시도 유지
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
CGO_ENABLED=0 go build -o /app/server ./cmd/serverNode.js에서의 npm 캐시:
# syntax=docker/dockerfile:1
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json .
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN npm run buildsecret mount — 보안 자격증명 처리
빌드 중에만 필요한 API 키나 SSH 키를 이미지 레이어에 남기지 않고 전달한다.
# syntax=docker/dockerfile:1
FROM python:3.12-slim
RUN --mount=type=secret,id=pip_token \
pip install --extra-index-url https://token:$(cat /run/secrets/pip_token)@pypi.company.com/simple/ private-pkg$ docker build --secret id=pip_token,src=./pip_token.txt .시크릿은 빌드 레이어에 포함되지 않아 docker history나 이미지 레이어 검사로 노출되지 않는다.
병렬 스테이지 빌드
BuildKit은 의존성이 없는 스테이지를 자동으로 병렬 실행한다.
# syntax=docker/dockerfile:1
# 이 두 스테이지는 서로 의존하지 않으므로 병렬 실행
FROM node:20-alpine AS frontend-builder
WORKDIR /app/frontend
COPY frontend/package*.json .
RUN npm ci && npm run build
FROM golang:1.24-alpine AS backend-builder
WORKDIR /app/backend
COPY backend/go.mod backend/go.sum .
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY backend/ .
RUN go build -o /backend/server ./cmd/server
# 두 결과를 하나의 최종 이미지로 합침
FROM nginx:alpine AS runner
COPY --from=frontend-builder /app/frontend/dist /usr/share/nginx/html
COPY --from=backend-builder /backend/server /usr/local/bin/server이미지 크기 줄이기 체크리스트
이미지 분석 도구
# 레이어별 크기와 명령어 확인
$ docker history myapp:latest
# dive: 레이어별 파일 변경사항 시각화
$ dive myapp:latest
# Docker Scout (공식 취약점 분석)
$ docker scout cves myapp:latest
# 이미지 압축 크기 확인
$ docker image inspect myapp:latest | grep Size정리: 빌드 최적화 우선순위
- .dockerignore 작성 — 빌드 컨텍스트 최소화 (가장 쉬운 개선)
- COPY 순서 최적화 —
package.json→npm install→ 소스 코드 - 멀티스테이지 빌드 — 빌드 도구와 런타임 분리 (이미지 크기 50~99% 감소)
- BuildKit cache mount — CI에서 의존성 캐시 유지 (빌드 시간 40~70% 단축)
- 경량 베이스 이미지 — alpine·slim·distroless 중 요건에 맞게 선택
다음 편에서는 컨테이너 네트워킹과 볼륨을 다룬다: bridge·host·overlay 네트워크 드라이버, 볼륨 vs 바인드 마운트, tmpfs, 그리고 컨테이너 간 데이터 공유 패턴.
References
- Building best practices — Docker Docs
- Multi-stage builds — Docker Docs
- Optimize cache usage in builds — Docker Docs
- Advanced Dockerfiles: Faster Builds and Smaller Images Using BuildKit — Docker Blog
- The ultimate guide to Docker build cache — Depot
- Understanding Multi-Stage Docker Builds — Blacksmith
- Docker Layer Caching in CI Pipelines Cut Build Times by 70% — Netdata
- Dockerfile Best Practices: A Comprehensive Guide 2025 — Medium