Deploy SvelteKit on Hostinger VPS: PM2, Nginx, Namecheap DNS & CDN

by Fahim

Serverless runtimes are great until you hit memory limits, random function timeouts, or an unexpected bill. Running SvelteKit on a small Linux VPS gives you persistent connections, predictable pricing, and full control over background jobs. In this guide, we’ll build a standalone Node.js production release of SvelteKit and run it on a Hostinger VPS with PM2 for uptime, Nginx as a reverse proxy, and custom Namecheap DNS records.

I moved one of my internal dashboards to this exact setup after hitting serverless memory limits on heavy aggregation queries. If you’ve already walked through our guides to deploy Next.js on Hostinger VPS or Remix deployments, this flow will feel right at home.

Close-up of server hardware running a Node.js process terminal for SvelteKit deployment
Close-up of server hardware running a Node.js process terminal for SvelteKit deployment

Step 1: Switch SvelteKit to Node Adapter

Fresh SvelteKit projects ship with adapter-auto. That works fine on serverless platforms, but on a VPS we need a dedicated Node server. We’ll swap it for @sveltejs/adapter-node.

In your local project root, install the adapter:

npm install -D @sveltejs/adapter-node
npm uninstall @sveltejs/adapter-auto

Then open your svelte.config.js file and update the import:

import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */
const config = { preprocess: vitePreprocess(), kit: { adapter: adapter({ out: 'build', precompress: true, envPrefix: '' }) }
}; export default config;

I always set precompress: true here. Vite will generate precompressed .gz and .br files for static assets during the build. Nginx can serve those directly off the disk without burning CPU cycles compressing them on the fly. Check the official SvelteKit adapter-node documentation for more build flags.

Step 2: Prepare the Hostinger VPS Environment

SSH into your VPS. We’ll pull package updates and install Node.js 20 LTS from NodeSource:

sudo apt update && sudo apt upgrade -y
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs nginx git
node -v
npm -v

Make sure node -v prints v20.x or higher. Next, install PM2 globally so your app stays running across crashes and reboots:

sudo npm install -g pm2

Create a directory for your application under /var/www/:

sudo mkdir -p /var/www/svelte-app
sudo chown -R $USER:$USER /var/www/svelte-app

Step 3: Build and Deploy the SvelteKit App

You can push your local build artifacts over rsync or clone your repository directly onto the server. If your VPS has at least 2GB of RAM, building on the server is straightforward:

cd /var/www/svelte-app
git clone https://github.com/your-username/your-repo.git .
npm ci
npm run build

Once npm run build finishes, you’ll have a build directory containing index.js, an env.js helper, and your static assets.

Next, create a PM2 config file named ecosystem.config.cjs in the root of your project:

