Why self-host SvelteKit on a VPS
SvelteKit adapts to many targets via its adapters (Vercel, Netlify, Cloudflare…), but the official @sveltejs/adapter-node adapter produces a standalone Node server, perfect for a VPS. Instead of splitting your application into serverless functions subject to cold starts and execution-time limits, you get a persistent process that keeps its database connections open and responds without startup latency. Form actions, server-side load functions and +server.ts endpoints run in a single runtime that you control. On a VPS, you also avoid per-invocation billing, common on front-end platforms, which becomes unpredictable as soon as traffic rises.
Concrete benefits
- Persistent Node server via adapter-node: no cold start on server routes
- Database connections kept open in a pool, without reconnecting on each request
- No per-invocation billing: stable cost even with high and irregular traffic
- Form actions and
+server.tsendpoints executed without an imposed duration limit - WebSockets and SSE possible, where serverless forbids or complicates them
- Environment variables and secrets managed directly, without a third-party platform dashboard
Hardware and software prerequisites
A SvelteKit application in Node mode consumes moderately: a VPS with 1 vCPU and 2 GB of RAM suits a medium-traffic site; scale up to 2 vCPU / 4 GB if you co-locate a PostgreSQL database and have heavy server load functions. Install Node.js 20 LTS, and either Docker or PM2 to manage the process as a daemon. Nginx will serve as a reverse proxy. Point your domain at the VPS IP. The build (build/ generated by adapter-node) stays light; 5 to 10 GB of disk is enough with the node_modules.
Step-by-step deployment
Install and configure the Node adapter
Add @sveltejs/adapter-node and declare it in svelte.config.js (adapter: adapter()). It is what transforms your app into a standalone Node server rather than a serverless bundle.
Build the application
Run npm run build. SvelteKit generates a build/ folder containing index.js, the server's entry point. You test it locally with node build; it listens by default on port 3000.
Containerize or daemonize the process
With Docker: a node:20-alpine Dockerfile that copies build/ and package.json, installs the production dependencies (npm ci --omit=dev) and runs node build. Without Docker: use PM2 with pm2 start build/index.js --name svelte-app for automatic restart.
Inject the runtime configuration
Pass PORT, ORIGIN (indispensable for SvelteKit to validate form actions) and your secrets via the environment: ORIGIN=https://mydomain.com. Without a correct ORIGIN, form actions will fail in production with a CSRF error.
Configure Nginx as a front end
Create a vhost with proxy_pass http://127.0.0.1:3000; and the proxy_set_header Host and X-Forwarded-Proto $scheme headers. Enable proxy_http_version 1.1 and the Upgrade headers if you use WebSockets.
Secure with Let's Encrypt
Run certbot --nginx -d mydomain.com. Once the certificate is installed, check that ORIGIN matches the final HTTPS URL, otherwise form submissions will be rejected.
Cache the output of static routes with the Cache-Control header in your load functions, and enable precompression: adapter-node automatically serves .gz and .br files if they exist. Build with precompression by setting precompress: true in the adapter options. On the process management side, prefer PM2's cluster mode (pm2 start build/index.js -i max) to make use of all the VPS cores under a high CPU load.