TELEPASI

검색하기 전에 통하다

SvelteKit(adapter-node)을 한 서버에 안정적으로 배포하기

강병우
2026.07.14 👁️ 79

버전 올리고 → 빌드하고 → rsync로 올리고 → PM2로 재기동하는 실무 흐름
아래는 versionUp.sh · frontDeploy.sh 패턴을 example.com 기준 예시로 정리한 글입니다.
도메인·계정·IP·포트는 공개용 샘플이며, 운영 환경에 맞게 바꿔 쓰면 됩니다.

이런 글을 쓰게 된 이유

SvelteKit은 Vercel 같은 플랫폼에 올리면 설정이 거의 필요 없습니다.
하지만 자기 VPS / 전용 서버에 올리고, 한 대의 머신에 여러 Svelte 웹을 돌리려면 이야기가 달라집니다.

  • 빌드 결과물을 어디에 둘지
  • Nginx는 어떤 포트로 프록시할지
  • PM2 프로세스 이름은 어떻게 구분할지
  • .env는 배포 때 지우면 안 되는데 어떻게 보존할지

이 글에서는 @sveltejs/adapter-node로 Node 서버를 만들고,
versionUp.sh로 버전·빌드를 맞춘 뒤 frontDeploy.shrsync + PM2 reload 하는 흐름을 풀어 씁니다.

공개용 예시 값

  • 도메인: example.com
  • SSH 계정: deploy
  • SSH 포트: 2222
  • 서버 IP: 203.0.113.10 (문서용 IP, RFC 5737)
  • Node 앱 포트: 3000, 3001, 3002

전체 그림

[개발자 PC]
  versionUp.sh
    → package.json / prjConst / service-worker 버전 ↑
    → yarn build  (→ ./build)
    → git commit "Version x.y.z"

  frontDeploy.sh [service|기본]
    → rsync ./build + package.json → 서버 /var/www/example.com/web
    → SSH: npm install --production
    → SSH: PORT=xxxx pm2 reload 앱이름
              ↑
[서버] Nginx :443 → 127.0.0.1:PORT (앱별 포트)

한 줄로 말하면:

로컬에서 산출물(build)을 만들고, 서버에는 산출물만 올리며, 실행은 PM2 + PORT로 분리한다.


1. 왜 adapter-node인가

svelte.config.js에서 Node 어댑터를 씁니다.

import adapter from '@sveltejs/adapter-node';

const config = {
  kit: {
    adapter: adapter({
      precompress: true, // .gz / .br 사전 압축
      strict: true
    })
    // ...
  }
};

빌드(yarn build)가 끝나면 ./build 아래에 대략 다음이 생깁니다.

경로 역할
build/index.js Node HTTP 서버 엔트리
build/client/ 브라우저 정적 자산
build/server/ SSR용 서버 번들

서버에서는 이 index.js를 PM2가 실행합니다.
어댑터 특성상 리슨 포트는 process.env.PORT를 따릅니다.
그래서 PM2를 올릴 때 PORT를 명시하지 않으면 다른 앱과 포트가 겹치거나, 기대한 포트와 어긋날 수 있습니다.

팁: npm start 스크립트에 PORT=3000을 넣어 두어도,
pm2 start index.js처럼 스크립트를 거치지 않고 직접 실행하면 그 PORT가 적용되지 않습니다.
아래 배포 스크립트는 SSH 안에서 export PORT=3000pm2 reload 하는 방식으로 이 함정을 피합니다.


2. 버전을 올리고 빌드하기 — versionUp.sh

배포 전에 “지금 올린 게 몇 번인지”를 코드·캐시·푸터에 맞춰 두는 것이 좋습니다.
versionUp.sh가 하는 일은 단순하지만 빠뜨리기 쉬운 것들을 한 번에 처리합니다.

2-1. 무엇을 올리나

현재 버전이 1.0.9라면 patch(C)만 +1 → 1.0.10.

동시에 갱신하는 파일:

  1. package.json — npm/yarn 기준 버전
  2. src/prj/prjConst.jsVERSION, RELEASE_DATE(오늘 날짜) — 사이트 푸터·관리 화면 표시용
  3. static/service-worker.jsCACHE_VERSION — PWA 캐시 깨기

그다음:

yarn build
git add package.json src/prj/prjConst.js static/service-worker.js build/
git commit -m "Version 1.0.10"

