recipes / container-images / dockerfile
A minimal multi-stage Dockerfile
Install with npm ci, build in one stage, copy only what runs into the final image.
Keep dev dependencies, source maps and the package manager out of the image that runs in production. Do it with two stages.
# syntax=docker/dockerfile:1
FROM node:24-bookworm-slim AS build
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
COPY src ./src
FROM gcr.io/distroless/nodejs24-debian12:nonroot
WORKDIR /app
ENV NODE_ENV=production PORT=3000
COPY --from=build --chown=nonroot:nonroot /app/node_modules ./node_modules
COPY --from=build --chown=nonroot:nonroot /app/package.json ./
COPY --from=build --chown=nonroot:nonroot /app/src ./src
EXPOSE 3000
CMD ["src/server.js"]
Order matters
Copy package.json and the lockfile before the source. The dependency layer is
rebuilt only when the lockfile changes, so day-to-day builds reuse it.
.dockerignore
node_modules
npm-debug.log
.git
.env*
dist
coverage
Without it, COPY sends your local node_modules into the build context and
can overwrite the clean install.
Notes
npm cifails if the lockfile andpackage.jsondisagree. That is a feature.- Pin the base image by digest in CI for reproducible builds.
- If you have a TypeScript build step, run it in the build stage and copy
dist/instead ofsrc/. - The final image has no shell, so
RUNdoes not work there. Do all the work in the build stage. - See distroless images for what the runtime stage gives you.
See it applied
- examples/full/Dockerfile in the repository
- examples/full - every recipe applied to one app
Updated 2026-09-10 · tags: dockerfile, docker, build · edit on GitHub