미니 프로젝트를 진행하며 AWS를 지원받게 되었고, Next.js 프로젝트를 Vercel이 아닌 EC2에 배포해보기로 했다.
EC2에 그냥 Next.js 서버를 백그라운드로 켜놓는 식으로 배포할 수도 있었지만, 같은 EC2에 스프링부트 애플리케이션도 띄울 예정이었기 때문에 Docker를 사용하지 않으면 EC2에 jre와 node.js 등 필요한 개발 환경을 일일이 구축해야 했다.
그래서 Docker만 설치해서 간편하게 구동시킬 수 있도록 하기로 했고, Github Actions 기반으로 배포 CI/CD를 구축해 보기로 했다.
처음에 작성했던 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 단계의 비중이 컸던 것 같다.. 저 단계는 생략해도 된다고 한다. 그래서 저 단계도 제거했다.)
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"]
이 도커파일이 개선한 점은 다음과 같다.
npm install
대신 npm ci
사용으로 더 신뢰성 있는 설치이렇게 만들어진 도커 컨테이너의 크기는?
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 빌드 최적화, 근데 이제 도커를 곁들인