Next.js 배포 방식에 대한 고찰, Docker가 필요할까?

jwo0o0·2024년 11월 20일

Infra

목록 보기
2/2

Next.js로 개발한 사이드 프로젝트를 어떻게 배포하는게 좋을까? 🤔
모든 기술을 사용할 때 그렇듯이 각각의 방식이 장단점이 있다.
잘 정리되어 있는 기술 블로그가 있어 몇가지 방법 정리해보았다.

Next.js App을 위한 8가지 배포 및 호스팅 방법

출처: https://dev.to/ethanleetech/best-8-deployment-and-hosting-for-nextjs-dip

1. Vercel

Next.js 개발자들이 만든 플랫폼으로, Next.js 애플리케이션을 배포하기에 가장 적합한 선택

특징

  • automatic scaling을 지원
  • 서버리스와 엣지 캐싱에 대한 기본 제공을 지원
  • 효율적인 업데이트를 위한 ISR 제공
  • 글로벌 CDN으로 빠른 콘텐츠 전달

요금: 무료 플랜 O, 유료 플랜은 월 $20부터 시작
적합한 대상: Next.js와의 통합 경험을 원하거나 뛰어난 성능을 필요로 하는 개발자

2. Netlify

간단하면서도 강력한 기능을 제공하는 정적 및 동적 사이트 호스팅 플랫폼

특징

  • Git 저장소와 지속적 배포 지원
  • 동적 기능을 위한 내장 서버리스 함수
  • 자동 빌드 및 최적화
  • 역시나 글로벌 CDN 제공

요금: 무료 플랜 O, 유료 플랜은 월 $19부터 시작
적합한 대상: 쉬운 배포와 서버리스 기능이 필요한 프로젝트

3. AWS Amplify

풀스택 애플리케이션 배포를 위한 종합적인 솔루션 제공

특징

  • 정적 및 동적 콘텐츠와 SSR 지원
  • S3, Lamda 같은 다른 AWS 서비스와 손쉬운 통합
  • 자동화된 CI/CD 배포 지원

요금: 사용량 기반 과금
적합한 대상: 광범위한 클라우드 서비스와 확장성을 요구하는 애플리케이션

4. Render

간단함과 개발자 경험에 중점을 둔 클라우드 플랫폼으로, 정적 및 동적 호스팅을 모두 제공

특징

  • Git에서 자동 배포
  • 서버리스 함수 기본 지원
  • 글로벌 CDN 및 자동 SSL 인증서 제공

요금: 무료 플랜 제공, 유료 플랜은 $7/월부터 시작
적합한 대상: 단순하면서도 좋은 성능의 호스팅 솔루션을 원하는 개발자.

위 4가지 외에도 출처에서 소개하는건 DigitalOcean, Firebase Hosting, Heroku, Hostinger 가 있다.

추가로 내가 고려한 방법은

+) AWS EC2

AWS에서 지원하는 가상 서버 서비스로, Next.js 애플리케이션을 실행하는데 필요한 모든 리소스를 직접 설정 가능

특징

  • 서버 환경을 사용자가 완전히 제어
  • 유연한 확장성 (AWS Auto Scaling 사용시)
  • 커스텀 도메인 및 SSL 지원
  • 다양한 배포 방식 지원 - Github, GitLab 또는 Docker, Kubernetes 통합도 지원
  • Node.js 및 SSR 지원
  • 단점은 초기 설정의 복잡성과 직접 관리 필요, 비용 예측 어려움

요금: 프리티어 계정의 경우 월 750시간, EBS 30GB까지 무료, 유료는 사용량 기반 요금
적합한 대상: 대규모 프로젝트로 트래픽이 많거나 높은 확장성이 요구되는 경우, AWS 서비스를 통합적으로 활용하려는 경우

Vercel과 Next.js 비교

여러가지 방법 중에 내가 후보로 뽑은 두가지는 위 두가지 방식이다.
우선 Vercel의 경우 빠르고, 쉽고, 사이드 프로젝트 배포에 적합하다고 생각했다.
그리고 EC2는 이미 Express.js 서버가 EC2 인스턴스에 올라가 있었고, 이미지용 S3로 버킷도 있기 때문에 호스팅은 AWS 안에서 모두 처리하도록 구성해 통일성을 높이고 싶었다.
(AWS Amplify는 언제 과금될지 몰라 pass..)