2-2. 사용법

chmod +x versionUp.sh
./versionUp.sh

성공하면 로컬에 새 버전 커밋과 ./build가 준비된 상태입니다.
(원격 push는 스크립트가 강제하지 않습니다. 필요할 때 git push.)

2-3. 스크립트 전문 — versionUp.sh

아래는 전체 스크립트 샘플입니다. 프로젝트 루트에 두고 실행하면 됩니다.

#!/bin/bash

# 색상 정의
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}  Version Up Script${NC}"
echo -e "${GREEN}========================================${NC}"

# 현재 버전 읽기
CURRENT_VERSION=$(node -p "require('./package.json').version")
echo -e "${YELLOW}현재 버전: ${CURRENT_VERSION}${NC}"

# 버전 분리 (A.B.C)
IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_VERSION"
A="${VERSION_PARTS[0]}"
B="${VERSION_PARTS[1]}"
C="${VERSION_PARTS[2]}"

# C 값 1 증가 (patch)
NEW_C=$((C + 1))
NEW_VERSION="${A}.${B}.${NEW_C}"

echo -e "${GREEN}새 버전: ${NEW_VERSION}${NC}"

# 오늘 날짜 (YYYY-MM-DD)
TODAY=$(date +%Y-%m-%d)

# package.json 버전 업데이트
echo -e "${YELLOW}package.json 업데이트 중...${NC}"
if [[ "$OSTYPE" == "darwin"* ]]; then
  # macOS
  sed -i '' "s/\"version\": \"$CURRENT_VERSION\"/\"version\": \"$NEW_VERSION\"/" package.json
else
  # Linux
  sed -i "s/\"version\": \"$CURRENT_VERSION\"/\"version\": \"$NEW_VERSION\"/" package.json
fi

# prj/prjConst.js 업데이트 (푸터 표기용)
if [ -f "src/prj/prjConst.js" ]; then
  echo -e "${YELLOW}prj/prjConst.js 업데이트 중...${NC}"
  if [[ "$OSTYPE" == "darwin"* ]]; then
    sed -i '' "s/VERSION: '[^']*'/VERSION: '$NEW_VERSION'/" src/prj/prjConst.js
    sed -i '' "s/RELEASE_DATE: '[^']*'/RELEASE_DATE: '$TODAY'/" src/prj/prjConst.js
  else
    sed -i "s/VERSION: '[^']*'/VERSION: '$NEW_VERSION'/" src/prj/prjConst.js
    sed -i "s/RELEASE_DATE: '[^']*'/RELEASE_DATE: '$TODAY'/" src/prj/prjConst.js
  fi
fi

# service-worker.js 업데이트 (PWA 캐시 버전)
if [ -f "static/service-worker.js" ]; then
  echo -e "${YELLOW}service-worker.js 업데이트 중...${NC}"
  if [[ "$OSTYPE" == "darwin"* ]]; then
    sed -i '' "s/const CACHE_VERSION = '[^']*'/const CACHE_VERSION = '$NEW_VERSION'/" static/service-worker.js
  else
    sed -i "s/const CACHE_VERSION = '[^']*'/const CACHE_VERSION = '$NEW_VERSION'/" static/service-worker.js
  fi
fi

# yarn build 실행
echo -e "${YELLOW}yarn build 실행 중...${NC}"
yarn build

if [ $? -ne 0 ]; then
  echo -e "${RED}빌드 실패!${NC}"
  exit 1
fi

# Git commit
echo -e "${YELLOW}Git commit 생성 중...${NC}"
git add package.json src/prj/prjConst.js static/service-worker.js build/
git commit -m "Version ${NEW_VERSION}"

if [ $? -eq 0 ]; then
  echo -e "${GREEN}========================================${NC}"
  echo -e "${GREEN}  버전 업데이트 완료!${NC}"
  echo -e "${GREEN}  새 버전: ${NEW_VERSION}${NC}"
  echo -e "${GREEN}  릴리즈 날짜: ${TODAY}${NC}"
  echo -e "${GREEN}========================================${NC}"
  echo -e "${YELLOW}참고: 원격 저장소에 푸시하려면 'git push origin main'을 실행하세요.${NC}"
else
  echo -e "${RED}커밋 실패!${NC}"
  exit 1
fi

