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
# 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"].git
node_modules/
build/
.svelte-kit/
.env*
Dockerfile
.dockerignoreBefore you build
- Install @sveltejs/adapter-node and set it as the adapter. New projects keep this in vite.config.ts; older ones use svelte.config.js.
- Commit package-lock.json so npm ci can run.
- Set the ORIGIN environment variable at runtime when the app sits behind a proxy or custom domain.
How this Dockerfile works
FROM node:24-alpine AS buildThe build stage holds every dev dependency the Vite build needs. None of it reaches the final image.
COPY package.json package-lock.json ./ RUN npm ciInstalling from the lockfile before copying source caches dependencies, so editing a component doesn't trigger a full reinstall.
RUN npm run build && npm prune --omit=devadapter-node writes a standalone server into build. Pruning afterwards leaves only the runtime dependencies your server code imports.
ENV NODE_ENV=production PORT=3000The adapter-node server reads PORT to decide where to listen. NODE_ENV=production makes libraries skip their development-only behavior.
COPY --from=build --chown=node:node /app/build ./buildCopies the compiled server and client assets. The pruned node_modules and package.json are copied next to it so imports and module type resolve.
USER nodeRuns the server as the non-root user that comes with the official Node image.
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
docker build -t sveltekit-app .
docker run --rm -p 3000:3000 sveltekit-appOpen 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.
| Variant | Runtime image | Image size | CI |
|---|---|---|---|
| adapter-node on Node Alpine | node:24-alpine | 173.4 MB | Passing |
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.