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.jsonand 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’snodeentrypoint. Nevernpm 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:1to build,oven/bun:1-distrolessto 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
- A minimal multi-stage Dockerfile - Install with npm ci, build in one stage, copy only what runs into the final image.
- Distroless instead of Alpine or Debian slim - Ship the Node.js runtime and your app, nothing else. No shell, no package manager, non-root.
- 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.
- Build an image with Cloud Native Buildpacks - Get a production Node.js image without writing a Dockerfile, using pack and Paketo.
- 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.
- Containerize a Next.js app - Build with output standalone, copy three folders into a distroless image, run server.js directly.
- 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
- examples/full/Dockerfile in the repository
- examples/full - every recipe applied to one app
Updated 2026-09-10 · tags: dockerfile, docker, images, distroless, buildpacks · edit on GitHub