prjConst.js / service-worker.js 경로는 프로젝트에 맞게 조정하세요.
macOS는 sed -i '', Linux는 sed -i 문법이 달라서 OS 분기가 들어 있습니다.

2-4. 왜 빌드 산출물을 커밋하기도 하나

팀·환경에 따라 build/.gitignore 하는 경우도 많습니다.
이 흐름에서는 버전 업 직후 frontDeploy가 ./build/를 그대로 rsync 하므로,
“방금 올린 Version 커밋 = 방금 빌드한 산출물”이 맞물리게 두는 편이 운영이 단순합니다.

서버에서 yarn build를 다시 돌리지 않는 철학입니다.
서버는 의존성 설치 + 프로세스 재시작만 담당합니다.


3. rsync로 올리기 — frontDeploy.sh

3-1. 한 줄 개념

rsync -avz --delete -e "ssh -p 2222" \
  --exclude '.env' \
  --exclude 'node_modules/' \
  --exclude 'uploads/' \
  ./build/ \
  ./package.json \
  deploy@203.0.113.10:/var/www/example.com/web

의미:

옵션 의미
-avz 아카이브, 진행 표시, 압축 전송
--delete 로컬에 없는 파일은 서버에서 제거 (옛 해시 청크 정리에 중요)
-e "ssh -p 2222" 비표준 SSH 포트 (예시)
--exclude '.env' 서버에만 있는 비밀값 보존
--exclude 'node_modules/' 서버에서 npm install로 맞춤
--exclude 'uploads/' 업로드 파일은 배포로 덮어쓰지 않음
./build/ adapter-node 산출물 내용물을 목적지에 펼침
./package.json 서버 production 설치용

3-2. 사용법

chmod +x frontDeploy.sh

# 개발 타깃 (스크립트 기본)
./frontDeploy.sh

# 서비스(운영) 타깃
./frontDeploy.sh service

인자를 나누어 두면 로그·습관·향후 경로/IP 분리가 쉬워집니다.
다른 프로젝트는 TARGET_NAME / APP_NAME / WEB_PATH / APP_PORT만 바꾸면 같은 패턴을 재사용합니다.

3-3. 스크립트 전문 — frontDeploy.sh

아래는 배포 스크립트 샘플입니다.
주석까지 포함해 전부 예시 값이며, 실제 운영 값으로 치환해 사용하세요.

#!/bin/bash

# 목표 앱 설정 (예시)
# 다른 사이트를 배포할 때는 아래 두 줄만 바꿔도 됩니다.
# TARGET_NAME="app2.example.com"
# APP_NAME="app2.example.com"
TARGET_NAME="example.com"
APP_NAME="example.com"

# 기본값 (dev)
USER="deploy"                       # SSH 로그인 계정 (예시)
TARGET="dev"
SERVER_IP="YOUR_DEV_SERVER_IP"           # 서버 IP (문서용 예시 IP)
WEB_PATH="/var/www/$TARGET_NAME/web"
SSHPORT="ssh -p 2222"               # SSH 포트 (예시)
APP_PORT="3000"                     # 이 앱 전용 Node 포트 (앱마다 다르게)

# 서비스(운영) 타깃 — 파라미터가 'service'일 때
if [ "$1" == "service" ]; then
    USER="deploy"
    TARGET="service"
    SERVER_IP="YOUR_SERVICE_SERVER_IP"       # 운영 서버가 다르면 여기서 변경
    # WEB_PATH="/var/www/$TARGET_NAME/web"
    # APP_PORT="3000"
fi

# 최종 목적지 주소 조합
DESTINATION="$USER@$SERVER_IP:$WEB_PATH"

# --------------------------------
# 배포 실행
# --------------------------------

echo "🚀 Deploying to $TARGET ($SERVER_IP)..."

# 1. 빌드 폴더의 '내용물' 전송
# 2. package.json 전송 (서버에서 npm install 하기 위함)
# 3. --exclude 설정을 통해 서버의 중요 파일 보존
rsync -avz --delete \
    -e "$SSHPORT" \
    --exclude '.env' \
    --exclude 'node_modules/' \
    --exclude 'uploads/' \
    ./build/ \
    ./package.json \
    "$DESTINATION"

echo "✅ $TARGET_NAME($TARGET) Transfer OK"

echo "📦 Installing dependencies and reloading PM2 on $TARGET server..."

