버전 올리고 → 빌드하고 → rsync로 올리고 → PM2로 재기동하는 실무 흐름
아래는versionUp.sh·frontDeploy.sh패턴을 example.com 기준 예시로 정리한 글입니다.
도메인·계정·IP·포트는 공개용 샘플이며, 운영 환경에 맞게 바꿔 쓰면 됩니다.
이런 글을 쓰게 된 이유
SvelteKit은 Vercel 같은 플랫폼에 올리면 설정이 거의 필요 없습니다.
하지만 자기 VPS / 전용 서버에 올리고, 한 대의 머신에 여러 Svelte 웹을 돌리려면 이야기가 달라집니다.
- 빌드 결과물을 어디에 둘지
- Nginx는 어떤 포트로 프록시할지
- PM2 프로세스 이름은 어떻게 구분할지
.env는 배포 때 지우면 안 되는데 어떻게 보존할지
이 글에서는 @sveltejs/adapter-node로 Node 서버를 만들고,versionUp.sh로 버전·빌드를 맞춘 뒤 frontDeploy.sh로 rsync + 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=3000후pm2 reload하는 방식으로 이 함정을 피합니다.
2. 버전을 올리고 빌드하기 — versionUp.sh
배포 전에 “지금 올린 게 몇 번인지”를 코드·캐시·푸터에 맞춰 두는 것이 좋습니다.versionUp.sh가 하는 일은 단순하지만 빠뜨리기 쉬운 것들을 한 번에 처리합니다.
2-1. 무엇을 올리나
현재 버전이 1.0.9라면 patch(C)만 +1 → 1.0.10.
동시에 갱신하는 파일:
package.json— npm/yarn 기준 버전src/prj/prjConst.js—VERSION,RELEASE_DATE(오늘 날짜) — 사이트 푸터·관리 화면 표시용static/service-worker.js—CACHE_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 포트 / 도메인은 공개 글용 placeholderAPP_PORT변수로 분리 → 한 서버·여러 앱일 때 사이트마다3000/3001… 만 바꾸면 됨dev/service는 인자로 구분; 서버가 분리되면if블록에서 IP·경로만 다르게 지정
3-4. 전송 후 서버에서 하는 일
SSH 블록이 실제로 하는 일:
- NVM 로드 — 비대화형 SSH에서도
node/npm/pm2가 잡히게 cd $WEB_PATHnpm install --productionexport PORT=…pm2 reload $APP_NAME --update-env || pm2 start index.js --name "$APP_NAME"pm2 status로 확인
reload가 실패하면(처음 배포) start로 fallback 합니다.--update-env는 PORT를 바꾼 뒤에도 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 |
원칙:
- 앱마다 PM2 이름 고유
- 앱마다 PORT 고유 (배포 스크립트에 두거나 ecosystem 파일로 관리)
- 공개 포트는 Nginx(443)만, Node는 localhost만 리슨
.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_name과 proxy_pass의 포트만 바꾸면 됩니다.
8. 정리
SvelteKit을 adapter-node로 “원할하게” 배포하려면 거창한 CD보다,
아래 네 가지만 흔들리지 않게 지키면 됩니다.
- 버전·빌드·캐시 버전을
versionUp.sh로 한 번에 - 산출물만 rsync (
build+package.json,.env/node_modules/uploads제외) - 서버는 install + PM2 reload만
- 앱마다 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 install → PORT + 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)
로딩 중...