This recipe uses SvelteKit's Node adapter on port 3000. It includes a real POST form so you can verify that deployment, HTTPS and origin handling work together. A page that loads while its form fails is not a complete deployment.
Verification scope
The downloadable sample was checked on September 24, 2026 using both its Dockerfile and Nixpacks 1.39.0 build configuration. Checks covered HTTPS through a local Caddy proxy, static assets, runtime release IDs and configuration recovery. The form succeeded with the correct origin and rejected an incorrect origin. A local certificate authority was used for those tests; public certificate issuance and a licensed QuickDeploy SSH deployment still need verification on your own test server.
Before you start
Use a Linux VPS with Docker Engine and Docker Compose, an SSH user allowed to run Docker, and a domain pointing to that server. Ports 80 and 443 must reach your reverse proxy. Follow the Ubuntu server checklist first.
Start with a separate test domain and project. If a reverse proxy already owns ports 80/443, extend that proxy rather than starting another. The manual Docker path does not need a QuickDeploy license. The optional QuickDeploy path requires a license and a compatible server; check requirements before buying.
Get the sample
On your computer, download the archive using the button above or run:
curl -fL https://quickdeploy.dev/examples/sveltekit.tar.gz -o sveltekit.tar.gz
tar -xzf sveltekit.tar.gz
cd sveltekit
The archive includes the application, exact dependency lockfile, Dockerfile, .dockerignore, .env.example, and nixpacks.toml. Archive checksums identify the downloadable versions. Use Node.js 24 if building outside Docker. Do not replace your existing app with the sample; use it to verify the deployment path first.
Configure adapter-node
The sample pins SvelteKit 2.70.3, adapter-node 5.5.7 and Svelte 5.57.1. Its svelte.config.js is:
import adapter from '@sveltejs/adapter-node'
export default { kit: { adapter: adapter() } }
The scripts build with vite build and start with node build. Keep the generated build directory, package.json and production dependencies together in the runtime image. The downloadable Dockerfile installs all dependencies for the build, then prunes development dependencies before assembling the runtime.
Set HOST=0.0.0.0, PORT=3000 and an ORIGIN matching the public URL. Production node build does not automatically read .env like the development server; pass runtime variables with Docker/Compose or a supported environment-file loader. See Svelte's Node server documentation.
Build and submit a form locally
On your computer:
docker build -t sveltekit-recipe:1 .
docker run --rm --name sveltekit-recipe-local \
-p 127.0.0.1:3100:3000 \
-e ORIGIN=http://localhost:3100 \
-e RELEASE_ID=first-deploy sveltekit-recipe:1
Open http://localhost:3100, enter a name, and submit Test form. You should see a greeting. Also check /api/health and /verification.txt. If you use a different browser hostname or host port, change ORIGIN to exactly that URL. Stop this sample with Ctrl+C after the test.
The sample action returns the submitted name without storing it. When adapting it to your application, add authentication, authorization and input validation appropriate to the data you accept.
Deploy with Docker, Caddy and HTTPS
Follow the Docker VPS walkthrough to transfer the image and prepare Caddy's shared web network. Use image sveltekit-recipe:1, service/container sveltekit-recipe, and internal port 3000.
On the VPS, create the app's Compose file in its own directory:
services:
app:
image: sveltekit-recipe:1
container_name: sveltekit-recipe
environment:
HOST: 0.0.0.0
PORT: "3000"
ORIGIN: https://svelte.example.com
RELEASE_ID: first-deploy
expose:
- "3000"
networks:
- web
restart: unless-stopped
networks:
web:
external: true
Use your actual domain in ORIGIN, with no path. Add a site to the existing proxy configuration:
svelte.example.com {
reverse_proxy sveltekit-recipe:3000
}
Start the app with docker compose up -d, then validate and reload Caddy as described in the walkthrough. Do not start a second proxy on occupied ports. For a nonstandard public port, include that port in ORIGIN.
Deploy the source with QuickDeploy
QuickDeploy uses Nixpacks to build from source. It does not use the sample Dockerfile. The included nixpacks.toml explicitly selects Node.js 24 and OpenSSL from a pinned Nix archive and installs development dependencies for the build; the Nixpacks image was also checked locally. Complete a live QuickDeploy deployment check on your own test server.
Create .env.production in the project directory:
NODE_ENV=production
HOST=0.0.0.0
PORT=3000
ORIGIN=https://svelte.example.com
RELEASE_ID=first-deploy
After installing and configuring QuickDeploy, run on your computer:
quickdeploy push --domain svelte.example.com --project sveltekit-recipe --port 3000
Use your own domain and an unused project name. QuickDeploy manages shared Caddy configuration and can recreate that proxy on a first deployment. Use a suitable test server, not a host whose existing containers must remain untouched. Leave the command attached and then verify the public app.
Test the public form and an update
Open the HTTPS page in your browser and submit the form. From your computer, you can also exercise the same action:
curl --fail --show-error https://svelte.example.com/api/health
curl --fail --show-error https://svelte.example.com/verification.txt
curl --fail --show-error \
-H 'Origin: https://svelte.example.com' \
--data-urlencode 'name=Deployment check' \
https://svelte.example.com/
The POST response should contain Hello, Deployment check!. Change RELEASE_ID to second-deploy, repeat the deployment with the same project name and confirm the new ID in /api/health. Submit the form again after the update.
For the manual path, keep the previous image tag and environment so you can restore them with Compose if validation fails. With QuickDeploy, redeploy a known working source revision and configuration. Its process check is not an HTTP or form-action readiness check.
Fix the cross-site POST error
If the app loads but forms report Cross-site POST form submissions are forbidden, compare the browser's public scheme, host and port with ORIGIN. A localhost origin is wrong once the app is served at an HTTPS domain. Correct the runtime value, recreate/redeploy the app and retest.
For a deployment with several legitimate public origins, review SvelteKit's trusted-proxy header configuration instead of assuming one fixed origin. Do not disable CSRF protection to hide a deployment configuration error. Programonaut's existing explanation covers the error in more detail.
Other common failures:
- Cannot find
build/index.js: build the app with adapter-node, copy the complete output, and usenode build. - Variables worked in development: production does not automatically load the same environment file. Check Compose/runtime configuration, without printing secrets to logs.
- Caddy returns 502: check the container logs, the shared network, port 3000 and binding to
0.0.0.0.
Once this path works, use the CI/CD guide to automate it. Use the PostgreSQL guide for database network and persistence requirements.