Docker 이미지 크기 줄이기 — 멀티스테이지 빌드 실전 가이드

게시일: 2025년 6월 10일 · 14분 읽기

프로덕션 서버의 비명

우리 회사의 첫 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"]

간단해 보이지만, 이건 끔찍하다:

멀티스테이지 빌드로 크기 줄이기

핵심 아이디어: 빌드 단계와 런타임 단계를 분리한다.

<?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'

실제 성과

우리 앱에서 최적화한 결과:

최종 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"]

주의사항

작은 이미지가 항상 최고는 아니다:

마무리

Docker 이미지 최적화는 한 번의 작업이 아니라 계속되는 과정이다. 멀티스테이지 빌드가 기본이고, 레이어 캐싱을 이해하면 개발 속도도 빨라진다.

1.2GB에서 80MB로 줄인 경험이 있으니까, 이제 새 프로젝트를 시작할 때도 바로 최적화된 Dockerfile로 시작한다. 초기 5분이 나중의 500분을 절약한다.

iL
ian.lab

실무 개발자입니다. 현장에서 겪은 문제와 해결 과정을 기록합니다. 오류 제보는 연락처로 보내주세요.