Multi-stage Dockerfile for Node.js (2026)
build stage that installs dependencies and builds, and a
slim runtime stage that copies only what's needed and runs as a non-root user. This cuts
image size and shrinks the attack surface. Copy-paste template below.
The template
# Build stage — has dev deps + toolchain
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build --if-present
# Runtime stage — slim, only what's needed, non-root
FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package*.json ./
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD node -e "fetch('http://localhost:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "dist/index.js"]
Why each part matters
Two stages
The build stage carries your full toolchain and dev dependencies. The runtime stage
starts fresh from a slim base and copies in only the built artifact and production
node_modules — so compilers, caches, and dev deps never ship.
Non-root user
The official Node images include a node user. Running as it (USER node)
means a compromised process isn't root inside the container — a cheap, large security win.
HEALTHCHECK
Lets your orchestrator know when the container is actually serving, not just started.
The example hits a /health endpoint with Node's built-in fetch — no extra tools.
Common mistakes
- Using
npm installinstead ofnpm ci—ciis reproducible and respects the lockfile. - Copying the whole build stage — copy only
node_modules+dist, not the source tree. - No
.dockerignore— without it you shipnode_modulesand.gitinto the build context.
Going further
For a complete, ready-to-run project — Dockerfile, docker-compose with Postgres, CI, and a
health endpoint already wired — the SysBuild Pro Pack ships five such
stacks (including node-express-postgres and nextjs-standalone) for $29 one-time.