How to Dockerize SvelteKit

How to Dockerize SvelteKit

Jonas Scholz - Co-Founder von sliplane.ioJonas Scholz
5 min

This guide is for SvelteKit apps with a Node server. A static Svelte site needs a different deployment. We will use adapter-node, build the app once, and run the output in a separate Docker stage.

Set up adapter-node

If you do not have a project yet, create one with npx sv create my-svelte-app, select npm, and install its dependencies. In the project directory, install the adapter:

npm install -D @sveltejs/adapter-node

Change the adapter import in svelte.config.js from @sveltejs/adapter-auto to @sveltejs/adapter-node. Keep the rest of your configuration, including preprocessors. The relevant settings are:

import adapter from "@sveltejs/adapter-node";

export default {
  kit: {
    adapter: adapter(),
  },
};

The adapter generates a Node server in build by default. The adapter-node documentation describes the available options.

Build and runtime stages

Dockerfile
FROM node:24-bookworm-slim 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-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=3000
COPY --from=build --chown=node:node /app/build ./build
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/package.json ./
USER node
EXPOSE 3000
CMD ["node", "build"]

The first stage installs dependencies from the lockfile, builds the app, and removes development dependencies. The final stage copies the server, its remaining dependencies, and package.json. node build starts the server; it does not rebuild it.

Keep packages that the server loads at runtime in dependencies. Build tools belong in devDependencies. Do not move every package to devDependencies just to reduce the image size: a package that remains external to the bundle must still be installed in the runtime image.

Add a .dockerignore so local artifacts and secrets stay out of the context:

node_modules
.svelte-kit
build
.git
.env
.env.*
npm-debug.log

Build and run

docker build -t sveltekit-app .
docker run --rm --name sveltekit-app -p 3000:3000   -e ORIGIN=http://localhost:3000 sveltekit-app

Open http://localhost:3000 and test a form submission as well as a page request. ORIGIN tells SvelteKit the public origin of the application. In production, set it to the actual HTTPS origin, such as https://app.example.com, without a path.

If you rely on forwarded headers instead, only trust headers supplied by your reverse proxy. An explicit ORIGIN is simpler for a service with one public domain.

Build-time and runtime variables

SvelteKit distinguishes four environment modules:

  • $env/dynamic/private: Read at runtime; available only to server code.
  • $env/dynamic/public: Read at runtime; public-prefixed values are available to browser code too.
  • $env/static/private: Fixed at build time; available only to server code.
  • $env/static/public: Fixed at build time; public-prefixed values are available to browser code too.

The default public prefix is PUBLIC_. Never put secrets in public variables. A static value is fixed during the build; changing an environment variable when the container starts will not change that compiled value.

For runtime configuration, pass variables with docker run -e or your hosting platform's environment settings. Do not assume the production server loads your local .env file automatically.

Add a health endpoint

Create src/routes/health/+server.ts:

export function GET() {
  return new Response("ok");
}

Then add this instruction to the runtime stage of the Dockerfile:

Dockerfile
HEALTHCHECK --interval=30s --timeout=3s   CMD node -e "fetch('http://127.0.0.1:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"

This checks that the HTTP server responds. It does not verify your database or every dependency. Add deeper readiness checks only if your deployment needs them.

Memory and deployment

NODE_OPTIONS=--max-old-space-size=512 limits V8's old-generation heap, not the whole process. Native allocations, buffers, and other memory still count against the container limit. Leave headroom and measure usage rather than treating a heap setting as a guarantee against crashes.

You can inspect the image with docker scout quickview sveltekit-app if Docker Scout is installed. Rebuild it when dependencies or base images need updates.

To deploy on Sliplane, commit the Dockerfile, choose the repository, set HTTP port 3000, and set ORIGIN to the public service origin. Test form actions again after adding a custom domain.

Welcome to your cloud platform

Sliplane makes it simple to deploy and scale your apps in the cloud. Start with a container or your favorite framework and grow from there. Try it now and get started in minutes!