Containerization · · 6 min read

How to containerize a SvelteKit application

This article guides you through the steps required to containerize your SvelteKit application

How to containerize a SvelteKit application

Introduction

In this article, I'll guide you through the process on how to containerize a SvelteKit application.

I wont go into the reasons why containerization is a good thing and why you would want it. Since you are reading this, I assume you're already convinced that in todays cloud-drive area, its a baseline infrastructure discipline 😄

SPONSORED
CTA Image

I build scalable cloud platforms & SaaS solutions, modernize critical software, and help organizations make high-stakes technology decisions with confidence.

👉🏼 Let's discuss your architure!

Learn more

Prerequisites

Before we'll get to the actual Dockerfile there are couple of things we need to carry out and understand beforehand.

Reconfigure your Svelte application to use the Node adapter

Since we will use the node:22 container image as a base image your application needs to be able to run on that platform. The setup is fairly simple, instructions can be found here.

Understand how SvelteKit handles environment variables

SvelteKit provides the following modules to access environment variables. These are:

  • $env/dynamic/private
  • $env/dynamic/public
  • $env/static/private
  • $env/static/public

Since the proper usage of these modules impacts how we need to build the container, it also has an impact on the security of your build artifact (container image). Let's have a closer look.

$env/dynamic/private

  • Provides access to runtime environment variables as defined by the platform (process.env)
  • Won-t be statically injected to your code at build time
  • Only includes variables NOT beginning with PUBLIC_
  • Cannot be imported into client-side code

👉🏼 So this is the candidate to use for injection secret API keys, etc. at runtime.

$env/dynamic/public

  • Only includes variables beginning with PUBLIC_
  • Can safely be exposed to client-side code
  • Must all be sent from server to client, causing slightly larger network requests

👉🏼 This module is a good fit to inject variables like for example version strings.

$env/static/private

  • Values are statically injected into the bundle at build time
  • Cannot be impoted into client-side code
  • All environment variables referenced in the code should be declared in .env file, even if they don't have a value until the app is deployed

👉🏼 The usage of this module should scare you and I recommend not using it, since this would bake potential secrets into the code and therefor will end up in the container image.

⚠️
You should avoid using the module $env/static/private, since it will bake potential secrets into the bundle, that will end up in the container image.

If you later plan to publish your container image 💥 boom💥 you have leaked a credential into the public.

$env/static/public

  • Only includes variables beginning with PUBLIC_
  • Can safely be exposed to client-side code
  • Values are are replaced statically at build time
  • Variables will end up in the bundle

👉🏼 These type of variables need to be passed at build time, which implies a change of this variable requires a rebuild of the container.

Running on the node adapter without Docker

Let's first understand how the application needs to run on the adapter-node before we'll get to the Dockerfile and building the actual image.

  1. Build your application with npm run build. This will output the build artifacts to the folder build if using the default options for adapter-node.
  2. Copy build/, package.json and package-lock.json to target folder (e.g. /app)
  3. Run npm ci --omit dev from /app to generate production dependencies
  4. Start the application with node build

Example SvelteKit code for demo

The following instructions are based on the following simple SvelteKit example.

# Can only be used with $env/static/public or dynamic/public.
# Needs to be passed as --build-args when using static/public
PUBLIC_SITE_NAME="blog-article-demo"
PUBLIC_SITE_VERSION="v1.0.0"

# Can only be used with $env/dynamic/private (avoid static/private)
API_KEY="41519720-7728-4f1e-869d-d6ab9b037b93"

.env.local

This runs server-side, why it's safe to dynamically read private environment variables.

import { env } from "$env/dynamic/private";
import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async () => {
  if (env.API_KEY) {
    console.log("API_KEY is set");
  } else {
    console.error("API_KEY is not set");
  }

  return {};
};

+page.server.ts

Since +page.svelte runs client-side, we are safe to use $env/dynamic/public and $env/static/public. Btw. if we would have tried to use $env/dynamic/private here, the build process (or even earlier the HRM) would have failed.

<script lang="ts">
  // This will be fetched at runtime
  import { env } from "$env/dynamic/public";

  // This will be baked into the bundle
  import { PUBLIC_SITE_NAME } from "$env/static/public";
</script>

<h1>Environment Variables</h1>

<h2>Dynamically imported</h2>

{#if env.PUBLIC_SITE_VERSION}
  <pre>{env.PUBLIC_SITE_VERSION}</pre>
{:else}
  <pre>PUBLIC_SITE_VERSION is not set</pre>
{/if}

<h2>Statically imported</h2>

<pre>{PUBLIC_SITE_NAME}</pre>

+page.svelte

The actual Dockerfile

This is what you came here for, a minimal, non-root, multi-stage Dockerfile using node:22-bookworm-slim.

# First builder stage
FROM node:22-bookworm-slim AS builder

# needs to be defined after FROM and before npm run build
ARG PUBLIC_SITE_NAME
ENV PUBLIC_SITE_NAME=$PUBLIC_SITE_NAME

# Set the workding directory for any RUN, CMD, ENTRYPOINT, COPY and ADD
# instructions that follow. It will create the specified directory if it
# doesn't exist
WORKDIR /temp

# Copy source code from host to builder stage
COPY . ./

# npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828).
RUN npm ci
# Actually build the code
RUN npm run build

# Second runner stage
FROM node:22-bookworm-slim AS runner

WORKDIR /app

# Setting NODE_ENV to anything but production is considered an antipattern.
# https://nodejs.org/en/learn/getting-started/nodejs-the-difference-between-development-and-production
ENV NODE_ENV=production

# Copy build artefacts into runner stage
COPY --from=builder --chown=node:node /temp/build build/
COPY --from=builder --chown=node:node /temp/package.json /temp/package-lock.json ./

# Generate production dependencies
RUN npm ci --omit dev

# Drop privileges
USER node

# We are using the node adapter
EXPOSE 3000

CMD ["node", "/app/build"]

Dockerfile

... and its companian the .dockerignore file

.env*
node_modules
.git
.vscode

As you can see, I am defining the build argument PUBLIC_SITE_NAME since it needs to be passed at build time.

⚠️
Please don't copy any .env files to your container image as this could potentially leak sensitive information.

Also, if your application requires secrets at build time, don't pass them as build-args to Docker. Instead use the secure RUN --mount=type=secret

Of course, the Dockerfile can be further optimized, e.g. by using an additional deps stage for better caching. But I think it is a good starting point.

Building the image now boils down to...

docker build -t blog-article:demo --build-arg PUBLIC_SITE_NAME=foobar .

And for executing, we can either pass variable by variable, using -e or point docker to an .env file using the --env-file parameter.

docker run -p 3000:3000 -e PUBLIC_SITE_VERSION="v2.0.0" blog-article:demo
docker run -p 3000:3000 --env-file=.env blog-article:demo

Conclusion

  • Use adapter-node in your application for preparation
  • Use ARG in your Dockerfile and pass build time arguements with the --build-args parameter
  • Don't pass secrets with build-args but use RUN --mount=type=secret instead
  • Avoid the module $env/static/private when containerizing SvelteKit apps
  • Remember rebuilding your application when any of your $env/static/public variables are changing

Further reading

Variables
Using build arguments and environment variables to configure builds
$env/dynamic/private • SvelteKit Docs
$env/dynamic/private • SvelteKit documentation
Env Variables and Modes
Next Generation Frontend Tooling
The Twelve-Factor App
A methodology for building modern, scalable, maintainable software-as-a-service apps.

Read next