Next.js를 AWS EC2에 무중단으로 배포하기
Next.js를 EC2에 무중단 배포하는 방법은 여러 가지가 있는데, 규모와 복잡도 따라 추천이 달라져요. 크게 3가지 접근을 설명하고, 실무에서 가장 많이 쓰는 Blue-Green + Nginx 방식을 정리했습니다.
PORT 3000 · ACTIVE
⇄
PORT 3001 · STANDBY
→ 헬스체크 통과 후 Nginx가 이 둘을 서로 바꿔치기합니다.
무중단 배포의 핵심은 간단합니다. 새 버전을 완전히 띄운 뒤, 준비가 끝난 것을 확인하고 나서 트래픽을 넘긴다. EC2 위에서 이걸 구현하는 방법은 크게 세 가지가 있는데, 규모에 따라 선택지가 달라집니다.
| 방식 | 난이도 | 무중단 보장 | 적합한 규모 |
|---|---|---|---|
| PM2 cluster reload | 낮음 | 준수함 (완벽 X) | 단일 인스턴스, 소규모 |
| Blue-Green + Nginx | 중간 | 확실함 | 단일~소수 인스턴스 |
| CodeDeploy + ALB Blue-Green | 높음 | 확실함 | Auto Scaling Group, 다중 인스턴스 |
- PM2 reload: 같은 서버 안에서 워커를 하나씩 순차 재시작. 설정이 제일 간단하지만, 빌드 산출물 자체가 깨지거나 새 코드가 실행 중 프로세스와 충돌하면 순간 에러가 날 수 있어요.
- Blue-Green + Nginx: 포트 두 개(3000/3001)를 번갈아 쓰면서 Nginx가 트래픽을 스위칭. 롤백도 config 한 줄만 되돌리면 되니 실무에서 가장 균형 잡힌 선택이에요.
- CodeDeploy + ALB: 인스턴스가 여러 대거나 Auto Scaling을 쓴다면 AWS 네이티브 기능으로 가는 게 맞아요. 다만 설정이 더 무겁습니다.
인스턴스가 1~2대 규모라면 Blue-Green + Nginx가 가장 균형 잡힌 선택입니다. 설정이 크게 무겁지 않으면서도, 롤백이 config 한 줄로 끝난다는 장점이 있습니다.
배포 흐름
구체적인 구현
아래 순서대로 구성하면 됩니다.
디렉터리 구조 (EC2)
릴리스마다 폴더를 분리하고 symlink로 현재 버전을 가리키게 합니다.
/home/ubuntu/app/
├── releases/
│ ├── 20260714120000/
│ └── 20260714150000/
├── current -> releases/20260714150000 (symlink)
└── shared/.env
PM2 ecosystem 설정
포트 두 개를 각각 별도 PM2 프로세스로 등록합니다.
ecosystem.config.js
module.exports = {
apps: [
{ name: "next-blue", script: "npm", args: "start", env: { PORT: 3000 } },
{ name: "next-green", script: "npm", args: "start", env: { PORT: 3001 } },
],
};
Nginx 설정
배포 스크립트가 이 upstream 줄만 바꿔치기합니다.
/etc/nginx/conf.d/upstream.conf
upstream nextjs_upstream {
server 127.0.0.1:3000;
}
server {
listen 80;
location / {
proxy_pass http://nextjs_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
배포 스크립트
현재 활성 포트를 감지하고, 반대쪽 포트에 새 버전을 띄운 뒤 헬스체크를 통과하면 전환합니다.
deploy.sh
#!/bin/bash
set -e
CURRENT_PORT=$(grep -oP '127.0.0.1:\K[0-9]+' /etc/nginx/conf.d/upstream.conf)
if [ "$CURRENT_PORT" == "3000" ]; then
NEW_PORT=3001; NEW_APP="next-green"; OLD_APP="next-blue"
else
NEW_PORT=3000; NEW_APP="next-blue"; OLD_APP="next-green"
fi
echo "새 버전을 포트 $NEW_PORT 에 배포합니다."
cd /home/ubuntu/app/current
pm2 restart $NEW_APP --update-env
# 헬스체크 (최대 30초 대기)
for i in {1..15}; do
if curl -sf http://127.0.0.1:$NEW_PORT/api/health > /dev/null; then
echo "헬스체크 통과"
break
fi
sleep 2
done
# Nginx upstream 전환
sudo sed -i "s/127.0.0.1:[0-9]\+/127.0.0.1:$NEW_PORT/" /etc/nginx/conf.d/upstream.conf
sudo nginx -s reload
# 구버전 종료
pm2 stop $OLD_APP
echo "배포 완료: 활성 포트 $NEW_PORT"
GitHub Actions 워크플로우
main 브랜치에 push되면 빌드 후 EC2로 전송하고 배포 스크립트를 실행합니다.
.github/workflows/deploy.yml
name: Deploy to EC2
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- name: Rsync build output to EC2
uses: burnett01/[email protected]
with:
switches: -avzr --delete
path: ./
remote_path: /home/ubuntu/app/releases/${{ github.sha }}/
remote_host: ${{ secrets.EC2_HOST }}
remote_user: ubuntu
remote_key: ${{ secrets.EC2_SSH_KEY }}
- name: Run zero-downtime deploy script
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_SSH_KEY }}
script: |
ln -sfn /home/ubuntu/app/releases/${{ github.sha }} /home/ubuntu/app/current
bash /home/ubuntu/app/current/deploy.sh
핵심 포인트
헬스체크 API를 반드시 두세요.
/api/health가 없으면 아직 부팅 중인 새 버전으로 트래픽이 넘어가 순간 에러가 날 수 있어요.
DB 마이그레이션에 주의하세요.
파괴적 변경은 expand-contract 패턴으로 나눠서, 구버전과 신버전이 동시에 떠 있는 순간에도 문제가 없도록 해야 합니다.
롤백은 스위칭 로직만 반대로 실행하면 됩니다.
문제가 생기면 매우 빠르게 되돌릴 수 있어요.
인스턴스가 여러 대라면 ALB + CodeDeploy로.
Target Group 두 개(blue/green)를 두고 CodeDeploy가 전환하는 방식이 더 적합합니다.
#Nextjs #AWS #EC2 #CICD #무중단배포 #BlueGreen배포 #GitHubActions #Nginx #PM2 #DevOps
댓글
댓글 쓰기