SvelteKit apps deploy to many platforms via adapters. For your own server, @sveltejs/adapter-node produces a standalone Node.js server that you run with systemd or PM2 behind Nginx.
1. Switch to adapter-node
npm i -D @sveltejs/adapter-node
// svelte.config.js
import adapter from '@sveltejs/adapter-node';
export default {
kit: {
adapter: adapter({ out: 'build' }),
},
};
(If your site is fully prerenderable, adapter-static produces plain files for Nginx instead — see deploying a static site.)
2. Build
npm ci
npm run build
The output in build/ contains index.js (the server entry) and the client assets. At runtime it still needs your production dependencies:
build/
package.json
package-lock.json
Copy those to the server and install production dependencies there (npm ci --omit=dev), or bundle dependencies into the build so node_modules isn’t needed.
3. Environment variables that matter
adapter-node reads these at runtime:
| Variable | Purpose |
|---|---|
HOST, PORT |
Bind address (use 127.0.0.1 behind Nginx) |
ORIGIN |
Public URL, e.g. https://example.com — required for form actions’ CSRF check behind a proxy |
PROTOCOL_HEADER, HOST_HEADER |
Alternative to ORIGIN: derive the URL from x-forwarded-proto / x-forwarded-host |
ADDRESS_HEADER |
e.g. X-Forwarded-For — so event.getClientAddress() returns the visitor IP |
XFF_DEPTH |
Number of trusted proxies when using X-Forwarded-For |
BODY_SIZE_LIMIT |
Max request body (default 512 KB) — raise for uploads |
Without ORIGIN (or the header settings), form submissions fail with “Cross-site POST form submissions are forbidden” behind a proxy.
App-specific variables are read through $env/dynamic/private (runtime) or $env/static/private (inlined at build time).
4. Run with systemd
# /etc/systemd/system/sveltekit.service
[Unit]
Description=SvelteKit app
After=network.target
[Service]
User=deploy
WorkingDirectory=/srv/sveltekit
Environment=NODE_ENV=production
Environment=HOST=127.0.0.1
Environment=PORT=3000
Environment=ORIGIN=https://example.com
Environment=ADDRESS_HEADER=X-Forwarded-For
Environment=XFF_DEPTH=1
Environment=BODY_SIZE_LIMIT=10M
EnvironmentFile=-/etc/sveltekit.env
ExecStart=/usr/bin/node build
Restart=always
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload && sudo systemctl enable --now sveltekit
curl -I http://127.0.0.1:3000
For zero-downtime reloads, PM2 in cluster mode works too (pm2 start build/index.js -i 2) — see the PM2 guide.
5. Nginx
server {
listen 80;
server_name example.com;
client_max_body_size 10M;
location /_app/immutable/ {
proxy_pass http://127.0.0.1:3000;
expires 1y;
add_header Cache-Control "public, immutable";
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Files under /_app/immutable/ have content hashes in their names, so a long cache is safe. (adapter-node already sends good cache headers; the explicit block helps if a CDN sits in front.) Keep client_max_body_size in line with BODY_SIZE_LIMIT.
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d example.com
6. Deploy updates
npm ci && npm run build
rsync -az --delete build package.json package-lock.json deploy@server:/srv/sveltekit/
ssh deploy@server 'cd /srv/sveltekit && npm ci --omit=dev && sudo systemctl restart sveltekit'
Or build a Docker image in CI and pull it on the server — see Dockerize a Node.js app.
Troubleshooting
- “Cross-site POST form submissions are forbidden” — set
ORIGIN(orPROTOCOL_HEADER/HOST_HEADER). - Client IP is 127.0.0.1 — set
ADDRESS_HEADERandXFF_DEPTH. - 413 on uploads — raise both
BODY_SIZE_LIMITand Nginx’sclient_max_body_size. - Module not found at startup — production dependencies weren’t installed next to
build/.
Related: deploying Next.js and Nuxt on a VPS.