[Docker] Multi-stage 빌드 최적화 하기

안지환·2024년 5월 14일
0

Infra

목록 보기
2/2

Multi-stage 란?

Dockerfile 내에 FROM 명령어를 사용해 빌드 시 단계별로 시작 할 수 있습니다. 이전 단계에 복사하여 최종 이미지를 원하는 시점에 빌드를 가능하게 하는 기법을 Multi-stage 입니다.

Multi-stage 를 사용해야 하는가?

현재 진행 중인 프로젝트에서는 NestJS와 React를 사용하여 서버와 클라이언트를 분리해서 개발하고 있습니다.
서버(백엔드)에서 작업한 결과물을 클라이언트(프론트엔드)에 연결하여 서로 통신이 잘 되는지 확인할 필요가 있습니다.

이때 서버, 데이터베이스, 클라이언트가 모두 실행된 상태여야 합니다. 그러나 각 사용자마다 운영체제와 컴퓨팅 환경이 다르기 때문에 정상적으로 실행할 수 있다는 것을 보장하기 쉽지 않습니다.
Docker 가상 컨테이너를 활용하면 이러한 문제 환경을 해결할 수 있습니다. 운영체제, 사양, 버전 등 서로 다른 환경에서도 Docker 이미지로 실행을 보장할 수 있습니다.

DockerHub에는 수많은 이미지를 사용하고 있으며, 각 제조사에서 필요한 이미지를 DockerHub에 제공합니다.
현재 프로젝트에서는 Dockerfile을 사용하여 Docker 이미지화를 하고 있습니다. Docker 이미지화를 할 때 Dev 환경과 Prod 환경을 구분해야 합니다.

Prod 환경에 배포할 때는 불필요한 라이브러리 제거와 이미지 용량을 최소화해야 합니다. 또한 Dev 환경에서는 프론트엔드 개발자와 백엔드 개발자 간에 Docker 이미지를 공유하여 개발 환경에 맞게 Docker 이미지를 사용해야 합니다.

두 환경의 차이에 맞추어 Docker 이미지가 빌드되어야 합니다. 이 문제를 해결하기 위해 Multi-stage 를 사용하여 서로 다른 빌드 환경을 구분할 수 있습니다.

Multi-stage 하면 이전과 무엇이 달라졌을까?

개발 환경(Dev), 빌드 환경(Build), 프로덕션 환경(Prod)으로 단계를 구분했습니다.

  1. Dev 단계에서는 모든 의존성 라이브러리를 설치합니다.
  2. Build 단계에서는 최소한의 크기로 빌드를 컴파일합니다. 개발 환경에서 설치한 의존성을 복사하고, 로그 폴더가 없다면 생성합니다. 그리고 실제 빌드를 수행합니다.
  3. Prod 단계에서는 배포용 빌드를 합니다. 이 단계에서는 소스 코드를 컴파일하고 최소한의 자산을 생성합니다. 개발 단계와 동일한 기본 이미지와 모범 사례를 사용하며, 환경을 "프로덕션"으로 설정합니다. 빌드 단계에서 설치한 의존성을 복사하고, 프로덕션 전용 의존성을 설치한 후 캐시를 정리하여 번들 크기를 최소화합니다.

Dev 단계

Dev 단계에서는 모든 의존성 라이브러리를 설치합니다.

FROM node:18-alpine AS dev

RUN apk add --no-cache libc6-compat && \\
    apk update && \\
    apk add --update curl && \\
    rm -rf /var/cache/apk/*

USER root
WORKDIR /app

RUN addgroup --system --gid 1001 nodejs && \\
    adduser --system --uid 1001 nestjs

COPY --chown=nestjs:nodejs . .

RUN npm ci && npm cache clean --force

HEALTHCHECK CMD curl --fail <http://localhost:3333/health> || exit 1

USER nestjs

Build 단계

Build 단계에서는 Build와 dev를 제외한 의존성을 설치합니다. 로그 폴더가 없을 시 폴더를 생성합니다.

FROM node:18-alpine AS build

ENV NODE_ENV prod

RUN apk add --no-cache libc6-compat && \\
    apk update && \\
    apk add --update curl && \\
    rm -rf /var/cache/apk/*

USER root
WORKDIR /app

RUN addgroup --system --gid 1001 nodejs && \\
    adduser --system --uid 1001 nestjs

COPY --chown=node:node --from=dev /app/node_modules /app/node_modules

COPY --chown=nestjs:nodejs . .

RUN if [ ! -d "/app/log" ]; then mkdir -p /app/log; fi && \\
    npm run build && \\
    npm ci --omit=dev && \\
    npm cache clean --force

USER nestjs

Prod 단계

Prod 단계에서는 소스 코드를 컴파일하고, 최소한으로 이미지를 생성합니다.

  • 개발 단계와 동일한 기본 이미지를 사용합니다.
  • 환경을 "프로덕션"으로 설정합니다.
  • Nest CLI 빌드 스크립트를 실행하기 위해 개발 단계에서 설치한 종속성을 복사합니다.
  • 빌드가 완료되면 프로덕션 전용 종속성을 설치하고 NPM 캐시를 정리합니다. 이렇게 하면 번들 크기가 최소화됩니다.
FROM node:18-alpine AS prod

ENV NODE_ENV prod

# Set install apk
RUN apk add --no-cache libc6-compat && \\
    apk update && \\
    apk add --no-cache --update curl && \\
    rm -rf /var/cache/apk/*

## Set the timezone in Seoul
RUN apk --no-cache add tzdata && \\
    cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \\
    echo "Asia/Seoul" > /etc/timezone

USER root
WORKDIR /app

RUN addgroup --system --gid 1001 nodejs && \\
    adduser --system --uid 1001 nestjs

COPY --chown=nestjs:nodejs --from=build /app/dist /app/dist
COPY --chown=nestjs:nodejs --from=build /app/node_modules /app/node_modules
COPY --chown=nestjs:nodejs --from=build /app/log /app/log

USER nestjs

HEALTHCHECK CMD curl --fail <http://localhost:3031/health> || exit 1
CMD [ "node", "dist/main.js" ]

이렇게 Multi-stage를 사용함으로써 개발 환경, 빌드 환경, 프로덕션 환경을 분리하여 각 환경에 맞는 최적화된 Docker 이미지를 생성할 수 있게 되었습니다.

참고

profile
BackEnd Developer

0개의 댓글