TechFeedTechFeed
Cloud & DevOps

Docker 멀티스테이지 빌드 실전 - Node.js·Next.js 이미지 크기 줄이는 체크리스트

도커 멀티스테이지 빌드(Docker multi-stage build)로 Node.js Express·API 서버와 Next.js standalone 이미지를 최적화하는 실전 가이드. 단일 스테이지와 멀티스테이지 구조 차이, 레이어 캐시 최적화(package.json 먼저 복사), .dockerignore 설정, non-root 사용자 실행, 이미지 크기 확인 명령어, 자주 막히는 alpine 호환 문제·환경변수 주입까지 체크리스트 형태로 정리. 백엔드·풀스택·데브옵스 개발자를 위한 도커 배포 최적화 실무 가이드.

by

Node.js 앱을 그냥 빌드하면 도커 이미지가 1~2GB를 넘기 쉽다. node_modules 전체가 그대로 들어가고, 빌드 도구·개발 의존성까지 이미지에 남기 때문이다. 멀티스테이지 빌드(multi-stage build)를 쓰면 런타임에 필요한 것만 남겨 이미지를 대폭 줄일 수 있다.


아래는 Node.js·Next.js 기준 멀티스테이지 Dockerfile 패턴, 자주 빠뜨리는 설정, 최종 이미지 크기 확인 방법을 단계별로 정리한다. 명령어는 실제로 동작하는 것만 적는다.


왜 멀티스테이지 빌드를 쓰나 - 단일 스테이지의 문제

단일 스테이지 Dockerfile은 빌드 환경과 런타임 환경이 같다. 결과적으로 이미지에 필요 없는 것들이 들어간다. 개발 의존성이 많은 타입스크립트 프로젝트에서는 이 문제가 특히 두드러진다.


문제 원인 영향
이미지 크기 1기가 이상개발 의존성, 빌드 캐시 그대로 포함배포 속도 저하, 레지스트리 비용 증가
보안 취약점 노출 표면 확대빌드 도구·개발 패키지가 런타임에 잔류취약점 스캔에서 불필요한 패키지가 잡힘
레이어 캐시 비효율소스 파일 변경마다 의존성 설치 재실행빌드 시간 증가, 배포 지연

멀티스테이지 빌드는 한 파일 안에 여러 빌드 단계를 두고, 마지막 단계에서 앞 단계의 결과물만 골라 복사하는 방식이다. 빌드 도구는 중간 단계에서만 쓰고, 최종 이미지에는 런타임에 필요한 파일만 들어간다. 공식 도커 문서(Multi-stage builds)에서도 이 패턴을 기본 권장 방식으로 설명한다.


Node.js Express·API 서버 - 기본 멀티스테이지 패턴

Node.js API 서버의 가장 기본적인 멀티스테이지 구조다. 세 스테이지로 나눈다.


Dockerfile - Node.js API 서버 멀티스테이지 기본
# 1단계: 의존성 설치 (devDependencies 포함) FROM node:22-alpine AS deps WORKDIR /app COPY package*.json ./ RUN npm ci # 2단계: 빌드 (TypeScript 컴파일 등) FROM node:22-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build # 3단계: 런타임 (프로덕션 의존성만) FROM node:22-alpine AS runner WORKDIR /app ENV NODE_ENV=production COPY package*.json ./ RUN npm ci --omit=dev # devDependencies 제외 COPY --from=builder /app/dist ./dist EXPOSE 3000 CMD ["node", "dist/index.js"]
💡 팁: npm ci --omit=dev--production의 현대적 대체다. npm 7 이상에서는 --omit=dev를 권장한다. npm prune --production보다 deps 스테이지 결과물을 바로 쓰는 쪽이 캐시 효율이 더 좋다.

Next.js - standalone 출력 활용 패턴

