A Dockerfile
Beluga’s runtime needs dist/, dist-server/, db/migrations/, emails/,
public/ (for the bundled demo images), package.json, and production
node_modules. Paths resolve from the working directory, so the image starts from
the repository root. better-sqlite3 and sharp want a toolchain in the build
stage, which is why this uses the Debian-based Node image rather than Alpine.
Dockerfile
Section titled “Dockerfile”# syntax=docker/dockerfile:1
FROM node:22-bookworm-slim AS buildWORKDIR /appRUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ \ && rm -rf /var/lib/apt/lists/*COPY package.json package-lock.json ./RUN npm ciCOPY . .RUN npm run build && npm prune --omit=dev
FROM node:22-bookworm-slimWORKDIR /appENV NODE_ENV=productionCOPY --from=build /app/package.json ./COPY --from=build /app/node_modules ./node_modulesCOPY --from=build /app/dist ./distCOPY --from=build /app/dist-server ./dist-serverCOPY --from=build /app/db/migrations ./db/migrationsCOPY --from=build /app/emails ./emailsCOPY --from=build /app/public ./public# Run as the unprivileged user the base image provides, and make sure the# default data and assets locations are writable by it. A mounted volume# replaces these paths in production.RUN mkdir -p data public/assets && chown -R node:node /appUSER nodeEXPOSE 4000CMD ["node", "dist-server/server/index.js"].dockerignore
Section titled “.dockerignore”node_modulesdistdist-serverdatapublic/assets.env.env.*test-resultslegacy.gitpublic/assets is excluded because uploads belong on a volume or in a bucket, not
baked into an image. The bundled demo images live elsewhere under public/.
Running it
Section titled “Running it”docker build -t my-store .docker run --rm -p 4000:4000 \ -v beluga-data:/app/data -v beluga-assets:/app/public/assets \ -e SESSION_SECRET=$(openssl rand -base64 32) \ -e PUBLIC_URL=http://localhost:4000 \ my-storeThe log prints the setup token. Open http://localhost:4000/setup.
With a bucket for images and Postgres for the database, drop both volumes:
docker run --rm -p 4000:4000 \ -e DATABASE_URL=postgres://... \ -e ASSETS_S3_BUCKET=... -e ASSETS_S3_REGION=auto \ -e ASSETS_S3_ENDPOINT=https://... \ -e ASSETS_S3_ACCESS_KEY_ID=... -e ASSETS_S3_SECRET_ACCESS_KEY=... \ -e ASSETS_PUBLIC_URL=https://images.example.com \ -e SESSION_SECRET=... -e PUBLIC_URL=https://shop.example.com \ my-storeWhat this is careful about
Section titled “What this is careful about”npm run buildbeforenpm prune, because the build needs devDependencies and the runtime must not ship them.db/migrations/is copied. Migrations run at boot; an image without them fails to start, not just to upload.emails/is copied. Templates are read at send time.- One process. No supervisor, no nginx inside the container; the platform’s edge or a proxy outside handles TLS.