How to Deploy a SvelteKit App With the Node Adapter [Full Repo]

Swap adapter-auto for adapter-node, containerize the build, and ship server-rendered SvelteKit to a public HTTPS URL. Tested steps, the build crash we hit on a too-new Node, and live proof that the HTML arrives already rendered.

A SvelteKit app's route from the adapter swap in vite.config.ts through a two-stage Docker build to a live server-rendered HTTPS URL, with the Node 25 build crash called out as the trip-up

The first surprise in deploying modern SvelteKit is that the file every older tutorial tells you to edit does not exist. The sv CLI’s minimal template writes no svelte.config.js; the adapter — the thing that decides whether your build is a Node server, a static folder or an edge function — now lives in vite.config.ts.

So this guide starts there: swap @sveltejs/adapter-auto for @sveltejs/adapter-node, wrap the result in a two-stage Docker image, and put it on a public HTTPS URL. We ran the whole flow for real on the free plan, and the finished code sits in the companion repo.

Check your starting point

Before any Docker, figure out how your project configures its adapter today — that depends on your SvelteKit version, not on habit.

Your projectAdapter situationWhat to do
Fresh sv CLI scaffold (SvelteKit 3)adapter-auto, configured in vite.config.tsthis guide, start to finish
Earlier SvelteKit major with svelte.config.jsadapter configured in that filesame swap and import — but confirm against your installed @sveltejs/kit version’s docs, since config-file conventions changed between majors
Already on adapter-nodenothing to swapjump to the Dockerfile section
Fully prerendered site on adapter-staticno server at alla static container is simpler than this guide

Starting from zero instead? Scaffold with the sv CLI (npx sv create my-app, minimal template), run npm install, and commit the lockfile — the Dockerfile’s npm ci refuses to run without package-lock.json.

Our versions throughout: @sveltejs/kit 3.1.1, @sveltejs/adapter-node 6.0.1, svelte 5.57.3, vite 8.3.6, built on Node 22 LTS.

Configure the Node adapter in vite.config.ts

Install the Node adapter as a dev dependency, then hand it to the sveltekit() plugin where the scaffold had adapter-auto.

npm install -D @sveltejs/adapter-node
// vite.config.ts (trimmed to the relevant part — the repo's file
// also keeps the scaffold's compilerOptions block)
import adapter from '@sveltejs/adapter-node';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
	plugins: [
		sveltekit({
			adapter: adapter()
		})
	]
});

Now npm run build emits a build/ folder containing a complete Node server. You start it with node build, and its listen address comes from two environment variables: HOST for the interface and PORT for the port.

Those two cover listening, not everything. Depending on what your app does, the adapter-node docs describe further settings — notably how the server learns its public origin, a setting that has moved between Kit versions.

Current docs cover paths.origin (the ORIGIN environment variable is the older form), plus header options for running behind proxies.

The minimal starter needed none of them.

One warning that cost us a build: do this on Node 22 LTS. On Node 25, vite build with this adapter stack crashed outright — the exact error is in the troubleshooting section. Node 22 is also what the production image runs, so building on it locally keeps the two environments identical.

Build the Docker image

The build stage compiles; the runtime stage carries almost nothing, because this adapter’s output is self-contained for a dependency-free app.

# ---- Build stage ----
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# ---- Runtime stage ----
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production HOST=0.0.0.0 PORT=8080

# adapter-node output is self-contained for this starter (no runtime deps);
# package.json is still needed for "type": "module".
COPY --from=build /app/build ./build
COPY package.json ./

EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
  CMD wget -qO /dev/null http://127.0.0.1:8080/ || exit 1
USER node
CMD ["node", "build"]

Two choices worth naming. HOST=0.0.0.0 makes the server reachable from outside the container — a server bound to localhost inside a container answers nobody. And the runtime stage copies only build/ plus package.json, which must come along for its "type": "module" line.

That lean runtime stage works because the minimal app has zero production dependencies. Real apps usually don’t: per the adapter-node docs, the deployed server needs the build output, package.json, and the production node_modules. If your dependencies block is non-empty, extend the runtime stage like this:

# Runtime stage variant for apps WITH production dependencies
COPY --from=build /app/build ./build
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force

Build and test locally with docker build -t my-app . and docker run -p 8091:8080 my-app, then open http://localhost:8091/.

Deploy on Back4app

With the Dockerfile committed and pushed to GitHub, the platform side is three screens.

  1. Create a New Web Deployment and pick the GitHub repository as the source.

The Choose a source step of a new Web Deployment, with GitHub repository, Upload files and Docker Hub image as the available paths and the connected repository list below

