-
Nextjs + Docker를 통한 멀티 스테이지 빌더(multi-stage builder)그 외 다양한 웹 지식 2024. 3. 20. 14:58반응형
현재 Next.js를 사용하고 있고, Docker를 통해 배포를 하고 있다.
Filezilla를 통해서 업로드를 하고 있는데, React의 경우에는 build를 하고 나면 index.html과 assets 정도만 나오기 때문에 그것만 Filezilla로 올리도록 만들었다.
그런데 Nextjs의 경우에는 build를 하고 나서 static export를 하게 되면 하나의 index.html이 나오는 것이 아니라 여러개의 html 파일이 나오게 된다.
그러면서 기존에 업로드 하는 방식과 방법이 달라져서 문제가 생겼고, 이를 위해 Docker로 이미지를 만들어서 압축해서 올리면 그것을 선임 개발자가 처리하는 방식으로 했다.
그런데 다른 문제가 또 있었다.
기존에 회사에서 SI 업체에 맡겨서 작업을 했던 것들이 있어서 마이그레이션을 했는데 그 양이 무려 약 10GB 조금 못 미치는 크기가 나온 것이다.
사이즈가 크다보니 한번 빌드 할 때마다, 그리고 파일질라에 올릴 때 마다 시간이 어마어마하게 소요됐다.
그래서 적용해본 것이 멀티 스테이지 빌드(multi stage build) 되겠다.
Docker 문서에 보면 Docker Buid > Building images > Multi-state builds 라는 것을 찾을 수 있다.
Multi-stage builds are useful to anyone who has struggled to optimize Dockerfiles while keeping them easy to read and maintain.
멀티-스테이지 빌드는 도커 파일을 쉽게 읽고 유지하면서 최적화하는 데 어려움을 겪은 모든 사람에게 유용합니다.
멀티 스테이지 빌드란 (Multi-Stage build)?쉽게 말해서 가볍게 만드는 방법이다!
일반적으로 베이스가 되는 도커 이미지는 생각보다 큰 용량을 가지게 된다.
거기에 의존성(Dependencies) 파일 등이 설치되고, 빌드되면 생각보다 큰 용량으로 나타난다.
그렇기 때문에 과거부터 용량을 줄이려는 노력이 있었고 alpine, minimum 이미지와 같은 경량화 된 이미지를 사용하는 방법도 있다.
멀티 스테이지 빌드는 컨테이너 이미지를 만들면서 최종 컨테이너 이미지에는 필요 없는 환경을 제거할 수 있도록 단계를 나누어 이미지를 만드는 방법을 말한다.
조금 더 풀어서 설명하자면, 빌드에 사용 되어지는 파일과 디렉토리가 있을 것이고, 실행 시 필요한 것들이 있을 것이다.
이 두 가지를 나누어서 컨테이너 실행 시 빌드에 사용한 파일 및 디렉토리 등 의존성 파일들이 삭제 된 상태로 실행되게 만드는 것이다.
그리고 이를 통해 좀 더 가벼운 크기의 컨테이너를 사용할 수 있게 된다.
Builder Pattern
예전 리액트 프로젝트의 경우 하나의 도커 파일에서 빌드와 실행을 모두 담는 방법을 사용했었는데, 이는 하나의 이미지에서 큰 용량을 차지한다는 문제가 있었다.
이러한 문제를 해결하기 위해 빌드와 실행 이미지를 나누는 Builder Pattern이라는 것을 사용할 수도 있다.
빌더 패턴은 크게 세 가지 핵심 개념을 기반으로 한다.
1. 의존성 설정
빌드 단계에서는 애플리케이션 빌드에 필요한 모든 의존성을 설치한다.
그리고 실행 단계에서는 실제 실행에 필요한 최소한의 의존성만 포함한다.
2. 바이너리 생성
빌드 단계에서 애플리케이션 코드를 컴파일하고 최종 실행 파일(바이너리)을 생성하게 된다.
이 바이너리는 실행 단계에서 사용된다.
3. 실행 이미지 최적화
실행 이미지는 빌드 단계에서 생성된 바이너리만 포함되며 빌드에 사용된 의존성이나 불필요한 파일은 포함하지 않는다.
이러한 과정을 통해 컴팩트하게 필요한 것만 가지고 동작시키도록 하는 것이다.
그런데 이런 Builder Pattern을 위해서는 Builder와 실행 될 이미지까지 2개의 Dockerfile이 필요하다.
이는 관리도 불편한데, Multi-stage build를 사용하게 되면 하나의 Dockerfile에서 2개의 이미지를 빌드, 사용 할 수 있게 된다.
예시
예시를 보자면 기존의 방식은 아래와 같다.
[ 이전 방법 ]
# Build stage FROM node:21.5.0-alpine as builder # 환경변수 설정 ... # 작업 디렉토리 설정 WORKDIR /usr/src/app # package.json, yarn.lock 복사 COPY package.json ./ COPY yarn.lock ./ # 의존성 설치 RUN yarn install # 모든 파일 복사 COPY . . # Next.js 애플리케이션 빌드 RUN yarn build # 포트 노출 EXPOSE 3000 # Next.js 애플리케이션 시작 CMD [ "yarn", "start" ]
1. FROM : node:21.5.0-alpine 이미지를 기반으로 builder라는 이름의 빌드 단계를 정의한다.
2. ENV : 환경 변수를 설정한다. (없다면 생략 가능)
3. WORKDIR : 작업 디렉토리를 설정한다.
4. COPY : package.json과 yarn.lock 파일을 이미지에 복사한다.
5. RUN : yarn install을 사용해서 패키지를 설치한다.
6. COPY : Build에 필요한 모든 파일을 이미지에 복사한다.
7. RUN : yarn build 명령어를 사용해서 애플리케이션을 빌드한다.
8. EXPOSE : 컨테이너 PORT를 3000번으로 노출시킨다.
9. CMD : 컨테이너가 시작될 때 yarn start 명령을 실행한다.
[ 멀티 스테이지 빌드 방법 ]
# 기본 의존성 설치 FROM node:21.5.0-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 yarn.lock ./ RUN yarn --frozen-lockfile --production; RUN rm -rf ./.next/cache # 프로젝트 빌드 FROM base AS builder WORKDIR /usr/src/app COPY --from=deps /usr/src/app/node_modules ./node_modules COPY . . # 환경 변수 설정 ENV NEXT_PUBLIC_API= ENV NEXT_FRONT_BASE_API= ENV NEXT_FRONT_COMPLETE_MODUSIGN_PRODUCTION= ENV NEXT_MODUSIGN_API_KEY= ENV NEXT_PUBLIC_ACCESS_TOKEN_EXPIRY_DAYS=7 RUN yarn 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" ]
위에서 말했듯이 크게 빌드 단계와 실행 단계로 나뉘어지므로 나눠서 설명하겠다.
1. 빌드 단계 (FROM node:21.5.0-alpine as builder)
- FROM : node:21.5.0-alpine 이미지를 기반으로 builder라는 이름의 빌드 단계를 정의한다.
- ENV : 환경 변수를 설정한다. (없다면 생략 가능)
- WORKDIR : /usr/src/app 디렉토리를 작업 디렉토리로 설정한다.
- COPY : package.json과 yarn.lock 파일을 이미지에 복사한다.
- RUN : yarn install을 사용해서 개발과 빌드에 필요한 모든 패키지를 설치한다.
- COPY : 프로젝트의 모든 소스 코드를 이미지에 복사한다.
- RUN : yarn build 명령어를 사용해서 애플리케이션을 빌드하고 최적화 된 정적 파일을 생성한다. 빌드 결과물은 일반적으로 .next 디렉토리에 저장된다.
2. 실행 단계 (FROM node:21.5.0-alpine)
- FROM : node:21.5.0-alpine 이미지를 기반으로 실행
- WORKDIR : /usr/src/app 디렉토리를 작업 디렉토리로 설정한다.
- COPY : --from=builder 명령어를 사용하여 빌드 단계에서 생성 된 .next 디렉토리 및 그 안의 정적 파일을 실행 이미지로 복사한다.
- COPY : public 디렉토리, package.json, yarn.lock 파일 등 실행에 필요한 추가 파일들을 복사한다.
- RUN : yarn install --production 명령어를 사용해서 개발 의존성을 제외한 실행에 필요한 최소한의 패키지만 설치한다.
- EXPOSE : 컨테이너를 PORT 3000번으로 노출시킨다.
- CMD : yarn start 명령어를 사용해서 빌드 된 애플리케이션을 실행한다.
이점
이러한 멀티 스테이지 빌드를 통해서 빌드 및 실행 단계를 분리함으로써 실행 이미지에 불필요한 개발 의존성이나 소스 코드를 포함시키지 않을 수 있어서 이미지 크기 감소에 도움을 줄 수 있다.
그리고 빌드 단계의 레이어는 캐싱 되어서 실행 단계에서 재사용 될 수 있으므로 효율적인 레이어 캐싱이 될 수 있고, 실행 이미지가 보다 가벼워지면서 공격 노출 표면이 줄어들어 보안성이 강화된다 라는 장점을 가질 수 있다.
반응형'그 외 다양한 웹 지식' 카테고리의 다른 글
Docker <No space left on device> 문제 해결 방법 (0) 2024.07.22 Vite + React + typescript 기반의 PWA 만들기 (0) 2024.07.17 Docker 이미지 플랫폼 호환성 에러 (linux/amd64) (0) 2024.01.09 타임존(UTC, GMT)과 날짜 포맷(Date format) 그리고 luxon (0) 2023.06.28 웹페이지 미리보기 : OGP(OG) (Open Graph protocol) (0) 2022.01.03