SvelteKit Dockerfile for adapter-node (Alpine, Non-Root)

This SvelteKit Dockerfile builds your app with adapter-node, prunes development dependencies, and runs the generated Node server on node:24-alpine as the unprivileged node user. The server listens on port 3000 through the PORT variable. It fits server-rendered SvelteKit apps deployed as a container behind a proxy.

CI status
Build passing
Image size (uncompressed)
173.4 MB
Build time
12 s
Runtime image
node:24-alpine
Port
3000
Last built
2026-10-15
Dockerfile
# syntax=docker/dockerfile:1
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev

FROM node:24-alpine
ENV NODE_ENV=production PORT=3000
WORKDIR /app
COPY --from=build --chown=node:node /app/package.json ./
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/build ./build
USER node
EXPOSE 3000
CMD ["node", "build"]
.dockerignore
.git
node_modules/
build/
.svelte-kit/
.env*
Dockerfile
.dockerignore

Before you build

How this Dockerfile works

  1. FROM node:24-alpine AS build

    The build stage holds every dev dependency the Vite build needs. None of it reaches the final image.

  2. COPY package.json package-lock.json ./ RUN npm ci

    Installing from the lockfile before copying source caches dependencies, so editing a component doesn't trigger a full reinstall.

  3. RUN npm run build && npm prune --omit=dev

    adapter-node writes a standalone server into build. Pruning afterwards leaves only the runtime dependencies your server code imports.

  4. ENV NODE_ENV=production PORT=3000

    The adapter-node server reads PORT to decide where to listen. NODE_ENV=production makes libraries skip their development-only behavior.

  5. COPY --from=build --chown=node:node /app/build ./build

    Copies the compiled server and client assets. The pruned node_modules and package.json are copied next to it so imports and module type resolve.

  6. USER node

    Runs the server as the non-root user that comes with the official Node image.

  7. CMD ["node", "build"]

    Node resolves build/index.js and starts the SvelteKit server. Running node directly lets shutdown signals reach the process.

Build and run

terminal
docker build -t sveltekit-app .
docker run --rm -p 3000:3000 sveltekit-app

Open http://localhost:3000 after the container starts. In production, also pass ORIGIN, for example your public HTTPS URL, so form actions pass SvelteKit's cross-site request checks.

Other SvelteKit Dockerfile variants

adapter-node on Alpine is the standard way to run SvelteKit as its own server. Fully prerendered sites would instead use adapter-static with an Nginx-based image, and teams on pnpm or Bun would use matching variants. They appear in the table below once verified.

VariantRuntime imageImage sizeCI
adapter-node on Node Alpinenode:24-alpine173.4 MBPassing

Troubleshooting

Cross-site POST form submissions are forbidden
SvelteKit compares the request origin with ORIGIN. Set ORIGIN to the exact public URL users visit, including the protocol, when you start the container.
Error: Cannot find module '/app/build'
The build used a different adapter, so no build folder exists. Check that your SvelteKit config (vite.config.ts or svelte.config.js) imports @sveltejs/adapter-node and that npm run build completes inside the image.
A package is missing at runtime after the prune step
It is listed under devDependencies but imported by server code. Move it to dependencies so npm prune keeps it.

FAQ

Why does the final stage copy node_modules at all?
adapter-node bundles most code, but packages it treats as external are still imported at runtime. Copying the pruned production node_modules keeps those imports working.
Can I serve a static SvelteKit site with this file?
You can, but it's wasteful. Prerendered sites only need a web server, so an adapter-static build served by Nginx gives a smaller and simpler image.
How do I change the port?
Set PORT to another value with -e when running the container and publish that same port. Updating EXPOSE as well keeps the image self-documenting.