기본 바탕 배경
1) 이미지에 변경사항이 생기면 이를 반영하기 위해, 이미지를 다시 빌드해야 한다.
이는 컨테이너를 다시 시작하더라도 변경 사항은 반영되지 않는다. 즉 이 말은 이미지를 다시 빌드해야 한다는 것이다. 이미지는 당시의 소스 코드를 정확히 복사해서 스냅샷을 만든 것이다. 따라서 그 이후의 소스 코드 변경 사항은, 이미지의 소스 코드에 포함되지 않는다.
이미지는 기본적으로 잠겨있고, 이미지의 모든 것이 읽기 전용이다.
2) 이미지는 레이어(Layer) 기반 아키텍처 구조를 가진다.
Dockerfile에 지정한 모든 명령은 레이어를 나타낸다.
- 최종명령 이전의 모든 명령은 이미 이미지의 일부이자 별도의 레이어이다.
- 컨테이너는 이미지 위에 추가된 얇은 레이어일 뿐이다.
- 컨테이너는 이미지에 저장된 환경을 사용하고, 그 위에 부가 레이어를 추가한다.
이미지를 기반으로 컨테이너를 실행하면, 명령마다 새로운 레이어를 추가하고 이러한 레이어는 캐시된다.
변경된 소스코드가 프로젝트 종속성에 영향을 주지 않는 코드일 때, 최적화할 수 있는 부분이 생긴다.
따라서 여기서 멀티 스테이지 빌드 개념을 사용할 것이다.
개념
멀티 스테이지 빌드란 Dockerfile 내에 여러 개의 임시 빌드 단계를 정의하고, 각 단계는 다른 이미지에서 도출될 수 있도록 하는 Docker의 기능이다.
즉, 일반적인 도커 파일의 경우 단일 도커 파일이다. 이를 여러 단계의 빌드를 수행하는 방법이다.
만약, 위의 글을 봐도 이해가 안 된다면 아래의 영상을 보는 것을 추천합니다.
https://www.youtube.com/watch?v=RyildWeQ5rI
활용하기
1) 멀티 스테이지 빌드 사용 전
# Use an official Node.js runtime as a parent image
FROM node:18-alpine
# Set the working directory in the container
WORKDIR /app
# Copy the package.json and package-lock.json files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Build the Next.js application
RUN npm run build
# Expose the port that Next.js will run on
EXPOSE 3000
# Start the Next.js application
CMD ["npm", "start", "-turbo"]
사용 이전에는 베이스 이미지를 설정하고 작업 디렉토리를 설정한다. 그리고 package*.json 파일들을 이미지에 복사한다. 그리고 필요한 패키지들을 설치하고 이를 build에 필요한 모든 파일을 이미지에 복사해준다. 그리고 나서 build를 해주고, port 3000번을 통해 노출시킨 후 컨터이너를 시작시켜 명령을 실행시켜준다.
2) 멀티 스테이지 빌드 사용 후
# Use an official Node.js runtime as a parent image
FROM node:18-alpine as builder
# Set the working directory in the container
WORKDIR /app
# Copy the package.json and package-lock.json files
COPY package*.json /app
# Install dependencies
RUN npm install
# Copy the rest of the application code
COPY . /app
# Build the Next.js application
RUN npm run build
FROM node:18-alpine as production
COPY --from=builder --chown=nextjs:nextjs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nextjs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nextjs /app/public ./public
COPY --from=builder --chown=nextjs:nextjs /app/package.json ./package.json
COPY public ./public
COPY package.json ./
RUN npm install --production
# Expose the port that Next.js will run on
EXPOSE 3000
# Start the Next.js application
CMD ["npm", "start", "-turbo"]
~
여기서는 FROM 명령이 두 번 적용된다. 이 말은 빌드 단계와 프로덕트 단계로 나뉘어진다.
- builder
- FROM, WORKDIR : 위와 같이 베이스 이미지를 설정하고, 작업 디렉토리를 설정해준다.
- COPY : package*.json등의 필요한 파일을 작업 디렉토리에 복사해준다.
- RUN : npm install 명령어를 이용하여 필요한 패키지를 설치해준다.
- COPY : 프로젝트의 모든 소스 코드를 이미지에 복사한다.
- RUN : npm build 명령어를 사용해서 애플리케이션을 빌드하고 최적화 된 정적 파일을 생성한다. 빌드 결과물은 일반적으로 .next 디렉토리에 저장된다.
- production
- COPY : --from=builder 명령어를 사용하여 빌드 단계에서 생성된 .next 디렉토리 및 그 안의 정적 파일을 실행 이미지로 복사한다. public 디렉토리, package.json등의 실행에 필요한 추가 파일들을 복사한다.
- RUN : npm install --production 명령어를 사용해서 개발 의존성을 제외한 실행에 필요한 최소한의 패키지만 설치한다.
- EXPOSE : 컨테이너를 PORT 3000번으로 노출시킨다.
- CMD : npm run start -turbo 명령어를 사용해서 빌드 된 애플리케이션을 실행한다.
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /usr/src/app
# 노드 패키지 설치
# --frozen-lockfile 옵션으로 yarn.lock 파일에 작성한 그대로 패키지를 설치하도록
# 캐시 삭제해서 1차 이미지 경량화
COPY package*.json ./
RUN rm -rf ./.next/cache
# 프로젝트 빌드
FROM base AS builder
WORKDIR /usr/src/app
COPY . .
RUN npm install
RUN npm run build
# 프로젝트 실행
FROM base AS runner
WORKDIR /usr/src/app
# 컨테이너 환경에 시스템 사용자 추가
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 빌드 단계에서 생성된 결과만 복사
# next.config.js 에 standalone 디렉토리 설정 추가 -> standalone 파일 사용하여 프로젝트 실행
COPY --from=builder /usr/src/app/public ./public
COPY --from=builder --chown=nextjs:nodejs /usr/src/app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /usr/src/app/.next/static ./.next/static
# 포트 노출
EXPOSE 3000
# Next.js 애플리케이션 시작
# standalone 으로 나온 결과값은 노드 자체로만 실행 가능
CMD [ "node", "server.js" ]
오류
위까지 하다보면 standalone에서 오류가 발생할 수 있다. 여기서 Standalone 은 ‘독립형’ 또는 ‘독립적인 것’ 이라는 뜻을 가지고 있다. Next.js 에서는 웹 어플리케이션을 실행하는데 필요한 최소한의 코드만 추출하겠다는 의미로 사용된다. 내 경우 이 standalone이라는 파일이 생성되지 않아, 해당 파일이 없어서 나는 오류가 발생했다.
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
};
export default nextConfig;
next.config.mjs 파일에서 standalone에 대한 설정을 한다.
결과
위의 사진은 멀티 스테이지 빌드로 하기 전의 사진이고, 아래의 사진이 한 후이다.
원래의 용량 1.08GB → 816MB → 639MB → 149MB으로 줄었다. 최종적으로 엄청 줄었다.
Next.js의 경우 용량이 많이 줄어서 express의 경우에도 줄을까 해서 멀티 스테이지 빌드를 시도했는데 156MB에서 178MB로 오히려 용량이 조금 늘었다. 용량이 작다면 굳이 안 해줘도 될듯하다,,
👇🏻 참고 사이트
https://shimmer59.tistory.com/279
https://patrick-f.tistory.com/59
https://coding-life-diary.tistory.com/25
https://malwareanalysis.tistory.com/417
https://velog.io/@kong-e/Node.js-Express-서버-도커라이즈하기multi-stage-build