Docker Containerization Best Practices
Image layering, distroless bases, multi-arch builds, and supply-chain security for production containers.
Why
A naive 1.5GB image built on node:20 with root user and unscanned dependencies is a security incident waiting to happen. Disciplined containerization cuts image size 80%, removes 90% of CVE surface via distroless, and enables multi-arch (amd64+arm64) for cross-platform deploys.
When
Apply these practices to any container heading to production, especially regulated industries (finance, healthcare) needing SBOM and signed images. Skip hardening for local dev images (use docker-compose override).
How
Example
Common Problems
- ⚠Distroless breaks debugging — no shell to exec into, use a debug variant or ephemeral sidecar
- ⚠Multi-arch build is 3x slower — arm64 emulation on amd64 host via QEMU is slow, use native arm64 builder
- ⚠cosign verification not enforced — images signed but K8s admits unsigned too, add Kyverno/OPA policy
- ⚠Trivy fails on distroless — no package manager to query, use --scanners vuln with OS DB
Best Practices
- ✓Pin base image by digest — FROM node:20-alpine@sha256:abc... not :20-alpine, prevents supply-chain swap
- ✓Use BuildKit cache mounts — RUN --mount=type=cache,target=/root/.npm for fast rebuilds
- ✓Sign AND verify in cluster admission — Kyverno policy rejects unsigned images at deploy time
- ✓Keep .dockerignore tight — exclude test fixtures, .git, node_modules to shrink context 10x
FAQ
Multi-stage build — why and how?
It separates the build environment (with devDependencies, compilers) from the runtime image. Build in stage 1, copy only dist/ to stage 2 (FROM node:18-alpine). The final image has no source or build tools — smaller and more secure.
How do I keep secrets out of my Docker image?
Never COPY .env into the image or ARG secrets — they're visible in image layers. Use Docker BuildKit secrets (--secret) or mount at runtime via -e or --env-file. Scan with trivy to verify no secrets leaked.
Why should I pin image versions instead of using :latest?
:latest changes without warning — your build breaks when the base image updates. Pin to a specific digest (node:18-alpine@sha256:abc...) for reproducible builds. Update pins deliberately in a scheduled PR.
How do I optimize Docker layer caching?
Order Dockerfile commands from least to most frequently changing. Copy package.json (rarely changes) before source (changes every commit). Each changed layer invalidates all layers below it.
What's the difference between CMD and ENTRYPOINT?
ENTRYPOINT is the fixed command (e.g., node server.js); CMD is default args that can be overridden (docker run <img> --port 4000). Use ENTRYPOINT when the container is a single-purpose executable, CMD when it's a generic base.