WS + WAS 서버 Nginx로 배포하기(ft. 배포 스크립트)

dyeon-dev·2025년 9월 25일
post-thumbnail

학습 목적: WS 와 WAS 를 명확하게 책임/역할 별로 구분하는 것

학습 목적을 달성하기 위해 정적 컨텐츠는 Web Server, 동적 컨텐츠는 WAS가 담당하도록 했습니다. 쉽게 말해, WAS 앞단에 Web Server를 하나 둬서 효율적으로 서버 트래픽을 관리하는 것입니다.

브라우저에서 정적 컨텐츠를 만나면 Web Server는 WAS로 서빙하는 역할을 해주어야 합니다. 이러한 역할을 해주는 고성능 웹서버 프로그램인 Nginx를 사용했습니다.

배포를 위해 1) Nginx2) pm2를 설정해주고, 3) 배포 스크립트를 작성했어요. 🛠️

배포 환경

  • VMware Fusion
  • Ubuntu 24.04
  • 서버 스택
    • git
    • npm
    • pm2: 앱은 PM2로 127.0.0.1:3000에서 동작
    • Nginx: Node 3000 + Nginx 80

우분투 서버로 배포를 진행했고, 아래와 같은 로직으로 구성했습니다.

빌드 → Nginx 설정 적용 → 정적, 동적 컨텐츠 리버스 프록시 → pm2 프로세스로 WAS 실행

이 글에서 VMware 우분투 환경 설정 과정은 생략합니다.

1) Nginx 설정

우분투 서버에 Nginx 설치 및 시작

  • Ubuntu에서 Nginx를 설치하면 자동으로 systemd 서비스(nginx.service)에 등록됩니다.
  • 서버가 부팅될 때 systemd가 nginx를 기동합니다.
    → 즉, 기본적으로 OS 레벨에서 항상 실행 중이에요.
sudo apt update
sudo apt install nginx -y
sudo systemctl enable nginx    # 부팅 시 자동 시작
sudo systemctl start nginx
sudo systemctl status nginx --no-pager # active (running) 상태여야 정상.

방화벽(ufw) 개방

sudo ufw allow 'Nginx Full'   # 80/443 허용
sudo ufw status

테스트

→ 설정 OK 나와야 함.
외부 브라우저에서 확인
http://172.30.1.86/
→ 기본 Nginx 웰컴 페이지가 보여야 정상.

동작 확인 (서버 → 외부 순서)

# Nginx가 80 포트 리슨 중인지
sudo ss -lntp 'sport = :80'

# Nginx 경유 헬스체크(프록시 OK 확인)
curl -i http://127.0.0.1/health

# 정적 파일 핑(있을 경우)
curl -I http://127.0.0.1/assets/

외부(맥)에서

http://172.30.1.86/health
http://172.30.1.86/

Nginx에 .conf 파일 만들기

sudo mkdir -p /etc/nginx/conf.d
sudo nano /etc/nginx/conf.d/codestargram.conf
.conf에서 리버스 프록시 설정
# /etc/nginx/conf.d/codestargram.conf

# 앱(백엔드) 업스트림: PM2가 띄운 Node 서버
upstream codestargram_app { # 백엔드 서버(3000)를 논리적 그룹으로 묶는 곳
    server 127.0.0.1:3000;
    keepalive 64; # Nginx ↔ WAS 사이 연결 재사용. 요청마다 TCP를 새로 열지 않고, 성능/지연에 이점.
}

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;  # 어떤 Host로 오든(도메인/IP) 수신

    # 정적 빌드 자산 (Vite dist/assets)
    location /assets/ {
        alias /var/www/codestargram/repo/web-p3-codestargram2/dist/assets/;
        access_log off;
        expires 1y;
        # expires/Cache-Control로 강한 캐시 적용해 성능↑ (Vite의 해시 파일명과 궁합 좋음)
        add_header Cache-Control "public, max-age=31536000, immutable";
        try_files $uri =404;
    }

    # 헬스체크(앱으로 프록시) - 모니터링/로드밸런서가 이 경로를 주기적으로 호출해 상태 확인
    location = /health {
        proxy_pass http://codestargram_app;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Connection        "";
    }

    # 나머지 모든 경로는 Node 앱으로 프록시
    location / {
        proxy_pass http://codestargram_app;
        proxy_http_version 1.1;

        # 표준 프록시 헤더
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Connection        "";
    }
}
  

