Back to articles
4 min read

Deploying a SvelteKit Application with Docker

Build a production SvelteKit image, run it with Docker, and understand where a reverse proxy fits in.

SvelteKitDockerDevOps

Deploying a SvelteKit Application with Docker

Running an application locally is one thing. Running it reproducibly on another machine is another. Docker packages your compiled application and runtime environment into an image, so deployment does not require manually installing all the project's dependencies.

In this guide, we'll prepare a SvelteKit application using adapter-node, build a Docker image, and put the application behind a reverse proxy.

1. Know what you're deploying

During development, you usually run:

npm run dev

That server provides hot reload, development errors, and on-demand compilation. Production is different. With adapter-node, you typically use:

npm run build
node build

The first command builds the application. The second runs the Node.js server generated by SvelteKit.

The final container therefore needs the build output, production dependencies, and Node.js. It does not need to retain every build tool.

2. Write a multi-stage Dockerfile

A multi-stage build separates compilation from execution:

FROM node:24-alpine AS builder
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM node:24-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=3000

COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/build ./build

USER node
EXPOSE 3000
CMD ["node", "build"]

The builder stage installs all dependencies and compiles SvelteKit. The runner stage keeps only what is needed to start the app.

This example assumes adapter-node and an npm lockfile. If your build produces additional runtime assets, copy those as well.

3. Keep secrets out of the image

Create a .dockerignore file:

node_modules
.svelte-kit
build
.git
.env
.env.*
!.env.example

This helps prevent SMTP passwords and API keys from entering the build context. Production secrets should be provided when the container starts, not baked into the image.

4. Run the application with Compose

A compose.yaml file makes the runtime configuration reproducible:

services:
  app:
    build: .
    restart: unless-stopped
    env_file:
      - .env.production
    environment:
      HOST: 0.0.0.0
      PORT: "3000"
      NODE_ENV: production
    ports:
      - "127.0.0.1:3000:3000"

Start the service and inspect its logs:

docker compose up -d --build
docker compose logs -f app

Binding the port to 127.0.0.1 means it is reachable only from the host machine. This is useful if Caddy is installed directly on the host.

5. Put Caddy in front of SvelteKit

A reverse proxy accepts public HTTPS connections and forwards requests to your Node.js server.

If Caddy runs on the host, a basic Caddyfile might look like this:

example.com {
    encode zstd gzip
    reverse_proxy 127.0.0.1:3000
}

If Caddy runs inside Docker, 127.0.0.1 refers to the Caddy container itself. In that case, connect both services to the same Docker network and proxy to the service name, for example reverse_proxy app:3000.

Confusing host localhost with container localhost is a common source of deployment issues.

6. Build on the server or in CI?

On a small server with limited memory, compiling the app can consume a significant share of available resources. An alternative is to build the image in a CI pipeline and pull it onto the server.

A typical workflow is:

  1. A git push triggers the pipeline.
  2. The pipeline builds and publishes the Docker image.
  3. The server pulls the new image.
  4. Docker Compose replaces the application container.

Deployments become repeatable, and the production server no longer needs to compile the project itself.

7. Verify the deployment

Before calling the deployment complete, verify that the application listens on 0.0.0.0, HTTPS works, form submissions succeed, private environment variables are available, and logs do not expose secrets.

Remember that Docker is not a backup system. Persistent application data should be stored and backed up independently of containers.

Final thoughts

With adapter-node, Docker, and a reverse proxy, deploying SvelteKit can be straightforward. The essential part is understanding each component's role: SvelteKit serves the application, Docker provides a reproducible environment, and Caddy handles incoming HTTP(S) traffic.