Select a display theme:

Docker Multi Stage Build

8 번 조회

docker multi stage

Docker의 멀티 스테이지 빌드(Multi-stage Build)는 하나의 Dockerfile 내에서 여러 빌드 단계를 정의하여, 빌드에 필요한 의존성은 빌드 단계에서만 사용하고 최종 실행 이미지에는 오직 필요한 내용만 포함시킴으로써 이미지 크기를 대폭 줄일 수 있는 방법 입니다.

멀티 스테이지 빌드란?

Dockerfile 에서는 신경쓰지 않으면 어느샌가 빌드 도구, 라이브러리, 소스 코드 등 모든 요소가 한 이미지에 포함되어 최종 이미지의 용량이 커지는 문제가 있습니다.

멀티 스테이지 빌드는 여러 FROM 명령을 사용해 각 빌드 단계를 구분하고, 최종 이미지에는 빌드 결과물만 복사해 포함시킴으로써 불필요한 빌드 도구나 중간 산출물을 제거합니다.

즉, 빌드 환경과 실행 환경을 분리하여 더욱 깔끔하고 경량화된 이미지를 만들 수 있습니다.

왜 멀티 스테이지 빌드를 사용해야 하는가?

일반적으로 애플리케이션은 빌드 타임과 런타임의 의존성을 가지고 있습니다. 그 중 빌드 타임 의존성은 런타임보다 훨씬 많고 보안 취약점(CVEs)이 더 많습니다. 빌드와 런타임의 도커 이미지가 동일하다면, 불필요한 의존성(컴파일러, 테스트, 린터 등)이 포함되기 쉽습니다. 생각보다 이를 놓치거나, 제대로 처리 안되는 경우가 많기 때문에 Multi stage build 를 통해 명확히 분리한다면 좀 더 효율적인 이미지 운영이 가능합니다. 특히, Next.js 의 standalone 같은 방식을 사용한다면 multi stage 의 장점이 두드러집니다.

이미지 경량화

최종 이미지에는 실행에 필요한 산출물만 포함되므로, 불필요한 빌드 도구, 원본 소스 파일 등 불필요한 내용이 제거되어 이미지 크기가 크게 줄어듭니다. 이는 단순히 용량 감소가 아니라 더 낮은 이미지 보관 비용, 네트워크 비용 감소, 배포 시간 단축으로 이어집니다. 단순히 생각 하더라도 2GB 의 이미지와 300MB 의 이미지 라면 신규 버전 배포 시 어느쪽이 더 빠른지는 당연합니다.

보안 강화

실행에 불필요한 의존성이 최종 이미지에 포함되지 않기 때문에, 자연스럽게 상대적으로 보안 취약점이 감소합니다.

빌드 효율성

멀티 스테이지 빌드 시 각 단계별 캐시가 활용되며, 특히 BuildKit 을 사용하는 경우 대상 stage 가 의존하는 stage 만 빌드하며 서로 의존성이 없는 stage 는 병렬로 빌드할 수 있기 때문에 빌드 성능이 더 개선 됩니다.

유지보수 용이

개발, 테스트, 실행 환경을 동일 Dockerfile 내에서 분리하여 관리할 수 있어 유지보수가 용이합니다.

예시

# syntax=docker.io/docker/dockerfile:1

FROM node:18-alpine AS base

# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi


# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED=1

RUN \
  if [ -f yarn.lock ]; then yarn run build; \
  elif [ -f package-lock.json ]; then npm run build; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
  else echo "Lockfile not found." && exit 1; \
  fi

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT=3000

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

위 도커 파일은 Next.js 공식 예제 입니다. 내용을 보면 크게 의존성 설치, 빌드, 실행 세개의 stage 로 나뉘어 있는 것을 볼 수 있습니다. 여기서 가장 중요한 부분은 COPY 인데, 각 단계에서 이전 단계에서 필요한 파일만 복사하여 불필요한 중간 산출물이 의도치 않게 포함되지 않도록 하고 있습니다. 여기서 실제로 최종 이미지가 되는 부분은 runner stage 이며 builder stage 에서 빌드한 최종 결과물인 .next 디렉토리 의 내용만 복사해 오기 때문에 runner 이미지에는 node_modules, 소스코드 등 불필요한 요소가 포함되지 않게 됩니다. 그 결과 도커 이미지의 크기도 감소하고 개발 의존성으로 인한 보안 취약점도 사라지게 됩니다.

정리

Docker의 멀티 스테이지 빌드는 하나의 Dockerfile 내에서 빌드 환경과 실행 환경을 분리하여 불필요한 의존성을 제거하고 실행 환경에는 최종 산출물만 포함 하게 하는 방법 입니다. 이를 통해 이미지 경량화, 보안 강화, 빌드 및 배포 프로세스의 효율성 개선, 유지보수 용이성 등의 이점을 얻을 수 있습니다. 특히, BuildKit 을 활용하면 병렬 빌드 및 캐싱을 최적화하여 더욱 효율적으로 이미지를 운영할 수 있습니다. Next.js의 standalone 같은 방법과 같이 사용하면 멀티 스테이지 빌드의 장점이 더욱 극대화됩니다.