recipes / container-images

Container images for Node.js

What a production image should contain, and the recipes that get you there for plain Node.js, Bun, Next.js and TanStack Start.

A production image is the runtime, your code, its production dependencies and CA certificates. Nothing else. Everything below follows from that.

What good looks like

FROM node:24-bookworm-slim AS build          # tools live here
  npm ci --omit=dev, build TypeScript, bundle
FROM gcr.io/distroless/nodejs24-debian12:nonroot   # only this ships
  COPY --from=build ... ; CMD ["src/server.js"]
  • Two stages. Build tools, dev dependencies and source maps stay in the first stage.
  • Lockfile first. Copy package.json and the lockfile before the source, so the dependency layer is cached between builds.
  • Distroless runtime. No shell, no package manager, non-root user. Alpine and Debian slim are the fallbacks, not the goal.
  • Node as PID 1. CMD ["src/server.js"] under the image’s node entrypoint. Never npm start.
  • Secrets never touch a layer. Private registry tokens go in as BuildKit secrets.
  • Build once. The same image goes through every environment; configuration comes from the Pod spec.

Checklist

docker history --no-trunc image | grep -ci token      # 0
docker inspect image --format '{{.Config.User}}'      # 65532 or nonroot
docker run --rm --entrypoint sh image                 # should fail: no shell
docker images image --format '{{.Size}}'              # a Node.js API is ~200 MB, mostly the runtime

Notes

  • Bun has the same shape: oven/bun:1 to build, oven/bun:1-distroless to run.
  • Meta-frameworks add a build step and their own output folder, but the runtime stage is the same distroless image.
  • Buildpacks produce a reasonable image without a Dockerfile, at the cost of a bigger, shell-bearing base.

In this section

  1. A minimal multi-stage Dockerfile - Install with npm ci, build in one stage, copy only what runs into the final image.
  2. Distroless instead of Alpine or Debian slim - Ship the Node.js runtime and your app, nothing else. No shell, no package manager, non-root.
  3. Private packages with Docker build secrets - Pass NPM_TOKEN as a BuildKit secret. Never put it in an ARG, an ENV, or a copied .npmrc.
  4. Build an image with Cloud Native Buildpacks - Get a production Node.js image without writing a Dockerfile, using pack and Paketo.
  5. Containerize a Hono API - A Hono service on the Node.js adapter, built in two stages into a distroless image, with the Bun variant alongside.
  6. Containerize a Next.js app - Build with output standalone, copy three folders into a distroless image, run server.js directly.
  7. Containerize a TanStack Start app with Nitro - Add the Nitro Vite plugin, build a node-server bundle into .output, and run the entry with node in a distroless image.

See it applied

Updated 2026-09-10 · tags: dockerfile, docker, images, distroless, buildpacks · edit on GitHub