Multi-stage builds

# Stage 1 — build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2 — runtime (no build tools, much smaller)
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/index.js"]

Layer caching

Order Dockerfile instructions from least to most frequently changing. Copy package.json and run npm install before copying source code — the dependency layer is cached until package.json changes.

Security hardening

  • Run as a non-root user (USER node above).
  • Use specific image tags, never :latest in production.
  • Scan images with Trivy or Docker Scout in CI.
  • Use .dockerignore to exclude node_modules, .git, and secrets from the build context.

Size optimisation

Use Alpine-based images. Combine RUN commands to reduce layers. Remove build tools and caches in the same layer they are installed.