프론트엔드

[Next JS] Next JS 배포하자! - 1 (feat Docker, AWS EC2)

CandDoIt 2025. 4. 4. 23:45

본 포스팅에서는 Nest JS 애플리케이션 이미지를 빌드해서 AWS EC2에 배포하는 과정만을 설명합니다.

안녕하세요? 오늘은 next js 애플리케이션을 배포하는 과정에대해 포스팅 해보려합니다.

간단하게 Vercel을 사용하면 되지 않나요? AWS EC2 인스턴스를 사용하는 이유 !

1. 자유도

Vercel의 가장 큰 장점 중 하나는 편리함입니다! 배포 속도, 자동화된 빌드, GitHub 연동 등 사용자 친화적인 기능들이 잘 갖춰져 있습니다.
하지만 이러한 편리함은 곧 "추상화 수준이 높다"는 뜻이기도 합니다. 추상화가 높아지면 내부의 세세한 구성 요소들을 직접 제어하기 어려워지고, 자유도 측면에서 제약을 받을 수 있습니다. 예를 들어, Next.js의 server.js나 custom server, 미들웨어를 자유롭게 구성하거나, WebSocket 등 지속적인 서버 연결 기능을 직접 구현하는 데 제약이 있습니다. 또한, 무료 플랜에서는 도메인 설정에도 제한이 있습니다. 커스텀 도메인을 연결하려면 유료 요금제를 사용해야 하며, 라우팅 설정 역시 제한적으로 제공됩니다.

 

2. 비용 절감

Vercel은 프리티어를 제공하여 소규모 프로젝트나 프로토타입을 빠르게 배포하는 데 매우 유리합니다. 하지만 트래픽이 많아지거, 빌드 시간이 늘어나고, 사용량이 증가하면 요금이 급격하게 증가할 수 있습니다. 또한, 개인용이 아닌 퍼블릭 깃 저장소가 아닌 경우, 무료 플랜에서 사용이 제한될 수 있습니다. 기업용 프로젝트나 비공개 저장소를 사용할 경우엔 곧바로 유료 플랜으로 넘어가야 하며, 그에 따른 비용 부담도 고려해야 합니다. 반면, EC2는 일정 요금으로 인스턴스를 운영할 수 있고, 예약 인스턴스나 스팟 인스턴스를 활용하면 고정적인 비용 절감 효과를 누릴 수 있습니다. 특히 장기적으로 운영하는 서비스라면, EC2의 비용 구조가 더 유리할 수 있습니다.

 

3. 전체 인프라 통합 관리

nginx proxy, docker와 같은 오픈소스를 활용할 기회가 생깁니다. 특히 B2B SaaS나 영상 처리, 실시간 통신, 대규모 사용자 기반의 플랫폼 서비스에서는 고급 네트워크 설정이 필수적입니다. 하지만 vercel을 사용하게되면 이러한 문제를 고민할 기회를 놓치게 됩니다.

프로세스

준비물

1. cloud: AWS EC2(ubuntu), Route 53

2. 웹서버: Nginx, Lets Encrypt, Certbot

3. 컨테이너: Docker, Docker-compose

4. 도메인 이름 (가비아 or GoDaddy 구매)

 

  • 로컬에서 Docker 이미지 빌드 후 Docker Hub에 업로드 합니다.(build & push)
  • EC2에서 Docker Hub에서 이미지를 가져옵니다. (pull)
  • Docker Compose로 컨테이너 실행, 컨테이너 안에서 Next.js 애플리케이션 구동합니다.
  • Nginx를 Reverse Proxy로 설정합니다. Let's Encrypt를 사용해 HTTPS 인증 적용할 수 있습니다.
  • Route 53을 이용해 도메인 연결합니다. 사용자가 HTTPS로 접근 가능하도록 설정합니다.

 

Docker Image 만들기

우리의 운영 서버에 올릴 애플리케이션을 가볍게 만들어야 합니다. 즉, 모든 폴더를 다 가져올 필요가 없습니다. 개발에 필요없는 파일은 모두 버리고 실행에만 필요한 것들만 골라서 도커 이미지를 생성해야합니다. next js에서는 'standalone' 옵션을 통해 배포에만 필요한 프로덕션 폴더를 자동으로 설정할 수 있습니다. next.config.mjs 파일에 다음과 같이 옵션을 추가해줍니다.