VercelAWS EC2
간편함: github와 연동해서 자동으로 배포 가능복잡함: EC2를 구성하고 nginx, node.js, pm2 설정을 수동으로 해야 함
자동 빌드/배포: 브랜치 푸시 또는 PR 생성 시 자동으로 빌드 및 배포수동 배포: 코드를 직접 업로드하거나 배포 파이프라인을 구성해야 함
가격: 소규모 애플리케이션은 무료가격: 프리티어 한도 내에서 무료
스케일링: 트래픽 증가시 내부적으로 확장스케일링: 별도로 구성해야 함
⭐️ 소켓 사용: 불가⭐️ 소켓 사용: nginx 설정해서 가능

두 가지를 대략적으로 비교해보면 이렇다. 결론부터 말하면 EC2에 배포했는데 Vercel의 치명적인 단점은 소켓 사용이 불가하다..!

내가 배포하려는 애플리케이션은 SNS 사이드 프로젝트이기 때문에 실시간 채팅 서비스를 포함하고 있다.
그래서 몇개째인지 모를 AWS 프리티어 계정을 만들고

  • EC2 인스턴스에 Next.js 애플리케이션 실행(pm2)
  • nginx로 https 설정
  • Route53으로 커스텀 도메인 설정

으로 클라이언트 배포 환경을 구성했다.

그럼 CI/CD는?

첫번째로 선택한건 Github Actions이다. 레포지토리와 통합이 간편하기도 하고 사용해보기도 했었고 무엇보다 ⭐️무료⭐️이다. (월 2000분까지)

name: Deploy to EC2

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: SSH and deploy
        env:
          EC2_USER: ${{ secrets.EC2_USER }}
          EC2_HOST: ${{ secrets.EC2_HOST }}
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
        run: |
          echo "$SSH_PRIVATE_KEY" > private_key.pem
          chmod 600 private_key.pem
          ssh -o StrictHostKeyChecking=no -i private_key.pem -T $EC2_USER@$EC2_HOST << 'EOF'
            cd /home/ubuntu/SNS_Project
            git pull origin main
            npm install
            npm run build
            pm2 restart sns_client || pm2 start npm --name sns_client -- start
          EOF
          rm private_key.pem
        shell: bash

이렇게 yaml 파일을 작성했는데

  1. main 브랜치에 푸시
  2. EC2 인스턴스에 ssh로 접속
  3. main 브랜치를 pull 받고 pm2로 프로세스 실행

위 과정으로 CI/CD를 구성했다.

🤔 근데 뭔가 아쉬운데?
프로덕션 환경 EC2 환경에서 직접 빌드하는 것보단 빌드된 결과물만 보내서 실행하는게 낫지 않나?

하는 의문이 들었다. 보안상의 문제도 존재하고, 서버 빌드는 CPU, 메모리도 많이 차지하고 빌드 시간도 늘어난다.

그럼 이럴 때 쓰는건 뭐다?

🐳 도커다! (아님)

물론 도커의 장점도 있지만 별 고민없이 EC2 인스턴스에 도커를 설치하고 Next.js 애플리케이션을 컨테이너화 시켰다. 참고 링크

장점단점
일관된 환경 및 이식성초기 설정의 복잡성
간소화된 종속성 관리이미지 크기 관리
경량화리소스 사용량 증가
보안로컬 디버깅 어려움

결과적으로는 사용하지 않았지만 그래도 과정을 정리해보자면 아래와 같다.

  1. github actions에서 EC2에 접속해 도커 실행
  2. docker-compose로 Dockerfile 기반 이미지를 빌드하고 필요한 결과물만 복사
  3. docker-compose에서 컨테이너 실행

Dockerfile

# Base stage---------------------------
FROM node:20 AS base
RUN npm install -g pnpm

# Build stage---------------------------
FROM base AS builder
WORKDIR /app

