Why Multi-Stage Dockerfiles?
A single-stage build ships your build tools, source code, and dev dependencies to production — bloating the image and expanding the attack surface. Multi-stage builds split the build environment from the runtime environment, cutting final image sizes by 60–90% and eliminating leaked credentials, compilers, and test suites.
Layer Cache Optimization
Copy package.json before source code so Docker only re-runs npm install when dependencies change — not on every code edit. This turns 3-minute CI builds into 20-second ones.
Non-root User
Running as root inside a container means a breakout gives root on the host. Adding a dedicated user with useradd is a one-liner that closes this vector entirely.
Health Checks
Without HEALTHCHECK, Docker and orchestrators like Kubernetes consider a container healthy the moment it starts — even if your app crashed. A proper check polls your /health endpoint and enables zero-downtime deploys.
OCI Labels
Standard org.opencontainers.image.* labels attach metadata — version, source repo, build date — directly to the image. Tools like Trivy, Harbor, and Portainer surface these automatically.
distroless vs Alpine vs slim
distroless has no shell — minimal attack surface. Alpine is tiny (~5 MB) but uses musl libc, which can cause issues with some native modules. slim variants are Debian-based but stripped down — safest compatibility.
.dockerignore is critical
Without it, node_modules, .git, and local .env files get sent in the build context — leaking secrets and slowing every build. Always use one.