A Dockerfile is executable build documentation
A Dockerfile looks simple because each instruction is short, but every line changes caching, image size, security and reproducibility. Many problems are not syntax errors. The image builds successfully and only later turns out to be slow, huge or fragile.
A useful review therefore asks two questions: will this build, and will it keep behaving predictably when the project changes?
Using an unnecessarily large base image
The base image becomes the foundation of every layer. Choosing a full operating-system image when the application needs only a runtime can add hundreds of megabytes and increase the number of packages that need security updates.
Choose the smallest base that still gives your application the libraries and debugging capability it genuinely needs. “Smallest possible” is not always best; “small enough and operationally understandable” is a better target.
Breaking the build cache with COPY order
Dependency installation is often expensive. If you copy the entire project before installing dependencies, any source-code change can invalidate the dependency layer.
This layout allows Docker to reuse the dependency layer while only application source changes. The same idea applies to other ecosystems: copy the files that define dependencies first, install, then copy the frequently changing code.
Using package managers without reproducible inputs
For Node projects, a lockfile plus npm ci is usually more reproducible than a loose npm install. Similar rules apply to Python, Java and other ecosystems. Production builds should not silently resolve a completely different dependency tree just because the image was rebuilt a week later.
Leaving build tools in the runtime image
Compilers, package managers and source files may be necessary during build but unnecessary at runtime. Multi-stage builds separate those jobs. The first stage compiles or bundles the application; the final stage contains only what is needed to run it.
This reduces image size and the number of components exposed in production.
Running the application as root by default
Many base images start as root. If the application does not need elevated privileges, create or use a non-root user and switch to it before the runtime command. Container isolation is not a reason to ignore basic privilege reduction.
Copying secrets into the image
Secrets do not become safe because a later layer deletes them. Image history can preserve data from earlier layers. API keys, private certificates and production credentials should be injected at runtime through the deployment environment or an appropriate secret mechanism, not baked into the image.
Ignoring the build context
A missing .dockerignore can send node_modules, build output, local caches, logs or credentials to the Docker daemon. That slows builds and can accidentally make sensitive files available to COPY instructions. Keep the build context intentionally small.
Using one huge RUN command without understanding failure behavior
Combining commands can reduce layers, but dense shell chains also become harder to inspect. If you use pipes, understand how the shell reports failures. A command in the middle of a pipeline can fail while the final command still exits successfully, depending on the shell.
A review checklist before you build
- Is the base image appropriate and pinned as deliberately as your process requires?
- Are dependency-definition files copied before the main source tree?
- Is dependency installation reproducible?
- Can build-only tooling be removed with a multi-stage build?
- Does the runtime use a non-root user where practical?
- Are secrets excluded from the image and build context?
- Is a useful
.dockerignorepresent? - Are commands structured so failures are visible?
Lint before spending time on a build
The GhostBladeZero Dockerfile Linter can catch suspicious instructions and style problems before you wait for an image build. Use the linter output as a review queue, then decide whether each warning is relevant to your actual deployment.