📌 conf 흐름
클라이언트 요청 → Nginx :80 수신

  • /assets/* → Nginx가 직접 /var/www/codestargram/.../dist/assets/에서 파일 제공(고속·캐시 가능)
  • /health → Nginx가 PM2에서 띄운 Node.js WAS(127.0.0.1:3000)로 프록시
  • 나머지 /* (/login, /register, /api/...,그 외 모든 경로) → 전부 WAS(127.0.0.1:3000)로 프록시

Nginx 적용 시 장점

  • Nginx가 빠르게 캐시/서빙 → WAS 부하 감소
  • 동적/API는 프록시로 WAS에 전달 → 보안/구조 분리
  • 프록시 헤더로 진짜 IP/프로토콜을 WAS가 정확히 알 수 있음
  • upstream으로 확장성(멀티 WAS, 로드밸런싱) 확보

2) pm2 설정

pm2를 왜 사용할까?

PM2는 Process Manager의 약자로 NodeJS 프로세서를 관리하는 원활한 서버 운영을 위한 패키지입니다. 개발할 때 nodemon을 쓴다면, 배포할 때는 pm2를 씁니다.

  • 서버를 백그라운드에서 실행시켜 터미널을 닫아도 꺼지지 않음.
  • 서비스를 제공하고 있는 도중 서버가 다운되어도 서버를 다시 켜줌.
  • Node.js는 싱글 스레드 기반이지만, 멀티 코어 혹은 하이퍼 스레딩을 사용할수 있게 해줌.
  • 클라이언트로부터 요청이 올 때 알아서 요청을 여러 노드 프로세스에 고르게 분배함. (로드 밸런싱)

pm2 설정 파일 만들기 (ecosystem.config.js)

pm2를 보다 수월하게 관리하기 위해 ecosystem.config.js 라는 파일을 만들어 설정할 수 있습니다. 프로젝트 루트에 ecosystem.config.js을 생성합니다.

pm2 ecosystem # ecosystem.config.js 파일이 생성

저는 ESM을 사용중이었기 때문에 ecosystem.config.cjs 로 변경했습니다.

module.exports = {
  /* apps 항목은 pm2에 사용할 옵션을 기재 */
  apps : [{
    name: 'codestargram2', // app 이름 
    script: 'server/app.js', // 실행할 스크립트 파일
    cwd: __dirname,          // 레포 루트 기준 실행 (deploy.sh가 cd 후 실행)
    // 워칭(운영에서는 비활성)
    watch: false,
    // watch: ["server", "src"],       // 개발용: 필요 시 경로 지정
    // ignore_watch: ["node_modules", "logs", "dist"],

    // 로그
    merge_logs: true,
    time: true,
    out_file: "/var/www/codestargram/logs/out.log",
    error_file: "/var/www/codestargram/logs/error.log",
    
    env: {
      NODE_ENV: "production",
      PORT: 3000                      // Nginx upstream과 일치
    },
    env_development: {
      NODE_ENV: "development",
      PORT: 3000
    }
  }],
};

3) 배포 스크립트 작성 과정

1. 빌드

정적 컨텐츠를 Nginx를 통해 처리한다고 했습니다. 정적 컨텐츠 산출물을 위해 Vite의 빌드 과정이 필요합니다.
Vite는 public/ 하위 파일을 그대로 dist/로 복사하고, 루트에 있는 index.html을 도입점으로 하여 dist/에 AST 기반 최적화를 통해 빌드 산출물을 생성합니다.
이걸 서버에서 사용할 수 있게끔 해야하기 때문에 코드를 서버로 전달해야합니다.

2. 코드 배포 방식

처음에는 코드 배포를 어떻게 할지부터 고민했습니다.
맥에 있는 프로젝트를 우분투 서버에서 배포 스크립트(deploy.sh)로 돌려야 했기 때문에 아래의 두 가지 방식 중에 고민했습니다.

옵션 A) Git 기반 (반복 배포용)

  • 서버가 Git clone으로 코드를 가져오는 구조
  • 로컬에서 푸시 → 서버에서 deploy.sh 실행만 하면 됨. (또는 GitHub Actions로 자동 실행)