// next.config.mjs 파일
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "standalone",
};

export default nextConfig;

그리고 이제 필요한 라이브러리, 설정 등을 포함하고 있는 도커 이미지를 만들어야 합니다. 도커파일은 빌드 / 프로덕션 단계를 나누어 작성할 수 있습니다. 이를 통해 도커 이미지 용량을 줄일 수 있습니다.

FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build

FROM node:20-alpine AS prod
WORKDIR /app
RUN addgroup -S nextjs && adduser -S nextjs -G nextjs

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/package.json ./package.json
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

 

빌드 단계

1.  node:20-alpine 이미지를 기반으로 가볍고 빠른 Alpine Linux 환경에서 빌드합니다.

2. /app 디렉토리에서 작업을 수행합니다.

3. 의존성 설치를 위해 package.json, package-lock.json을 복사하고 npm install을 실행합니다.

4. 전체 프로젝트 파일을 컨테이너 내부로 복사합니다.

5. Next.js 앱을 빌드합니다. 결과는 .next/standalone, .next/static 등에 생성됩니다.

 

프로덕션 단계

1. 빌드 도구 없이 깔끔한 환경을 위해 같은 node:20-alpine 이미지를 사용합니다.

2. 보안을 위해 루트 계정이 아닌 nextjs 유저로 실행되도록 설정합니다.

3. 실행에 필요한 파일만 복사합니다. --chown=nextjs:nextjs 옵션으로 해당 파일들을 nextjs 유저 소유로 설정합니다.

4. 루트 권한이 아닌 안전한 nextjs 유저로 실행합니다.

5. 앱이 사용하는 포트 3000을 외부에 노출합니다.

6. 빌드된 서버를 실행합니다 (.next/standalone/server.js가 일반적으로 진입점입니다).

 

Docker Image Build & Push

우리가 만든 도커 이미지(next js 애플리케이션)는 이제 다음과 같은 과정을 거쳐 AWS의 클라우드 서버에 배포됩니다.

현재 도커 파일이 있는 디렉토리에서, 도커 파일 이미지를 빌드 후 도커 허브로 푸시합니다. 

// 빌드 명령어
docker build -t [docker-hub user name]/[repository]:[tag] .
// 푸시 명형어
docker push [docker-hub user name]/[repository]:[tag]

만일 권한 오류가 뜨면, docker login 명령어를 통해 먼저 로그인해줍시다!

 

Docker-Compose 파일 작성

또한, 우리가 올린 이미지를 명령어 한 줄로 실행해줄 docker-compose.yml도 작성해줍니다. 이 파일은 추후에 클라우드 서버에 올라갈 파일이니, gitignore에 넣어준 다음 프라이빗하게 관리해줍시다!

services:
  web:
    container_name: web
    image: [docker-hub user name]/[repository]:[tag]
    ports:
      - "3000:3000"
    volumes:
      - /var/log/:/logs/
    restart: always
    environment:
      - DEPLOY_PATH=[백엔드 url]

1. 서비스 이름과 컨테이너 이름을 지정합니다. (web, web)

2. docker hub에 업로드된 이미지를 적어줍니다. (위에서 빌드&푸시해준 도커 이미지 [docker-hub user name]/[repository]:[tag])

3. 포트를 매핑해줍니다. 예를 들어, http://localhost:3000으로 접근하면 컨테이너의 3000번 포트로 요청이 전달됩니다.

4. 컨테이너 내부의 /logs/ 디렉터리를 호스트 /var/log/와 연결하여 로그 파일을 지속적으로 유지합니다. 컨테이너가 재시작되더라도 로그가 유지됩니다.

5. 컨테이너가 비정상 종료되더라도 항상 자동으로 재시작되도록 설정합니다.

6. 컨테이너 내부에서 사용할 환경 변수를 설정합니다.

 

마무리

이번 시간에는 next js 애플리케이션을  클라우드 서버에 올릴 준비를 했습니다. 애플리케이션을 도커 이미지로 만들고, 이미지가 동작할 docker-compose 파일을 작성했습니다. 다음에는 클라우드 환경에서 해당 파일들을 배포하는 과정에대해 알아보겠습니다.

 

참조

https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile