Deploying SvelteKit on a VPS: The Setup I Now Use for Every Project
Vercel is great until you need persistent storage, custom cron jobs, or a fixed monthly cost. This is the Nginx + PM2 + GitHub Actions setup I landed on for Dharentrans and now use as my default deployment stack.
After shipping several projects on Vercel, there comes a time when you need something more — persistent storage, cron jobs, and a fixed monthly cost. That’s what ultimately pushed me toward a VPS for projects like Dharentrans.com.
This is the setup I now use as a default template for every SvelteKit project that needs SSR.
The Stack
- SvelteKit with
adapter-node - Nginx as a reverse proxy
- PM2 for process management
- GitHub Actions for automated CI/CD
- VPS: Ubuntu 22.04 (Hetzner CPX11, €4.35/month)
1. SvelteKit Setup with adapter-node
First, make sure you’re using adapter-node, not adapter-auto:
npm i -D @sveltejs/adapter-node
Update svelte.config.js:
import adapter from "@sveltejs/adapter-node";
export default {
kit: {
adapter: adapter({
out: "build",
}),
},
};
npm run build
# Output: /build folder
node build
# Server running on port 3000
2. VPS Setup (Ubuntu 22.04)
Log into the VPS, install Node.js and PM2:
# Install Node.js via nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.bashrc
nvm install 20
node -v # v20.x.x
# Install PM2 globally
npm install -g pm2
3. Nginx Configuration
Create a config for your domain:
sudo nano /etc/nginx/sites-available/dharentrans.com
server {
listen 80;
server_name dharentrans.com www.dharentrans.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache_bypass $http_upgrade;
}
}
sudo ln -s /etc/nginx/sites-available/dharentrans.com /etc/nginx/sites-enabled/
sudo nginx -t # Test config
sudo systemctl reload nginx
Enable HTTPS with Certbot:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d dharentrans.com -d www.dharentrans.com
4. PM2 Ecosystem File
Create ecosystem.config.cjs in the project root:
module.exports = {
apps: [
{
name: "dharentrans",
script: "build/index.js",
env: {
NODE_ENV: "production",
PORT: 3000,
ORIGIN: "https://dharentrans.com",
},
},
],
};
pm2 start ecosystem.config.cjs
pm2 save # Save process list
pm2 startup # Auto-start on reboot
5. CI/CD with GitHub Actions
Create .github/workflows/deploy.yml:
name: Deploy to VPS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install & Build
run: |
npm ci
npm run build
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /var/www/dharentrans
git pull origin main
npm ci --omit=dev
npm run build
pm2 restart dharentrans
Add secrets in your GitHub repository settings:
VPS_HOST— your VPS IPVPS_USER— username (usuallyrootorubuntu)VPS_SSH_KEY— private SSH key
Extra Tips
Swap space — Hetzner doesn’t enable swap by default. If you frequently run out of memory during builds:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
Environment variables — don’t hardcode in your codebase, store in .env on the server:
nano /var/www/dharentrans/.env
DATABASE_URL=mongodb://localhost:27017/dharentrans
MIDTRANS_SERVER_KEY=your-key
Conclusion
I’ve been running this setup on Dharentrans since October 2025 and it’s been rock solid. Total cost: €4.35/month for a Hetzner VPS — far cheaper than Vercel Pro once traffic starts climbing.
For small-to-medium projects, this is more than enough.
Loading comments...