# 종속성 파일 복사 및 설치
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

COPY . .
RUN pnpm run build

# Production Stage---------------------------
FROM node:20-alpine AS runner

# 작업 디렉토리 설정
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup -S nodejs && adduser -S -H -D -G nodejs nextjs

# 빌드 결과물과 필요한 파일만 복사
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public

USER nextjs

# 환경 변수 설정 (예: 포트)
EXPOSE 3000

# 애플리케이션 시작
CMD ["node", "server.js"]

docker-compose.yml

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      - PORT=3000
      - NEXT_PUBLIC_PROD_API_URL=${NEXT_PUBLIC_PROD_API_URL}
    env_file:
      - .env
    expose:
      - "3000" # 내부적으로 Nginx와 통신하기 위해 expose 사용
    networks:
      - webnet

  nginx:
    image: nginx:latest
    volumes:
      - /etc/nginx/conf.d/sns.jwoo.site.conf:/etc/nginx/conf.d/default.conf
      - /etc/letsencrypt:/etc/letsencrypt
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - app
    networks:
      - webnet

networks:
  webnet:
    driver: bridge

githubactions ci/cd.yml

name: Deploy to EC2

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: SSH and deploy with Docker
        env: 
          EC2_USER: ${{ secrets.EC2_USER }}
          EC2_HOST: ${{ secrets.EC2_HOST }}
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
        run: |
          echo "$SSH_PRIVATE_KEY" > private_key.pem
          chmod 600 private_key.pem
          ssh -o StrictHostKeyChecking=no -i private_key.pem -T $EC2_USER@$EC2_HOST << 'EOF'
            cd /home/ubuntu/SNS_Project
            git pull origin main
            docker-compose pull app # app 서비스의 이미지를 최신으로 가져옴
            docker-compose up -d --build # 변경된 내용을 적용하며 컨테이너를 빌드 및 재시작
            docker image prune -f # 불필요한 이미지를 삭제해 디스크 공간 확보
          EOF
          rm private_key.pem
        shell: bash

근데 우여곡절 끝에 막상 도커를 적용하고 나니까 드는 의문은 꼭 도커가 필요할까..??
파이프라인이 실행되는 시간도 약 3m~4m로 Next.js 애플리케이션의 크기에 비해 길다고 생각했고 EC2 프리티어 인스턴스 기준으로 docker가 적지 않은 용량을 차지한다.

그래서 어떤 경우에 Next.js에 도커를 적용하는지 찾아본 결과 흥미로운 영상을 보았다.

Next.js + Docker가 make sense, 즉 납득 가능한 조합이냐는건데, 결론적으로 말하면 도커로 구성된 다른 애플리케이션들이 있는 경우 납득 가능한 조합이라는 것이다.
그게 아니라 Next.js 애플리케이션 하나만 배포하려고 한다면 자신은 도커 캡틴이지만 그냥 Vercel을 사용하라고 할 것이라고 한다.

팀원이 많거나, 애플리케이션이 여러 개라던가, MSA 환경이라던가 하면 도커로 관리되고 있는 개발 환경에 Next.js가 이미지화되어 추가되는 것은 dockerize하는 것의 이점을 충분히 얻을 수 있지만, 그렇지 않다면 단점이 더 크다.

따라서 소규모 프로젝트이면서 리소스가 제한된 환경인 Next.js 1인 사이드 프로젝트에서는 도커가 적합하지 않은 기술 도입이라고 결론을 내렸다.

여기까지의 교훈은...

기술을 도입할 때는 충분히 알아보고 납득가는 근거가 있을 때 실행하자..(남들 다 쓴다고 무작정 따라하지 말자)
블로그 게시물로 정리하니까 간단해보이지만 시행착오 시간이 꽤 있었고, 이게 실무에서의 상황이라면 시간=돈이기 때문에 나는 돈을 날린 것이 아닌가 🥲

이후 Github Actions + S3 + CodeDeploy로 구성된 CI/CD로 다시 구축했고, 그 과정은 다음 포스팅에 이어서!

profile
프론트엔드 개발을 해보자

0개의 댓글