> BLOG/POSTS/BUILDING A SVELTEKIT CONTAINER WITH CHAINGUARD NODE IMAGE
← Back to blog (or backspace)

How to build a SvelteKit docker container using Chainguard's NodeJS image

Oct 21 2025

I could not find an example, so enjoy!

This was more difficult than I thought it was going to be, but in the end, we’re up and running and I’ve converted all of my SvelteKit containers to hardened chainguard images, including the website you’re looking at now!

FROM cgr.dev/chainguard/node:latest-dev AS builder

USER root
WORKDIR /app
RUN chown -R node:node /app

USER node
COPY --chown=node:node package*.json ./
RUN npm ci

COPY --chown=node:node . .
RUN npm run build

FROM cgr.dev/chainguard/node:latest

USER root
WORKDIR /app
RUN chown -R node:node /app

USER node
ENV NODE_ENV=production

COPY --chown=node:node --from=builder /app/build ./build
COPY --chown=node:node --from=builder /app/package*.json ./
RUN npm ci --omit=dev

EXPOSE 3000
CMD ["build/index.js"]

Here is the compose file that I’m using. I will note that this compose file is for use in a Docker swarm with a Traefik ingress container. You will need to modify it to run correctly outside of a swarm, and change the network names and environment variables, obviously.

services:
  sveltekit:
    image: jesseid/jesseid:latest
    networks:
      - traefik-public
      - jesseid
    environment:
      PORT: 3000
      NODE_ENV: production
      GITHUB_TOKEN_FILE: /run/secrets/github_token
    secrets:
      - github_token
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
      update_config:
        parallelism: 1
        delay: 10s
      rollback_config:
        parallelism: 1
        delay: 10s
      labels:
        - "traefik.enable=true"
        # Main router for jesse.id
        - "traefik.http.routers.jesseid-web.entrypoints=web-https"
        - "traefik.http.routers.jesseid-web.rule=Host(`jesse.id`)"
        - "traefik.http.routers.jesseid-web.tls=true"
        - "traefik.http.routers.jesseid-web.tls.certresolver=letsencrypt"
        - "traefik.http.services.jesseid-web.loadbalancer.server.port=3000"
        # WWW to non-WWW redirect
        - "traefik.http.routers.jesseid-www-redirect.entrypoints=web-https"
        - "traefik.http.routers.jesseid-www-redirect.rule=Host(`www.jesse.id`)"
        - "traefik.http.routers.jesseid-www-redirect.tls=true"
        - "traefik.http.routers.jesseid-www-redirect.tls.certresolver=letsencrypt"
        - "traefik.http.routers.jesseid-www-redirect.middlewares=jesseid-redirect-www-to-non-www"
        - "traefik.http.middlewares.jesseid-redirect-www-to-non-www.redirectregex.regex=^https://www\.(.+)"
        - "traefik.http.middlewares.jesseid-redirect-www-to-non-www.redirectregex.replacement=https://$${1}"
        - "traefik.http.middlewares.jesseid-redirect-www-to-non-www.redirectregex.permanent=true"

secrets:
  github_token:
    external: true

networks:
  traefik-public:
    external: true
  jesseid:
    external: true

And as an added bonus, here is the build.sh script that I use for this site, which scans the image with trivy once complete. Also important: it builds for ARM64 architecture. So if it doesn’t work for you, switch that to AMD64.

#!/bin/bash
set -e

# Load environment variables from .env file if it exists
if [ -f .env ]; then
  export $(cat .env | grep -v '^#' | xargs)
fi

# Extract version from package.json
VERSION=$(node -p "require('./package.json').version")

# Function to scan image with Trivy
scan_image() {
  echo "Scanning image with Trivy for vulnerabilities..."
  trivy image     --severity HIGH,CRITICAL     --ignore-unfixed     --scanners vuln     jesseid/jesseid:$VERSION

  if [ $? -eq 0 ]; then
    echo "✓ No HIGH or CRITICAL vulnerabilities found"
  fi
}

# Check for --scan flag
if [ "$1" = "--scan" ]; then
  echo "Scanning existing image jesseid/jesseid:$VERSION"
  scan_image
  exit 0
fi

# Build Docker image for arm64 platform with build arguments
# Note: AWS credentials can be dummy values here if using IAM roles on EC2
docker build --platform linux/arm64   -t jesseid/jesseid:$VERSION ./

# Tag as latest
docker tag jesseid/jesseid:$VERSION jesseid/jesseid:latest

# Push both tags
docker push jesseid/jesseid:$VERSION
docker push jesseid/jesseid:latest

echo "Successfully built and pushed version $VERSION"

# Scan image with Trivy
scan_image
> JESSE.ID
© 2025 All rights reserved by 👍👍 This Guy