Next.js standalone 빌드로 도커 이미지 최적화하기

공이·2025년 1월 11일
1

회고

목록 보기
3/3
post-thumbnail

미니 프로젝트를 진행하며 AWS를 지원받게 되었고, Next.js 프로젝트를 Vercel이 아닌 EC2에 배포해보기로 했다.

EC2에 그냥 Next.js 서버를 백그라운드로 켜놓는 식으로 배포할 수도 있었지만, 같은 EC2에 스프링부트 애플리케이션도 띄울 예정이었기 때문에 Docker를 사용하지 않으면 EC2에 jre와 node.js 등 필요한 개발 환경을 일일이 구축해야 했다.

그래서 Docker만 설치해서 간편하게 구동시킬 수 있도록 하기로 했고, Github Actions 기반으로 배포 CI/CD를 구축해 보기로 했다.

초기 작성 Dockerfile

처음에 작성했던 Next.js Dockerfile이다.

# 1단계: 환경설정 및 dependency 설치
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .

# 2단계: 빌드 단계
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package*.json ./
RUN npm install --production
ENV NEXT_PUBLIC_BASE_URL=http://action-be:8080

# 3단계: 실행 단계
CMD ["npm", "run", "start"]

멀티스테이지 방식과 alpine 이미지를 사용해서 나름 최적화된 Dockerfile이었다.

아래는 초기 작성했던 workflow yml 파일이다.

name: Deploy Frontend

on:
  push:
    paths:
      - "frontend-3rd-loan/**"
    branches:
      - "fe"
      # TODO: 최종에서는 fe -> main으로 바꾸기

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Set up Docker
        run: |
          sudo apt-get update
          curl -fsSL https://get.docker.com -o get-docker.sh
          sudo sh get-docker.sh
      - name: Log in to DockerHub
        run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin

      # docker hub push
      - name: Push frontend Docker image
        run: |
          cd frontend-3rd-loan
          docker build -t ${{ secrets.DOCKER_USERNAME }}/action-fe:1.0 .
          docker push ${{ secrets.DOCKER_USERNAME }}/action-fe:1.0
      # EC2 Server connection & docker deploy
      - name: EC2 Docker deploy
        uses: appleboy/ssh-action@v0.1.6
        with:
          host: ${{ secrets.EC2_IP }}
          username: ${{ secrets.EC2_USER }}
          key: ${{ secrets.EC2_SSH_KEY }}

          # Stop and remove only the relevant container
          # Remove old image
          # Pull new image and run
          script: |
            sudo docker stop action-fe || true
            sudo docker rm action-fe || true
            sudo docker rmi -f ${{ secrets.DOCKER_USERNAME }}/action-fe:1.0 || true
            sudo docker pull ${{ secrets.DOCKER_USERNAME }}/action-fe:1.0
            sudo docker run -d -p 80:3000 --name action-fe ${{ secrets.DOCKER_USERNAME }}/action-fe:1.0

그렇지만 github workflow 과정에 걸리는 시간이 3분 정도로 너무 길었기 때문에 도커이미지를 더 최적화시키면 되지 않을까? 하는 생각으로 Next.js 이미지를 더 최적화 해보고자 했다. (그런데 오래 걸렸던 원인 중에서 위의 워크플로우 중 Set up Docker 단계의 비중이 컸던 것 같다.. 저 단계는 생략해도 된다고 한다. 그래서 저 단계도 제거했다.)

수정한 Dockerfile

Next.js standalone 빌드 방식을 사용했다.

Next.js standalone 빌드란?

Next.js의 standalone output은 Next.js 12부터 도입된 기능으로, next build 실행 시 .next/standalone 디렉토리에 완전히 독립적으로 실행 가능한 서버를 생성한다. 최소한의 node_modules만 포함하여 node server.js로 직접 실행시킬 수 있다.

이 방식을 사용하기 위해서는 next.config.js도 수정해야한다.

const nextConfig = { output: "standalone" };

export default nextConfig;

그렇게 해서 열심히 서치해가며 수정된 Dockerfile!

# 기본 이미지로 node:18-alpine을 사용하여 base 스테이지 생성
FROM node:18-alpine AS base

# deps 스테이지: 의존성 설치를 위한 단계
FROM base AS deps
# libc6-compat 패키지 설치 (알파인 리눅스 호환성을 위해)
RUN apk add --no-cache libc6-compat
# 작업 디렉토리 설정
WORKDIR /usr/src/app
# package.json과 package-lock.json 파일 복사
COPY package.json package-lock.json ./
# npm 의존성 설치
RUN npm ci
# Next.js 캐시 삭제
RUN rm -rf ./.next/cache

