INTRO
Single-stage `FROM node` images with the full repo and devDependencies are easy - and fat. Multi-stage builds copy only what you need to run: built JS, production node_modules, and a slim runtime. That reduces attack surface and cold-start pull time.
Builder stage vs runner stage
Stage 1: install all deps, run `npm run build` or `tsc`. Stage 2: fresh base, copy `package*.json`, `npm ci --omit=dev`, copy `dist/` from builder.
Use `node:20-bookworm-slim` or distroless for runtime; avoid shipping gcc and test runners.
code-snippet.dockerfile
FROM node:20-bookworm-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-bookworm-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]Health checks and graceful shutdown
Expose `/healthz` that checks process up, not full DB depth (unless orchestrator requires it). Kubernetes uses probes; Docker Compose uses `HEALTHCHECK`.
Handle SIGTERM: stop accepting new connections, drain in-flight requests, then exit - prevents dropped uploads and partial writes.
KEY TAKEAWAYS
“Separate build tools from runtime artifacts with multi-stage builds.”
- 02
Use `npm ci --omit=dev` in the final image.
- 03
Run as non-root; add health checks and graceful shutdown.
CONCLUSION
Closing thought
Docker for Node is mostly about what you leave out of the final image. Multi-stage builds are the default pattern, not an optimization. Ready to talk scope? Use Contact — or browse Services for fit.
CONTINUE
Related pages
UP NEXT
Structured Logging for Node.js APIs
Why printf-style logs break observability, and how Pino-style JSON logs make search and alerts possible without vendor lock-in first.
Read next article