옵션 B) rsync로 맥 → 서버 직접 동기화(즉시 적용용)

  • 맥의 폴더를 서버로 복사하는 구조
  • deploy.sh의 “git 변경 감지” 부분은 맞지 않으므로 건너뛰거나 비활성화해야 함.

옵션 A 선택

배포 스크립트가 git fetch / rev-parse를 쓰기 때문에, Git을 사용하는 구조가 깔끔합니다.
또한, 운영 환경에서는 항상 Git 저장소와 동기화된 코드로 빌드하는 게 가장 안전하다고 판단했습니다.
그래서 “로컬에서 코드 수정 → GitHub에 push → 서버에서 pull 후 빌드” 흐름을 기본 원칙으로 잡았습니다.

3. 배포 스크립트 작성

매번 수동으로 git pull, npm install, npm run build, pm2 restart 하는 건 번거롭고, 실수하기 쉽습니다.
그래서 이 과정을 하나로 묶은 자동화 스크립트(deploy.sh)를 만들기로 했습니다.

정리하면, “Git → 빌드 → 배포 → PM2 재시작 → 헬스체크 → 롤백 → Nginx 리로드” 전체 과정을 자동화한 파일입니다.

서버에서 직접 스크립트 파일을 만들어서 작성해도 되지만, 개발 편의성을 위해 프로젝트에서 스크립트를 작성하고 맥(로컬) 환경에 있는 배포 스크립트를 서버로 업로드 해주었습니다.

deploy.sh

#!/usr/bin/env bash
set -euo pipefail

# ==========================
# 고정 설정
# ==========================
APP_NAME="codestargram2"
REPO_DIR="/var/www/codestargram/repo/web-p3-codestargram2"   # Git 작업 디렉토리
STATIC_DST="/var/www/codestargram/repo/web-p3-codestargram2/dist"    # Nginx 정적 자산 경로
HEALTH_URL="http://127.0.0.1:3000/health"                    # 앱 헬스체크 엔드포인트
RELEASES_DIR="/var/www/codestargram/releases"                # 빌드 산출물 보관
KEEP_RELEASES=5                                              # 보관 개수
NODE_ENV="production"

# 배포 브랜치
BRANCH="deploy"

# ==========================
# 유틸 함수
# ==========================
log() { printf "\033[1;36m[deploy]\033[0m %s\n" "$*"; }
err() { printf "\033[1;31m[error]\033[0m %s\n" "$*" >&2; }
die() { err "$*"; exit 1; }
need() { command -v "$1" >/dev/null 2>&1 || die "필요 명령어가 없습니다: $1"; }

# ==========================
# 전제 체크
# ==========================
need git; need npm; need pm2; need curl; need rsync
mkdir -p "$STATIC_DST" "$RELEASES_DIR"

[ -d "$REPO_DIR/.git" ] || die "REPO_DIR 경로가 Git repo가 아닙니다: $REPO_DIR"

log "USING REPO_DIR=$REPO_DIR, BRANCH=$BRANCH"
cd "$REPO_DIR"

# ==========================
# Git 동기화
# ==========================
log "원격 브랜치 존재 확인: origin/$BRANCH"
git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null \
  || die "원격에 브랜치가 없습니다: origin/$BRANCH"

log "git fetch origin $BRANCH"
git fetch origin "$BRANCH"

LOCAL=$(git rev-parse HEAD || echo "UNKNOWN")
REMOTE=$(git rev-parse "origin/$BRANCH")

if [ "$LOCAL" = "$REMOTE" ]; then
  log "변경 사항 없음 → 종료"
  exit 0
fi

log "변경 사항 감지됨 (local=$LOCAL, remote=$REMOTE) → 원격 상태로 맞춤"
git reset --hard "origin/$BRANCH"

# ==========================
# 빌드
# ==========================
log "의존성 설치 (빌드용: dev 포함)"
if [ -f package-lock.json ]; then
  npm ci
else
  npm install
fi

log "빌드 실행"
export NODE_ENV="$NODE_ENV"
npm run build

# (선택) 런타임 최적화: dev 제거
log "런타임 최적화: devDependencies 제거"
npm prune --omit=dev


[ -d "dist" ] || die "dist 디렉토리가 없습니다. 빌드 스크립트를 확인하세요."

