What makes a good Dockerfile?
A good Dockerfile builds fast, produces a small image, and exposes little attack surface. It orders instructions so the cache is reused, uses multi-stage builds to drop build tooling from the final image, and runs as a non-root user.
Why it matters
Image size and build time directly affect how fast you ship and how much you pay. A bloated image is slower to push and pull and carries more vulnerabilities. These practices are the difference between a 1GB image that takes minutes to deploy and a 50MB one that ships in seconds.
What to learn
- Ordering instructions so dependency layers cache
- Multi-stage builds to separate build and runtime
- Choosing a small, secure base image
.dockerignoreto shrink the build context- Running as a non-root user
- Pinning versions for reproducibility
- Scanning images for vulnerabilities
Common pitfall
Copying the whole project before installing dependencies, so every code change busts the dependency cache and reinstalls everything. Copy the lockfile, install dependencies, then copy the rest — that way a code-only change reuses the cached dependency layer and the build stays fast.
Resources
Primary (free):
- Docker — Building best practices · docs
- Docker — Multi-stage builds · docs
- Snyk — Docker security best practices · article
Practice
Take a working Dockerfile and improve it: reorder so dependencies cache before
code, convert it to a multi-stage build, switch to a slim base, add a
.dockerignore, and run as non-root. Compare the image size before and after.
Done when the final image is dramatically smaller and rebuilds are fast.
Outcomes
- Order instructions to maximize layer cache reuse.
- Shrink images with multi-stage builds and slim bases.
- Reduce the build context with
.dockerignore. - Run containers as a non-root user.