# SSH 접속 시 NVM 경로를 명시적으로 로드합니다.
$SSHPORT "$USER@$SERVER_IP" << EOF
    echo "🔍 Loading NVM environment..."
    export NVM_DIR="\$HOME/.nvm"
    [ -s "\$NVM_DIR/nvm.sh" ] && \. "\$NVM_DIR/nvm.sh"

    echo "🔍 Changing directory to $WEB_PATH..."
    cd $WEB_PATH

    echo "🔍 Installing dependencies..."
    npm install --production

    # adapter-node는 process.env.PORT로 리슨.
    # index.js 직접 실행 시 npm start의 PORT가 적용되지 않으므로 여기서 통일.
    export PORT=$APP_PORT

    echo "🔍 Reloading PM2 (PORT=\$PORT)..."
    pm2 reload $APP_NAME --update-env || pm2 start index.js --name "$APP_NAME"

    echo "🔍 Checking PM2 status..."
    pm2 status
EOF

echo "✨ $TARGET_NAME($TARGET) Deploy & Restart Complete!"

샘플을 읽을 때 참고할 점:

  • SERVER_IP / USER / SSH 포트 / 도메인은 공개 글용 placeholder
  • APP_PORT 변수로 분리 → 한 서버·여러 앱일 때 사이트마다 3000 / 3001 … 만 바꾸면 됨
  • dev / service는 인자로 구분; 서버가 분리되면 if 블록에서 IP·경로만 다르게 지정

3-4. 전송 후 서버에서 하는 일

SSH 블록이 실제로 하는 일:

  1. NVM 로드 — 비대화형 SSH에서도 node/npm/pm2가 잡히게
  2. cd $WEB_PATH
  3. npm install --production
  4. export PORT=…
  5. pm2 reload $APP_NAME --update-env || pm2 start index.js --name "$APP_NAME"
  6. pm2 status로 확인

reload가 실패하면(처음 배포) start로 fallback 합니다.
--update-envPORT를 바꾼 뒤에도 PM2가 새 환경변수를 먹게 할 때 필요합니다.


4. 한 서버 · 여러 Svelte 앱 — PM2와 포트

한 서버에 example.com, 다른 SvelteKit 사이트, API를 같이 올리면 포트 충돌이 가장 흔한 장애입니다.

4-1. 권장 규칙

PM2 name PORT Nginx 웹 루트
example.com example.com 3000 proxy_pass http://127.0.0.1:3000 /var/www/example.com/web
app2.example.com app2.example.com 3001 …:3001 /var/www/app2.example.com/web
app3.example.com app3.example.com 3002 …:3002 /var/www/app3.example.com/web

원칙:

  1. 앱마다 PM2 이름 고유
  2. 앱마다 PORT 고유 (배포 스크립트에 두거나 ecosystem 파일로 관리)
  3. 공개 포트는 Nginx(443)만, Node는 localhost만 리슨
  4. .env는 서버에만 — rsync exclude

4-2. 왜 reload 시 PORT를 다시 export 하나

PM2가 이미 떠 있는 프로세스를 reload할 때,
예전에 다른 PORT로 시작됐거나 env가 비어 있으면 그대로 죽을 수 있습니다.
배포 스크립트마다:

export PORT=3000
pm2 reload example.com --update-env

처럼 그 앱의 포트를 명시하는 습관을 들이는 것이 안전합니다.

4-3. ecosystem 파일을 쓰는 방법(참고)

앱이 늘어나면 bash heredoc보다 ecosystem이 낫습니다.

// ecosystem.config.cjs 예시
module.exports = {
  apps: [
    {
      name: 'example.com',
      script: 'index.js',
      cwd: '/var/www/example.com/web',
      env: { PORT: 3000, NODE_ENV: 'production' }
    },
    {
      name: 'app2.example.com',
      script: 'index.js',
      cwd: '/var/www/app2.example.com/web',
      env: { PORT: 3001, NODE_ENV: 'production' }
    }
  ]
};

단일 앱이면 frontDeploy.sh처럼 인라인 PORT + reload로도 충분합니다.
규모가 커지면 ecosystem으로 옮기면 됩니다.


5. 실전 배포 체크리스트

운영에 올리기 직전:

# 1) 기능 커밋까지 정리
git status

