Multi-stage builds for smaller images

A multi-stage build uses multiple FROM statements in one Dockerfile. Only the final stage ships — earlier stages are discarded.

FROM node:24-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:24-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]

The builder stage has all dev dependencies and source files. The final stage gets only the compiled output. A typical Node app goes from 1.2GB to under 200MB.

Rules of thumb

Only copy what runs. Don’t COPY . . in the final stage — enumerate exactly what the app needs.

Pin your base images. node:24-alpine can change. Use digest pins for production: node:20-alpine@sha256:....

Build args in the right stage. A --build-arg passed at build time is only visible in the stage where ARG is declared. Declare it in each stage that needs it.

Inspecting layer size

docker history <image> --no-trunc
docker image inspect <image> --format='{{.Size}}'

dive is the most useful third-party tool for auditing what’s in each layer.