The source screen is identical for every container deploy in this series — dashboard as of November 2026.

  1. The Dockerfile is detected as the build method. In Build & deploy, type 8080 into the Port field yourself: in our testing the platform did not pick the port up from the Dockerfile’s EXPOSE, and the React deploy guide documents the launch failure that leaving it on auto produces. Our SvelteKit app had 8080 set at creation and never hit it.

The Build & deploy settings page with Dockerfile selected as build method and the Port field set to 8080

The Port field under Build & deploy — set it explicitly. Dashboard as of November 2026.

  1. Create the app and watch the first deploy come up on its public HTTPS URL; our creation-to-serving time is in the measurements table.

The "Your app is ready" dialog after a first deploy: the temporary HTTPS URL with a copy button and the 60-minute expiry countdown

The free plan’s live dialog: a real URL and its countdown. Dashboard as of November 2026.

Verify SSR on the live URL

A SvelteKit page can look identical whether the server rendered it or the client did, so we checked the one thing a browser view hides: the raw response of the home page, /.

On November 9, 2026 that HTML document already contained the page’s rendered content — the <h1> with “Welcome to SvelteKit” was in the response body itself, not injected later by a script. That is server rendering at work on the route we tested, provable with nothing but curl.

The same response also names the JavaScript the client will hydrate with: 9 script files listed via modulepreload links. We summed those referenced files’ gzip transfer on the live URL and got the same bytes we had measured from the local container — exact figures in the measurements table.

To be precise about what this covers: it is the JavaScript referenced by the served HTML, not a browser-recorded capture of all network activity on load.

Deploy from an AI assistant with MCP

An assistant connected to Back4app’s remote MCP server can run this whole deploy from your editor. MCP is only the interface: the image is still built from the Dockerfile above. Setup for each client is on the official MCP page.

For the tool calls behind each step, see How to deploy a web app with MCP and an AI assistant.

In our run on November 10, 2026 (Claude, custom connector, signed in), the assistant called create_upload, ran the returned npx @back4app/cli upload command in the project folder, then create_web_app with port 8080. The app came up with builder: DOCKERFILE and reached READY about 62 seconds after creation.

On the public URL the served HTML carried the rendered heading, and the page referenced the same nine JavaScript files as the dashboard deploy (32,171 B in total).

Two upload behaviors are specific enough to SvelteKit projects to mention:

  • .npmrc is left out. The CLI skipped it with “Left out, as they may hold secrets”. This starter’s .npmrc holds one line (engine-strict=true) and the build passed without it; a project that needs a private registry line there would not.
  • The upload ignores .gitignore. Local build/ and .svelte-kit/ folders went along: 178 files packed against 19 tracked. The Dockerfile build was unaffected because the builder reads .dockerignore.

Details and ids: MCP evidence file.

Update the deployed app

Releasing a change takes a push and one click: push the commit, then App actions → Deploy the latest commit, and the rebuild lands on the URL you already have — we validated that round trip on this deploy’s platform. Paid plans can instead auto-deploy on every push.

Two adjacent facts complete the picture: the free plan requires that manual click per release, and deleting and re-creating the app always works but mints a different URL, so keep re-creation for emergencies. For the full update-flow test and its one caveat about changed branch settings, see the React deploy guide.

Measurements

The live deploy ran the starter at commit ebdb342; local figures were taken November 6, 2026 and live figures November 9, 2026.

Transfer sizes (what crossed the network, gzip) are kept apart from on-disk sizes. Full method and raw output are in the repo’s evidence files.

WhatValueWhere
JS referenced by the served HTML (transfer)32,171 B gzip across 9 fileslive URL
HTML document on / (transfer)2,007 B gziplive URL
App creation → serving~36 sdeploy timeline
TTFB on / (3 illustrative requests)1.075 s · 1.078 s · 3.462 s — the third an outlier, reported as observedlive URL
npm ci / npm run build2.6–2.8 s / 1.6 slocal
docker build (no cache)13–15 slocal
Image size (on disk)237 MB (arm64)local
Build output build/ (on disk)1.6 MBlocal

The referenced-JS transfer matching between local container and live URL to the byte is the quiet headline: what the adapter precompresses is what the public URL transferred on this date.

Troubleshooting

ERR_INVALID_ARG_VALUE mentioning util.styleText and Received 'grey' — you are building on Node 25. This crashed vite build for us with SvelteKit 3.1.1, adapter-node 6.0.1 and Vite 8.3.6; the identical project built cleanly on Node 22 LTS. Switch Node versions, not packages.

Please mention a port in settings to expose — the platform’s Port field is still on auto; it does not read EXPOSE from the Dockerfile. Set 8080 in Build & deploy and redeploy.