# builder 스테이지: 소스 코드 빌드를 위한 단계
FROM base AS builder
WORKDIR /usr/src/app
# deps 스테이지에서 설치한 node_modules 복사
COPY --from=deps /usr/src/app/node_modules ./node_modules
# 현재 디렉토리의 모든 파일 복사
COPY . ./
# Next.js 애플리케이션 빌드
RUN npm run build

# runner 스테이지: 최종 프로덕션 이미지
FROM base AS runner
WORKDIR /usr/src/app
# 프로덕션 환경 설정
ENV NODE_ENV=production
# 보안을 위한 시스템 그룹과 사용자 생성
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 빌드된 파일들을 복사
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
# nextjs 사용자로 전환
USER nextjs
# 3000번 포트 노출
EXPOSE 3000
# 포트 환경변수 설정
ENV PORT=3000
# 백엔드 API URL 환경변수 설정
ENV NEXT_PUBLIC_SERVER_API_URL=http://action-be:8080
# 서버 실행 명령
CMD ["node", "server.js"]

이 도커파일이 개선한 점은 다음과 같다.


  1. 보안 강화
  • 전용 시스템 사용자(nextjs)와 그룹(nodejs) 생성
  • root 대신 nextjs 사용자로 애플리케이션 실행
  • 보안 취약점 감소 및 권한 제한
  1. 더 효율적인 의존성 관리
  • npm install 대신 npm ci 사용으로 더 신뢰성 있는 설치
  • deps 스테이지를 별도로 분리하여 캐싱 최적화
  • 불필요한 캐시 제거 (.next/cache)
  1. 더 체계적인 멀티 스테이지 빌드
  • base, deps, builder, runner 4단계로 명확히 구분
  • 각 단계의 역할이 더 명확하게 분리됨
  • 중간 빌드 결과물을 효율적으로 재사용
  1. 더 최적화된 결과물 복사
  • standalone 출력물 사용으로 더 최적화된 프로덕션 번들
  • 정확한 파일 권한 설정 (chown 사용)
  • 필요한 파일만 선택적으로 복사
  1. 명확한 포트 설정
  • EXPOSE 명령으로 컨테이너 포트 명시
  • PORT 환경변수 별도 설정

이렇게 만들어진 도커 컨테이너의 크기는?

654.83MB -> 147.18MB로 크기가 약 77.52% 감소했다.

Github Actions Workflow 시간도 2m 53s에서 1m 26s 정도로 줄어들었다. (위에서 말했듯 Set up Docker 단계를 삭제해서 줄어든 영향도 있을 것이다.)

도커 이미지 크기가 줄어든다고 Github Actions workflow 시간도 줄어드는걸까? 상관관계가 있을까? 궁금해서 지선생에게 물어봤다.

도커 이미지 크기와 GitHub Actions 워크플로우 시간 간에는 상관관계가 있을 수 있습니다. 주요 요인은 다음과 같습니다.
1. 빌드 시간: 도커 이미지가 작아지면 빌드 시간이 줄어들 수 있습니다. 이미지에 포함된 레이어가 적어지고, 필요한 패키지나 라이브러리의 크기가 줄어들면 빌드 프로세스가 더 빨라질 수 있습니다.
2. 캐시 활용: 도커 이미지 크기가 작으면, GitHub Actions가 이전 빌드의 캐시를 더 효율적으로 사용할 수 있습니다. 캐시를 사용하면 빌드 시간이 단축될 수 있습니다.
3. 푸시/풀 시간: 워크플로우 중 도커 이미지를 레지스트리에 푸시하거나 풀링하는 과정이 있을 경우, 이미지 크기가 작아지면 이 과정도 더 빠르게 진행됩니다. 네트워크 전송 시간도 줄어들게 됩니다.
4. 테스트 및 배포 시간: 이미지 크기가 줄어들면 컨테이너의 초기화 시간도 줄어들 수 있어, 테스트 및 배포 단계에서도 시간이 절약될 수 있습니다.

결론적으로, 도커 이미지 크기를 줄이면 GitHub Actions 워크플로우의 전체 시간이 단축될 가능성이 있다. 그렇지만 워크플로우의 전체 시간은 네트워크 속도, GitHub Actions의 실행 환경 등 다양한 요인에 의해 영향을 받을 수 있으므로 이미지 크기만이 유일한 요인은 아니다.


참고

Next.js 프로젝트 docker 배포 + 이미지 크기 줄이기
캐시(Cache)를 이용한 Next.js 빌드 최적화, 근데 이제 도커를 곁들인

0개의 댓글