About the Dockerfile Best Practices cheat sheet
Almost every Dockerfile problem is one of two things: a layer order that defeats the build cache, or an image that ships far more than it needs to run. Both come from writing the file in the order you happen to think of the steps rather than in the order the cache wants.
The cache rule is simple and rarely followed. Each instruction produces a layer, and changing one invalidates everything after it. Copying your whole source tree before installing dependencies means every source edit reinstalls every dependency. Copy the manifest, install, then copy the source — that single reordering is usually the largest build-time win available.
The size rule is multi-stage builds. Compilers, headers, test fixtures and package caches belong in a build stage; the final stage should contain the artefact and its runtime dependencies. This is a security control as much as a size one, because a toolchain that is not in the image cannot be used by anyone who gets into it.
Frequently asked questions
Why does my build reinstall dependencies on every code change?
Because the source is copied before the install step, so editing any file invalidates the layer the install sits on. Copy only the dependency manifest first, run the install, then copy the rest of the source. The install layer is then reused until the manifest itself changes, which is usually rarely.
Should I use alpine for smaller images?
Sometimes, and it is not free. Alpine uses musl instead of glibc, which can cause subtle runtime differences and notably slower DNS resolution in some workloads, and Python wheels often need compiling rather than installing prebuilt. Distroless or slim variants frequently give most of the size benefit without the libc change. Measure both rather than assuming alpine is the smaller answer.
How do I keep secrets out of the image?
Never pass them as build arguments — build args are visible in image history to anyone who pulls it, and removing a file in a later layer does not remove it from earlier ones. Use BuildKit secret mounts, which make the value available during a single instruction without persisting it into any layer. For runtime configuration, inject at run time rather than baking it in.
Why should containers not run as root?
A process running as root inside the container is root in the namespace, so a container escape or a mounted host path starts from the strongest position rather than the weakest. Create a user in the image and switch to it before the entrypoint. Some platforms enforce this — OpenShift assigns arbitrary non-root UIDs, so images that assume root simply fail to start.
What is the difference between COPY and ADD?
COPY does exactly what it says. ADD additionally unpacks local archives and can fetch remote URLs, both of which are surprising behaviours to inherit accidentally. Default to COPY, and use ADD only when you specifically want archive extraction. For remote files, fetching explicitly makes the download visible and lets you verify a checksum.
Need this managed for you, not just automated?
We're also a hands-on DevOps consultancy — Kubernetes, CI/CD, and cloud infrastructure.