module.exports = { apps: [ { name: 'sveltekit-app', script: 'build/index.js', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production', PORT: 3000, HOST: '127.0.0.1', ORIGIN: 'https://example.com' } } ]
};

Pay close attention to that ORIGIN variable. SvelteKit checks it for Cross-Site Request Forgery (CSRF) protection on form actions. If you forget to declare ORIGIN, form submissions via use:enhance or native POST requests will fail with a 403 Forbidden error.

Start your app with PM2 and generate the systemd startup hook:

pm2 start ecosystem.config.cjs
pm2 save
pm2 startup

Run the command that pm2 startup prints in your terminal so PM2 automatically restarts when the server boots.

Step 4: Configure Nginx as a Reverse Proxy

You don’t want users hitting port 3000 directly. Nginx will handle incoming HTTP/HTTPS traffic, serve your static files straight from disk, and forward dynamic SSR requests to Node.

Create a new server block file:

sudo nano /etc/nginx/sites-available/svelte-app

Paste in this configuration:

server { listen 80; listen [::]:80; server_name example.com www.example.com; # Serve immutable static client assets directly location /_app/immutable/ { alias /var/www/svelte-app/build/client/_app/immutable/; access_log off; expires 1y; add_header Cache-Control "public, max-age=31536000, immutable"; try_files $uri =404; } # Reverse proxy for SSR and dynamic routes location / { proxy_pass http://127.0.0.1:3000; 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; 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; }
}

Enable the site, test the configuration syntax, and reload Nginx:

sudo ln -s /etc/nginx/sites-available/svelte-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

If the default Nginx welcome page is still enabled, run sudo rm /etc/nginx/sites-enabled/default before reloading so it doesn’t conflict on port 80.

Step 5: Configure Namecheap DNS and SSL

Head to your Namecheap account, open your Domain List, hit Manage next to your domain, and switch to the Advanced DNS tab.

Add these records pointing to your Hostinger VPS IP address (swap in your actual server IP for 192.0.2.1):

  • A Record: Host: @ | Value: 192.0.2.1 | TTL: Automatic
  • CNAME Record: Host: www | Value: example.com. | TTL: Automatic

If you’re using a subdomain instead, follow our tutorial on creating a subdomain on Hostinger with Namecheap DNS. Once DNS propagates, secure the site with a free Let’s Encrypt certificate via Certbot:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

Certbot will update your Nginx config and configure auto-renewals. If you run into renewal issues later down the road, see our troubleshooting guide on fixing Certbot SSL auto-renewal failures on Nginx.

Step 6: Edge CDN Caching Configuration

Putting a CDN in front of your VPS saves server bandwidth and speeds up page loads worldwide. SvelteKit uses content hashes for everything under /_app/immutable/, so those files can safely be cached at the edge for an entire year.

Here is how I set up edge rules:

  • Static Assets: Route /_app/immutable/* → set Edge Cache TTL to 1 Year (31,536,000 seconds).
  • HTML SSR Pages: Set Edge Cache TTL to bypass or a very low TTL (0 to 60 seconds) depending on how often data changes.
  • API Endpoints: Always bypass the edge cache for /api/* and any routes that rely on cookies or session headers.

If your CDN provider doesn’t respect your headers out of the box, check out our guide on how to override origin Cache-Control headers with CDN edge rules.

What I Ran and the Gotcha I Fixed

The first time I deployed this, static pages and client navigation worked without a problem. But the second I submitted a login form, the browser threw an immediate HTTP 403 error:

Cross-site POST form submissions are forbidden

This happens because SvelteKit compares the request’s incoming origin header against the expected host. If you’re behind Nginx and didn’t forward the proxy headers—or if ORIGIN isn’t set in the Node environment—SvelteKit assumes it’s a CSRF attack and rejects the POST request.

To fix it, make sure two things are in place.

First, verify your ecosystem.config.cjs has the full URL set for ORIGIN:

env: { NODE_ENV: 'production', PORT: 3000, ORIGIN: 'https://example.com'
}

Second, ensure Nginx forwards the host and protocol headers inside your location / block:

proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

Reload both services to apply the fixes:

pm2 restart sveltekit-app --update-env
sudo systemctl reload nginx

After that, the 403 disappeared and forms submitted normally. Take a look at the MDN X-Forwarded-Proto reference to see how proxies pass this context downstream.

Step 7: Automated Zero-Downtime Deployments

Manually SSHing into the server to pull code and restart PM2 gets tedious fast. Create a simple deploy.sh script inside /var/www/svelte-app/:

#!/bin/bash
set -e echo "Pulling latest changes from git..."
git pull origin main echo "Installing dependencies..."
npm ci --omit=dev echo "Building application..."
npm run build echo "Reloading PM2 instances..."
pm2 reload sveltekit-app echo "Deployment complete!"

Make it executable with chmod +x deploy.sh. Because PM2 runs in cluster mode (instances: 'max'), using pm2 reload restarts workers sequentially so visitors never see a dropped connection during a release.

Frequently Asked Questions

Can I run multiple SvelteKit apps on the same Hostinger VPS?

Yes. Give each app a distinct port in its ecosystem.config.cjs file (like PORT: 3000 and PORT: 3001). Then create separate Nginx server blocks for each domain, pointing the proxy_pass to the right port.

Why does my SvelteKit app show a 502 Bad Gateway error?

A 502 means Nginx is running, but nothing is listening on port 3000. Run pm2 status to see if your app is alive, and check pm2 logs sveltekit-app --err to see what caused Node to crash.

How much RAM does a SvelteKit app consume on a VPS?

Under normal SSR load, an idle SvelteKit instance typically uses 45MB to 85MB of RAM per worker. On a Hostinger KVM 1 or KVM 2 plan, you’ll have plenty of overhead for Nginx and system tasks.

Should I build the app locally or on the server?

If you’re on a VPS with 1GB of RAM, Vite’s build step can run out of memory. If that happens, run the build inside a GitHub Actions workflow and rsync the resulting build/ folder and package.json directly to the server.

For a similar setup with a different framework, take a look at our walkthrough to deploy Astro SSR on Hostinger VPS using the same reverse-proxy pattern.

all_in_one_marketing_tool