Next.js는 output: 'standalone' 설정을 쓰면 빌드 결과물에 필요한 node_modules 파일만 골라서 .next/standalone 폴더에 넣어준다. 이걸 도커 이미지에 그대로 복사하면 런타임 이미지를 최소화할 수 있다.


next.config.js - standalone 출력 설정
/** @type {import('next').NextConfig} */ const nextConfig = { output: 'standalone', // ...다른 설정 }; module.exports = nextConfig;
Dockerfile - Next.js standalone 멀티스테이지
# 1단계: 의존성 설치 FROM node:22-alpine AS deps WORKDIR /app COPY package*.json ./ RUN npm ci # 2단계: Next.js 빌드 FROM node:22-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build # 3단계: 런타임 (standalone 결과물만 복사) FROM node:22-alpine AS runner WORKDIR /app ENV NODE_ENV=production # standalone 폴더 전체 복사 COPY --from=builder /app/.next/standalone ./ # 정적 자산 복사 (standalone에 포함되지 않음) COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" CMD ["node", "server.js"]
Docker 멀티스테이지 빌드 이미지 크기 비교 다이어그램
단일 스테이지 vs 멀티스테이지 빌드 이미지 크기 비교 (출처: Docker 공식 문서)

레이어 캐시 최적화 - package.json을 먼저 복사하는 이유

도커는 COPY 명령어의 소스 파일이 바뀌면 그 이후 레이어를 전부 다시 실행한다. package.json과 소스 파일을 함께 복사하면 소스만 바뀌어도 npm install을 매번 다시 실행하게 된다.


올바른 순서는 package.json을 먼저 복사하고 npm ci를 실행한 다음, 소스 파일을 복사하는 것이다. 이렇게 하면 의존성이 바뀌지 않는 한 npm ci 레이어가 캐시에서 재사용된다.


패턴 소스만 변경 시
COPY . . → npm ci (잘못된 순서)npm ci 캐시 무효화 → 매번 재설치
COPY package*.json → npm ci → COPY . . (올바른 순서)npm ci 캐시 그대로 → 소스 복사만 재실행

이미지 크기 확인과 최적화 점검 명령어

빌드 후 이미지 크기와 레이어 구성을 확인하는 명령어들이다.


이미지 크기 확인 및 레이어 분석
# 이미지 크기 확인 docker images my-app # 레이어별 크기 분석 (dive 설치 필요) dive my-app # docker history로 레이어 크기 확인 docker history my-app --no-trunc # 빌드 시 단계별 크기 출력 docker build --progress=plain -t my-app .
멀티스테이지 빌드 체크리스트 - 한 번에 검증
# 빌드 docker build -t my-app:test . # 이미지 크기 확인 docker images my-app:test # 컨테이너 실행 후 헬스 체크 docker run -d -p 3000:3000 --name my-app-test my-app:test curl http://localhost:3000/health # 컨테이너 파일 구조 확인 (불필요 파일 잔류 체크) docker exec my-app-test ls -la /app # 정리 docker stop my-app-test && docker rm my-app-test
Docker 컨테이너 레이어 구조 다이어그램
도커 멀티스테이지 빌드의 레이어 캐시 구조 (출처: Docker 공식 문서)

배포 전 멀티스테이지 빌드 체크리스트 - 자주 빠뜨리는 항목

멀티스테이지 빌드 점검 항목


  • 최종 스테이지에 node_modules 전체가 아니라 프로덕션 의존성만 들어갔는지 확인
  • COPY . . 전에 .dockerignore.git, node_modules, .env를 제외하는지 확인
  • Next.js: output: 'standalone' 설정 후 .next/staticpublic을 별도 복사했는지 확인
  • ENV NODE_ENV=production이 최종 스테이지에 있는지 확인
  • 시간대(TZ) 환경변수가 필요하다면 최종 스테이지에서 설정했는지 확인
  • EXPOSE가 실제 앱 포트와 일치하는지 확인
  • non-root 사용자로 실행하도록 USER node(또는 별도 사용자) 추가 여부
  • 이미지 태그에 커밋 해시나 버전을 넣어 롤백이 가능한지 확인

특히 .dockerignore를 만들지 않으면 COPY . .가 로컬 node_modules를 컨테이너로 복사해 버린다. 이러면 의존성 스테이지에서 설치한 패키지가 로컬 것으로 덮어써지는 문제가 생긴다.


.dockerignore - 기본 설정
node_modules .git .gitignore .env .env.local .env.*.local .next dist coverage *.log .DS_Store README.md

FAQ - 자주 막히는 것

멀티스테이지 빌드를 쓰면 이미지가 얼마나 줄어드나요?

프로젝트마다 다르다. 일반적으로 devDependencies가 많고 타입스크립트를 쓰는 Node.js 앱은 단일 스테이지 대비 최종 이미지 크기가 절반 이하로 줄어드는 경우가 많다. Next.js standalone 옵션까지 결합하면 더 작아질 수 있다. 정확한 수치는 직접 빌드해서 docker images로 비교해야 한다.


alpine 베이스 이미지를 써도 되나요? 문제가 없나요?

alpine은 이미지 크기가 작아서 많이 쓰지만, musl libc를 쓰기 때문에 일부 네이티브 모듈(특히 C++ 바인딩이 있는 패키지)에서 호환 문제가 생길 수 있다. 문제가 생기면 node:22-slim(Debian 기반, 최소 패키지)을 쓰는 게 대안이다.


빌드 캐시가 자꾸 무효화됩니다. 원인이 뭔가요?

가장 흔한 원인은 COPY . .package.json 복사보다 먼저 오는 구조다. 소스 파일이 조금만 바뀌어도 npm 설치 레이어부터 전부 다시 실행된다. COPY package*.json ./RUN npm ciCOPY . . 순서로 바꾸면 해결된다.


환경변수를 Dockerfile에 넣으면 안 되는 이유가 뭔가요?

Dockerfile에 ENV SECRET=value로 넣으면 이미지 레이어에 값이 그대로 기록된다. 이미지를 누구에게 배포하거나 레지스트리에 올리면 값이 노출된다. 런타임 환경변수는 docker run -e나 쿠버네티스 Secret, 도커 컴포즈 env_file로 주입해야 한다.


non-root 사용자로 실행하려면 어떻게 하나요?

node 공식 이미지에는 node 사용자가 이미 만들어져 있다. 파일 복사 후 USER node를 추가하면 된다. 파일 소유권이 root로 복사되는 경우가 있어서 COPY --chown=node:node로 소유권을 함께 지정해야 Permission denied를 피할 수 있다.


Docker 컨테이너 이미지 최적화 비교 화면
docker images 명령으로 단일 스테이지와 멀티스테이지 이미지 크기 비교 (출처: Docker)

국내 클라우드 환경(AWS 서울 리전, NCP, 카카오 클라우드)에서 컨테이너 이미지를 자체 레지스트리에 올릴 때도 이미지 크기는 비용에 직접 영향을 준다. 스토리지 비용보다는 배포 시 전송 시간과 오토스케일링 지연이 더 눈에 띄는 문제다. 파드가 새로 뜰 때 이미지 풀(pull) 시간이 짧을수록 서비스 복구 속도가 빠르다.


멀티스테이지 빌드를 도입한 뒤에는 의존성이 변경되지 않은 빌드에서 레이어 캐시가 효과적으로 작동하는지 확인해야 한다. 로컬 빌드 시간이 크게 줄었다면 캐시가 제대로 적용되고 있는 것이다.


도커Docker멀티스테이지 빌드Node.jsNext.js컨테이너이미지 최적화데브옵스개발자배포

관련 도구

함께 보면 좋은 문제 해결

EXPLORE / Cloud & DevOps

이어서 읽어보기

전체 토픽 둘러보기