Server-side rendered SvelteKit apps break on plain static hosting the second you introduce API endpoints, dynamic server load functions, or form actions. After getting tired of unpredictable serverless bills and cold starts, I moved my SvelteKit setup over to a Hostinger VPS running Ubuntu 24.04, Nginx, and PM2. The result: rock-solid response times and a flat $6/month hosting bill.
Here is how to take a fresh SvelteKit project, wire up @sveltejs/adapter-node, point your Namecheap DNS records, configure Nginx as a reverse proxy with Let’s Encrypt SSL, and set up CDN caching for your immutable assets.

Architecture Overview and Prerequisites
Here is what the production stack looks like:
- Origin Server: Hostinger KVM 1 or KVM 2 VPS running Ubuntu 24.04 LTS
- Runtime: Node.js 20 LTS kept alive with PM2 in cluster mode
- Reverse Proxy: Nginx forwarding requests to local port 3000 with gzip/brotli enabled
- DNS: Namecheap BasicDNS using low-TTL A records
- Edge & SSL: Let’s Encrypt cert on the origin, backed by a CDN for static asset caching
Before jumping in, make sure you have root or sudo SSH access to your VPS and a domain ready inside Namecheap.
Step 1: Switch SvelteKit to the Node Adapter
Default SvelteKit setups come with @sveltejs/adapter-auto. It tries to detect your deployment target automatically, which fails completely on a standard VPS. You need the official standalone Node adapter instead.
In your local project directory, install @sveltejs/adapter-node:
npm uninstall @sveltejs/adapter-auto
npm install -D @sveltejs/adapter-nodeOpen your svelte.config.js and switch the import to adapter-node:
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: 'APP_' }) }
}; export default config;Check the SvelteKit Node adapter documentation if you need extra build flags. Setting precompress: true is well worth it—it generates gzip and brotli versions of pre-rendered pages and static assets at build time so Nginx doesn’t have to compress them on the fly.
Run a local build test to make sure everything compiles cleanly:
npm run build
node build/index.jsYour terminal should show the server running on http://localhost:3000. Hit Ctrl + C once verified.
Step 2: Provision Hostinger VPS and Install Dependencies
Log in to your Hostinger hPanel, grab your server’s public IPv4 address from the VPS dashboard, and SSH into the machine:
ssh root@YOUR_SERVER_IPFirst, update your package lists and install Nginx, Git, and build essentials:
apt update && apt upgrade -y
apt install -y curl git ufw nginx certbot python3-certbot-nginxNext, pull in Node.js 20 LTS from the official NodeSource repo per the standard Node.js LTS release cycle:
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs
npm install -g pm2Enable UFW to lock down everything except SSH, HTTP, and HTTPS traffic:
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enableStep 3: Configure Namecheap DNS Records
Head to Namecheap, go to your Domain List, select your domain, and open the Advanced DNS tab. Clear out any default parking page records, then add two A Records pointing straight to your Hostinger VPS IP:
- Type: A Record | Host: @ | Value: YOUR_SERVER_IP | TTL: Automatic (or 5 min if migrating)
- Type: A Record | Host: www | Value: YOUR_SERVER_IP | TTL: Automatic
Give it a couple of minutes, then verify the records resolve to your server IP from your local terminal:
dig +short yourdomain.comOnce you see your VPS IP address in the response, you are ready to move on.
Step 4: Deploy the Application and Configure PM2
Create a directory under /var/www/ for your project and clone your repository (or push your files up via rsync/SFTP):
mkdir -p /var/www/svelte-app
cd /var/www/svelte-app
git clone https://github.com/your-username/your-svelte-repo.git .
npm install
npm run buildTo ensure your app recovers from crashes and reboots, create an ecosystem.config.cjs file in /var/www/svelte-app:
module.exports = { apps: [ { name: 'svelte-app', script: 'build/index.js', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production', PORT: 3000, HOST: '127.0.0.1', ORIGIN: 'https://yourdomain.com' } } ]
};Fire up the app with PM2 and register the startup hook so it launches on system boot:
pm2 start ecosystem.config.cjs
pm2 save
pm2 startup systemdTo prevent PM2 logs from silently eating all your disk space over time, check our guide on how to configure PM2, startup scripts, and log rotation on VPS.
Step 5: Set Up Nginx Reverse Proxy
Nginx sits in front of Node, terminates SSL, routes traffic to PM2 on port 3000, and offloads static assets. Consult the Nginx proxy module documentation for advanced header configurations.
Create your site configuration at /etc/nginx/sites-available/svelte-app:
server { listen 80; server_name yourdomain.com www.yourdomain.com; 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_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_cache_bypass $http_upgrade; proxy_read_timeout 60s; }
}Symlink it to sites-enabled, test the syntax, and reload Nginx:
ln -s /etc/nginx/sites-available/svelte-app /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t
systemctl reload nginxFor more architectural details on hosting Node runtimes behind Nginx, take a look at our Node.js deployment guide for Hostinger VPS.
Step 6: Secure with Let’s Encrypt SSL
Now issue a free SSL cert using Certbot and let it configure HTTPS redirection automatically:
certbot --nginx -d yourdomain.com -d www.yourdomain.comFollow the prompts: provide an email, accept the terms, and choose to redirect HTTP to HTTPS. If you hit a verification error here, check our guide to fix Certbot ACME HTTP-01 challenge errors.
Step 7: Optimize CDN Caching for SvelteKit Static Chunks
SvelteKit places content-hashed JavaScript and CSS under /_app/immutable/. Because the filenames change whenever the code changes, you can safely cache these files for a full year on both browsers and edge CDN nodes.
Reopen /etc/nginx/sites-available/svelte-app and add dedicated static cache blocks directly above your location / block:
# SvelteKit Immutable Static Chunks
location /_app/immutable/ { alias /var/www/svelte-app/build/client/_app/immutable/; expires 1y; add_header Cache-Control "public, immutable, max-age=31536000"; access_log off;
} # General Static Assets (Favicons, robots.txt, static folder)
location ~* .(ico|png|jpg|jpeg|svg|webp|woff|woff2|ttf|txt)$ { root /var/www/svelte-app/build/client; expires 30d; add_header Cache-Control "public, max-age=2592000"; access_log off;
}Test and reload your Nginx configuration:
nginx -t && systemctl reload nginxWhen you place a CDN (like Cloudflare or Fastly) in front of this VPS, edge nodes will cache these immutable chunks without ever hitting your Node runtime. For a deep dive into caching headers, read our guide to configure immutable cache-control headers in Nginx.
Gotchas I Hit and How to Fix Them
The biggest issue I encountered right after deploying was that form submissions failed with a 403 Cross-site POST form submissions are forbidden error. SvelteKit checks incoming Origin headers against the expected domain to block CSRF exploits.
When running behind an Nginx reverse proxy, SvelteKit will throw this error unless you ensure two things:
- Set the
ORIGINenvironment variable in your PM2 config (e.g.,ORIGIN: 'https://yourdomain.com'). - Pass the
X-Forwarded-Proto $scheme;header from Nginx so SvelteKit recognizes the connection as HTTPS.
Also, if your app handles file uploads larger than 1MB, add client_max_body_size 20M; inside your Nginx server block, and define BODY_SIZE_LIMIT=20971520 in your PM2 environment variables.
Frequently Asked Questions
Why use adapter-node instead of adapter-static?
adapter-static only generates pre-rendered HTML/JS/CSS files. If your project relies on dynamic database lookups in +page.server.js, cookie authentication, or server endpoints (+server.js), you need a live Node runtime powered by adapter-node.
How do I deploy code updates without downtime?
Because PM2 is set to exec_mode: 'cluster', you can pull your latest commits, run npm run build, and execute pm2 reload svelte-app. PM2 will cycle through worker processes one at a time so zero connections get dropped.
Why bind the Node app to 127.0.0.1 instead of 0.0.0.0?
Binding to 127.0.0.1 ensures your SvelteKit Node app is only reachable locally. This stops direct external requests from bypassing your Nginx proxy, SSL layer, and rate limits on port 3000.
Next Steps for Your SvelteKit Stack
You now have a fully functional, self-hosted SvelteKit production app running behind Nginx with automatic SSL, fast DNS routing, and edge-friendly caching. From here, consider setting up automated database backups and establishing basic UFW rate limiting as your traffic grows.

