- Use multi-stage builds to keep production images under 200MB — copy only built artifacts to final stage
- Use BuildKit cache mounts for package managers: --mount=type=cache,target=/root/.npm
- Order COPY instructions from least to most changing: lock files first, then source code
- Use alpine or slim base images; distroless for maximum size reduction without shell access
- Enable BuildKit (DOCKER_BUILDKIT=1) for parallel stage execution and better caching
- Multi-stage builds: build stage (1GB+ with SDKs) → production stage (<100MB with runtime only)
- BuildKit cache mounts: --mount=type=cache,target=/root/.npm (or /root/.cache/pip, /go/pkg/mod)
- COPY ordering for cache: COPY package.json package-lock.json ./ → RUN npm ci → COPY . . (source last)
- Base image selection: distroless (~20MB), alpine (~5MB base), slim (~80MB) — match security vs. debugging needs
- Enable BuildKit: DOCKER_BUILDKIT=1 or set in daemon.json for parallel multi-stage execution
- Use .dockerignore aggressively: node_modules, .git (~100MB+), test files, documentation
- Combine RUN commands: fewer layers = smaller image; clean caches in the same RUN step
- Use dive tool to analyze layer sizes and identify wasted space: dive your-image:tag
- For Node.js: npm ci --omit=dev; for Python: pip install --no-cache-dir; for Go: CGO_ENABLED=0 go build
- Build with --platform for multi-arch support: linux/amd64,linux/arm64 for both x86 and Graviton/Apple Silicon
- Use build arguments for version pinning: ARG NODE_VERSION=22 → FROM node:${NODE_VERSION}-slim