Docker 이미지 크기 줄이기 — 멀티스테이지 빌드 실전 가이드
프로덕션 서버의 비명
우리 회사의 첫 Node.js 앱을 Dockerize했을 때 이미지 크기가 1.2GB였다. 서버에 배포했을 때 다운로드만 5분이 걸렸다. 스케일링할 때마다 그 시간이 반복된다. 이건 미친 짓이었다.
그때부터 Docker 이미지 최적화를 진지하게 공부했다. 지금은 같은 앱을 80MB로 만들 수 있다. 방법을 알려주겠다.
문제 파악: 왜 1.2GB일까
처음 Dockerfile을 보면 이렇게 생겼다:
<?xml version="1.0"?>
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
간단해 보이지만, 이건 끔찍하다:
- node:18 이미지 자체가 900MB (npm, yarn, build tools 포함)
- node_modules 200MB
- build artifacts 100MB
- 전체 소스코드 복사됨
멀티스테이지 빌드로 크기 줄이기
핵심 아이디어: 빌드 단계와 런타임 단계를 분리한다.
<?xml version="1.0"?>
# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
# 패키지 설치
COPY package*.json ./
RUN npm ci --only=production
# 소스코드 복사 및 빌드
COPY . .
RUN npm run build
# Stage 2: Runtime
FROM node:18-alpine
WORKDIR /app
# 빌드 단계에서 필요한 파일만 복사
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
EXPOSE 3000
CMD ["npm", "start"]
이렇게 하면 빌드 도구들(TypeScript 컴파일러, Webpack 등)이 최종 이미지에 포함되지 않는다.
Alpine vs Distroless 비교
위의 Dockerfile은 Alpine을 사용한다. Alpine은 매우 작지만, 다른 선택지도 있다.
<?xml version="1.0"?>
# 옵션 1: Alpine (매우 작음, 일반적인 선택)
FROM node:18-alpine
# 크기: ~150MB
# 옵션 2: Distroless (가장 작음, 최소한의 도구)
FROM gcr.io/distroless/nodejs18-debian11
# 크기: ~60MB
# 옵션 3: Debian (전통적, 모든 도구 포함)
FROM node:18-debian
# 크기: ~400MB
# 옵션 4: Slim (Debian의 최소 버전)
FROM node:18-slim
# 크기: ~180MB
성능 차이도 있다:
<?xml version="1.0"?>
# Alpine: 매우 작지만 glibc 대신 musl 사용 (일부 라이브러리와 호환 문제)
# Distroless: 가장 작고 보안도 좋지만, 디버깅 도구가 없음 (kubectl exec도 불가)
# Debian: 가장 크지만 안정적이고 도구가 풍부함
# 우리는 Alpine으로 충분했다
FROM node:18-alpine
레이어 캐싱 최적화
Docker는 Dockerfile의 각 줄을 레이어로 캐싱한다. 이를 효과적으로 활용하자.
<?xml version="1.0"?>
# 나쁜 예: 소스 코드가 바뀔 때마다 npm install도 반복
FROM node:18-alpine
WORKDIR /app
COPY . . # 이게 변하면 다음 줄도 다시 실행
RUN npm ci
RUN npm run build
# 좋은 예: 패키지 파일만 먼저 복사
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./ # 이것만 변해야 다음 줄 재실행
RUN npm ci # 캐시됨 (package.json 안 바뀌면)
COPY . . # 소스 코드 변화는 여기서만 영향
RUN npm run build
개발 중에는 최적화가 명확하게 드러난다. package.json 안 바뀌었으면 npm install이 스킵되고, 빌드 시간이 10초 → 2초로 줄어든다.
.dockerignore 활용
Dockerfile의 COPY와 ADD는 불필요한 파일도 복사한다. .dockerignore로 제외하자.
<?xml version="1.0"?>
# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
.DS_Store
dist
build
coverage
.next
.turbo
*.log
# 개발 도구
Dockerfile
docker-compose.yml
.vscode
.idea
# 테스트
__tests__
*.test.ts
*.spec.ts
이렇게 하면 이미지에 불필요한 파일이 포함되지 않는다.
멀티스테이지의 다양한 활용
멀티스테이지는 npm install 최적화 외에도 쓸 수 있다.
<?xml version="1.0"?>
# Stage 1: 의존성 다운로드
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Stage 2: 빌드
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
COPY . .
# 이전 단계의 node_modules를 복사할 수도 있지만
# 캐싱을 위해 다시 npm ci하는 게 낫다
RUN npm ci
RUN npm run build
# Stage 3: 테스트 (선택사항, 배포는 하지 않음)
FROM builder AS test
RUN npm run test
# Stage 4: 최종 런타임
FROM node:18-alpine AS runtime
WORKDIR /app
# 프로덕션 의존성만 설치
COPY package*.json ./
RUN npm ci --only=production
# 빌드 아티팩트 복사
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["npm", "start"]
이렇게 하면 테스트는 빌드 프로세스 중에 실행되지만, 최종 이미지엔 포함되지 않는다.
보안 취약점 스캔
작은 이미지도 보안 문제가 있을 수 있다.
<?xml version="1.0"?>
# 이미지 스캔
$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image my-app:latest
# GitHub Actions에서 자동 스캔
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: my-app:latest
format: 'sarif'
output: 'trivy-results.sarif'
실제 성과
우리 앱에서 최적화한 결과:
- 1단계 (기본 Dockerfile): 1.2GB
- 2단계 (멀티스테이지): 450MB
- 3단계 (Alpine + .dockerignore): 180MB
- 4단계 (의존성 최적화): 80MB
최종 Dockerfile:
<?xml version="1.0"?>
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Runtime
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
주의사항
작은 이미지가 항상 최고는 아니다:
- Alpine은 일부 바이너리 호환성 문제가 있음 (특히 Python 라이브러리)
- Distroless는 디버깅이 불가능 (쉘, curl, vi 없음)
- 보안 패치가 적용되려면 이미지를 자주 다시 빌드해야 함
마무리
Docker 이미지 최적화는 한 번의 작업이 아니라 계속되는 과정이다. 멀티스테이지 빌드가 기본이고, 레이어 캐싱을 이해하면 개발 속도도 빨라진다.
1.2GB에서 80MB로 줄인 경험이 있으니까, 이제 새 프로젝트를 시작할 때도 바로 최적화된 Dockerfile로 시작한다. 초기 5분이 나중의 500분을 절약한다.