The npm ci command can only install with an existing package-lock.json — the lockfile never got committed. npm install once locally, commit package-lock.json, push again.

Limitations of this setup

Honest edges of the flow you just built, so none of them surprises you later.

  • Free-plan mechanics: the URL expires 60 minutes after the app’s creation, and each release needs the manual dashboard click — permanence and push-triggered deploys are paid-plan features.
  • A long-running process: adapter-node assumes a Node server that stays up; it is not a static export or a per-request serverless function. If every route prerenders, adapter-static removes the server entirely.
  • Build-environment coupling: the Node version matters — we saw the build itself break on a newer Node than the deploy target. Pin your local and CI builds to the runtime’s version.
  • What we verified: server rendering was confirmed for the home route of the minimal app; apps behind proxies or serving multiple origins have extra configuration this starter never exercised (origin configuration per your Kit version is in the adapter docs).

When the app grows users and data, that is a next step, not a flaw: your own server routes can handle auth and persistence, or a managed backend can serve them over REST and GraphQL while the container stays focused on rendering.

What’s next?

Run the swap on your own app: adapter in vite.config.ts, Dockerfile from the companion repo, port set explicitly, and your server-rendered pages get a public HTTPS URL on the free plan (Containers docs).

Tagssveltekitsvelteadapter-nodedockernodessrcontainersdeploy

Frequently asked questions

Which SvelteKit adapter should I use for Docker?

@sveltejs/adapter-node. It turns the build into a plain Node server, started with node build, that reads its listen address from the HOST and PORT environment variables — the shape a container platform expects. This guide's deploy ran adapter-node 6.0.1 with SvelteKit 3.1.1 inside node:22-alpine and was verified on its live URL.

Where do I configure the adapter if my project has no svelte.config.js?

In vite.config.ts. The sv CLI's minimal SvelteKit 3 template no longer writes a svelte.config.js — the sveltekit() Vite plugin takes the adapter as an option instead. We imported @sveltejs/adapter-node there and passed adapter: adapter(). Earlier SvelteKit versions configure the adapter in svelte.config.js; where yours goes depends on the @sveltejs/kit version you have installed, so check that version's documentation rather than assuming either location.

What port does the SvelteKit Node server listen on?

Whatever the PORT environment variable says, on the interface HOST names — our Dockerfile sets HOST=0.0.0.0 and PORT=8080 in the runtime stage. Set the platform's port field to 8080 explicitly as well: in our testing the Dockerfile's EXPOSE alone was not adopted, and leaving the field on auto fails the deploy at launch.

Why does my SvelteKit build fail with ERR_INVALID_ARG_VALUE?

In our testing, vite build with adapter-node crashed on Node 25 with ERR_INVALID_ARG_VALUE mentioning util.styleText and the value 'grey' (SvelteKit 3.1.1, adapter-node 6.0.1, Vite 8.3.6). The same project built cleanly on Node 22 LTS — and since node:22-alpine is what the container runs anyway, building on 22 locally keeps the environments matched.

Does SvelteKit server-side render by default?

For this template, yes, and we checked it on the deployed app rather than trusting the docs: the live home page's HTML response already contained the rendered page content, h1 included, not an empty shell waiting for JavaScript. The client then hydrates using the script files the HTML references — their measured size is in the article's measurements table.

Can I try a SvelteKit container deploy without paying?

Yes — this entire guide ran on the free plan. Know the trade before you start: a free-plan URL expires 60 minutes after the app is created, so it demonstrates the pipeline rather than hosts the app, and new commits are redeployed with a manual dashboard action. Paid plans keep the URL and can deploy on push.

Further reading

Reviewed & verified by Back4app Engineering, Editorial review at Back4app

Tested Nov 9, 2026 — Local: Node 22 LTS, Docker CLI (Colima) on an Apple Silicon Mac · Live: Back4app Containers, free plan (0.25 vCPU / 256 MB), deployed from GitHub, measured November 9, 2026.

Changelog

  • Nov 10, 2026 — Added a section on running this deploy from an AI assistant through the Back4app MCP server, tested on this starter.
  • Nov 10, 2026 — Free-plan URL lifetime restated: 60 minutes from the app's creation, not per deploy. Added a link to the full MCP deploy walkthrough.

More from Back4app Engineering

Web Deployment · alternative

Top 10 Vue Alternatives in 2026 (Compared & Ranked)

Containers · tutorial

How to Deploy a React App: 3 Methods, Step by Step [Full Repo]

Containers · tutorial

How to Deploy a Next.js SSR App With Docker and a Backend [Full Repo]