# ==========================
# 릴리스 보관 + 정적 자산 배포
# ==========================
TS=$(date +"%Y%m%d-%H%M%S")
RELEASE_DIR="$RELEASES_DIR/$TS"
log "릴리스 보관 디렉토리: $RELEASE_DIR"
mkdir -p "$RELEASE_DIR"
cp -R dist/* "$RELEASE_DIR/"

log "현재 정적 자산 백업 및 교체"
mkdir -p "$STATIC_DST/.backup"
if [ -n "$(ls -A "$STATIC_DST" 2>/dev/null || true)" ]; then
  rsync -a --delete "$STATIC_DST/" "$STATIC_DST/.backup/$TS/"
fi
rsync -a --delete "$RELEASE_DIR/" "$STATIC_DST/"

# 오래된 릴리스 정리
log "오래된 릴리스 정리(최신 ${KEEP_RELEASES}개만 유지)"
ls -1t "$RELEASES_DIR" | tail -n +$((KEEP_RELEASES+1)) | while read -r old; do
  rm -rf "$RELEASES_DIR/$old"
done || true

# ==========================
# PM2 무중단 재시작
# ==========================
log "PM2 startOrReload (ecosystem.config.js)"
pm2 startOrReload ecosystem.config.cjs --update-env || {
  err "ecosystem.config.cjs 실행 실패 → 단일 스크립트로 재시도"
  pm2 restart "$APP_NAME" || pm2 start server/app.js --name "$APP_NAME"
}

# ==========================
# 헬스체크
# ==========================
log "헬스체크: $HEALTH_URL"
RETRY=20
SLEEP=1
ok=false
for i in $(seq 1 $RETRY); do
  if curl -fsS "$HEALTH_URL" >/dev/null 2>&1; then
    ok=true; break
  fi
  sleep "$SLEEP"
done

if [ "$ok" != true ]; then
  err "헬스체크 실패! 롤백 수행"
  if [ -d "$STATIC_DST/.backup/$TS" ]; then
    rsync -a --delete "$STATIC_DST/.backup/$TS/" "$STATIC_DST/"
  fi
  pm2 reload "$APP_NAME" || true
  die "배포 실패(헬스체크 불통). 롤백 완료."
fi

# ==========================
# Nginx 리로드(선택)
# ==========================
if command -v nginx >/dev/null 2>&1; then
  log "Nginx 설정 테스트"
  if nginx -t; then
    log "Nginx 리로드"
    nginx -s reload || systemctl reload nginx || true
  else
    err "Nginx 설정 테스트 실패(무시하고 진행)"
  fi
fi

log "✅ 배포 완료: $TS"

📌 deploy.sh 스크립트 요약

1. 배포 스크립트를 진행할 브랜치를 deploy로 설정했어요.
따로 배포 전용 브랜치를 만들어서 하는 것을 추천합니다.

2. Git 동기화 부분을 넣었어요.
스크립트 초반에는 항상 Git 원격 브랜치와 맞추는 절차를 넣었습니다.

  • git fetch 로 원격 최신 상태 가져오기
  • git rev-parse 로 로컬/원격 해시 비교
  • 해시 값이 다르면 git reset --hard origin/<branch> 로 맞췄습니다.
    • reset --hard 말고 git merge로 해도 되지만, 서버에서 실수로 수정 후 커밋된게 있으면 충돌이 발생할 수 있고, 수동 머지를 해야합니다. 결과적으로 배포 자동화가 깨지고, 사람이 개입해야 합니다.
    • reset --hard을 사용하면 현재 작업 트리를 원격 브랜치와 100% 동일하게 만들기 때문에 항상 깔끔한 상태에서 빌드를 보장할 수가 있습니다.

3. 빌드 & 배포 과정을 자동화했어요.
Git 동기화가 끝나면 바로 의존성 설치 후 빌드를 진행합니다.

  • npm ci 또는 npm install 로 의존성 설치
  • npm run build 로 빌드
  • 산출물(dist/)을 /var/www/codestargram/releases 에 타임스탬프별로 보관
  • 최신 결과물을 /var/www/codestargram/repo/web-p3-codestargram2/dist 로 덮어쓰기

그리고 오래된 릴리스는 자동으로 정리해서, 서버 공간을 아끼도록 했습니다.

4. 서버 프로세스 재시작을 PM2로 처리했어요.
빌드된 코드가 준비되면 배포 스크립트와 pm2 연동을 진행합니다.

  • pm2 startOrReload ecosystem.config.cjs 실행
    • 최초 실행 시 → PM2가 새 앱을 띄우고
    • 이후 실행 시 → 무중단 reload가 됨
  • 만약 설정 파일이 없다면, server/app.js를 직접 PM2로 띄우도록 예외 처리
  • 이렇게 하면 Node.js 앱이 항상 살아 있고, 서비스가 끊기지 않습니다.

5. 헬스체크와 롤백을 넣었어요.
단순히 재시작만 하면 끝이 아니라, 정말 서버가 잘 떴는지 확인해야 합니다.

  • curl 로 http://127.0.0.1:3000/health 요청
  • 실패하면 → 직전에 백업해둔 정적 자산을 다시 복원
  • 그리고 PM2를 이전 상태로 돌려서, 사용자가 다운타임을 겪지 않게 했습니다.

6. 마지막으로 Nginx 설정 리로드까지
정적 자산을 /var/www/codestargram/repo/web-p3-codestargram2/dist 에 새로 배치했으니, 필요할 때 Nginx도 리로드하게 했습니다.

  • nginx -t 로 설정 확인 후 nginx -s reload

4. 서버에 배포 스크립트 설정

1. 배포 스크립트를 설정을 위한 서버 준비를 1회 진행해줍니다.

ssh kimdayeon@172.30.1.86

# 디렉터리 준비
sudo mkdir -p /var/www/codestargram/{repo,releases,scripts,logs}
sudo chown -R $USER:$USER /var/www/codestargram

# 코드 클론 (GitHub)
cd /var/www/codestargram/repo
git clone https://github.com/<YOU>/web-p3-codestargram2 .   # 또는 SSH URL
git checkout deploy  # 배포 브랜치

# PM2 설정 파일을 repo 루트에 둠 (ecosystem.config.js)
# deploy.sh는 /var/www/codestargram/scripts/deploy.sh 에 둘 것

서버에서 혼란이 없도록, 디렉토리 구조와 경로를 역할에 따라 생성했습니다.

  • repo: 서버에서 git clone한 결과물
    • /repo/ecosystem.config.cjs으로 pm2 설정
    • /repo/web-p3-codestargram2/dist에서 Nginx로 정적 파일 서빙
  • releases: 릴리즈 보관용
  • scripts: 배포 스크립트
  • logs: 로그 기록

이렇게 정리하면, 파일이 어디에 있는지 바로 파악이 가능합니다.

2. 맥(로컬) 환경에 있는 배포 스크립트를 서버로 업로드해주고, 실행권한 설정을 해줍니다.

# 맥에서
scp /경로/deploy.sh kimdayeon@172.30.1.86:/var/www/codestargram/scripts/deploy.sh
ssh kimdayeon@172.30.1.86 "chmod +x /var/www/codestargram/scripts/deploy.sh \"

3. 서버에 패키지 설치가 안되어있다면 해줍니다.

# 패키지 업데이트
sudo apt update

# curl 설치 (없다면)
sudo apt install -y curl

# Node.js 20.x 설치 (LTS 권장)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# 설치 확인
node -v
npm -v

# pm2 설치 및 확인
sudo npm install -g pm2
pm2 -v

✅ 주요 체크 포인트

  • /var/www/codestargram/repo/web-p3-codestargram2 에 실제 레포가 클론되어 있어야 합니다. (.git 존재)
  • ecosystem.config.cjs 는 레포 루트에 위치해야 합니다.(없으면 PM2가 server/app.js로 fallback)
  • 관련 패키지가 서버에 설치되어있어야 합니다.

5. 배포 스크립트 실행 및 결과

체크 포인트를 확인했다면 스크립트를 실행합니다.

/var/www/codestargram/scripts/deploy.sh

변경사항이 있을 시:
deploy로 PR 생성/머지 후 스크립트 실행

변경사항이 없을 시:
변경사항 없음 -> 종료

배포 결과물

서버 주소에서 배포된 코드가 실행된 걸 확인할 수 있었습니다.

0개의 댓글