# 2) 버전 업 + 빌드 + Version 커밋
./versionUp.sh

# 3) 운영 배포
./frontDeploy.sh service

배포 후 서버에서:

pm2 status
pm2 logs example.com --lines 50
curl -I http://127.0.0.1:3000

브라우저에서는:

  • 하드 리프레시 또는 Footer의 Version / Release date 확인
  • PWA를 쓰면 Service Worker 캐시 버전이 바뀌었는지 확인

개발 타깃만 갱신할 때는:

./versionUp.sh          # 필요할 때만
./frontDeploy.sh        # 인자 없음 = 스크립트 기본(dev) 모드

6. 자주 하는 실수

.env를 rsync로 덮어씀

--exclude '.env'를 빼먹으면 서버 키가 로컬 값으로 바뀌거나, 로컬에 파일이 없으면 삭제될 수 있습니다.
비밀은 서버에만 두고, 배포는 항상 exclude.

node_modules까지 통째로 전송

OS/아키텍처가 다르면 native 모듈이 깨집니다.
exclude 후 서버에서 npm install --production.

--delete 없이 오래 운영

해시 붙은 옛 CSS/JS가 서버에 쌓입니다.
디스크는 괜찮아 보여도 “캐시가 이상하다”는 이슈의 원인이 되기도 합니다.

adapter-node인데 pm2 start만 하고 PORT 미설정

기본 포트(예: 3000이 Nginx upstream과 다를 때)로 떠서 프록시가 안 맞거나, 다른 앱과 충돌합니다.

NVM 경로를 SSH에서 안 읽음

비대화형 셸은 .bashrc를 건너뛰는 경우가 많습니다.
배포 SSH 블록에서 NVM_DIR을 명시적으로 source 하세요.

버전만 올리고 배포를 안 함 / 배포만 하고 버전을 안 올림

Footer·SW 캐시·실제 빌드가 어긋납니다.
versionUp → frontDeploy 순서를 팀 약속으로 두면 편합니다.


7. Nginx 쪽 최소 설정(개념)

공개 HTTPS는 Nginx, 앱은 localhost PORT.

server {
  listen 443 ssl http2;
  server_name www.example.com example.com;

  # SSL 인증서 설정 …

  location / {
    proxy_pass http://127.0.0.1:3000;
    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;
  }
}

다른 Svelte 앱은 server_nameproxy_pass의 포트만 바꾸면 됩니다.


8. 정리

SvelteKit을 adapter-node로 “원할하게” 배포하려면 거창한 CD보다,
아래 네 가지만 흔들리지 않게 지키면 됩니다.

  1. 버전·빌드·캐시 버전을 versionUp.sh로 한 번에
  2. 산출물만 rsync (build + package.json, .env/node_modules/uploads 제외)
  3. 서버는 install + PM2 reload만
  4. 앱마다 PM2 이름 + PORT + Nginx upstream을 1:1로

실제 배포 명령은 짧습니다.

./versionUp.sh
./frontDeploy.sh service

같은 서버에 사이트를 하나 더 올린다면,
frontDeploy.sh를 복사해 APP_NAME · WEB_PATH · APP_PORT만 바꾸면
거의 그대로 재사용할 수 있습니다.


참고 — 이 글에서 다루는 파일

파일 역할
svelte.config.js @sveltejs/adapter-node
versionUp.sh patch 버전 ↑, 상수·SW 갱신, yarn build, Version 커밋
frontDeploy.sh rsync → npm installPORT + PM2 reload
src/prj/prjConst.js Footer 등에 노출되는 VERSION / RELEASE_DATE

공개 예시 ↔ 각자 환경

항목 이 글의 예시 값 바꾸는 곳
도메인 example.com TARGET_NAME, APP_NAME, Nginx server_name
SSH 계정 deploy USER=
SSH 포트 2222 SSHPORT="ssh -p …"
서버 IP 203.0.113.10 SERVER_IP=
Node 포트 3000 / 3001 / 3002 APP_PORT=, Nginx proxy_pass
웹 루트 /var/www/example.com/web WEB_PATH=

(위 표의 값은 모두 문서용 샘플입니다. 실제 서버 정보·계정·포트를 글에 넣지 마세요.)


작성 환경: SvelteKit 2 + adapter-node + PM2 + rsync over SSH

주파수 소통방 (0)

로딩 중...