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