A container image, discussed generally in Serverless vs. Containers vs. VMs, is only as good as how it’s built. Two images running the same application can differ by an order of magnitude in size, startup time, and attack surface, purely based on how the Dockerfile that produced them was written.

How image layers actually work

A Docker image is built as a stack of layers, where each instruction in a Dockerfile typically creates a new layer on top of the previous one. Layers are cached: if an instruction and everything before it haven’t changed since the last build, Docker reuses the cached layer instead of re-executing it. Docker’s own best practices guide for building images is built around this mechanism directly — ordering instructions so that things that change rarely (installing system dependencies) come before things that change often (copying application source code) means a code change only invalidates the cache for the layers after it, not the entire build, which is what makes iterative local builds fast.

Multi-stage builds: separating build tools from the runtime image

Compiling or building an application often requires tools — compilers, build dependencies, package managers — that the application doesn’t actually need at runtime. A multi-stage build uses one stage, with the full build toolchain, to produce the compiled application, and then copies only the finished output into a second, much leaner final stage that becomes the actual image that ships. Everything from the build stage that isn’t explicitly copied over — the compiler, build-time dependencies, intermediate files — never makes it into the final image at all.

This is one of the single highest-impact techniques for reducing image size, because build toolchains are often far larger than the application they produce. It’s also a meaningful security improvement: a smaller final image with fewer installed tools has a correspondingly smaller attack surface — a compiler or package manager present in a production image is one more thing that could potentially be exploited if a container is ever compromised, and one that a properly multi-staged build simply never ships.

Choosing a minimal base image

The base image an application’s Dockerfile starts from sets a floor under how small the final image can possibly be. A full general-purpose operating system base image includes a large number of packages and utilities most applications never use; minimal or distroless base images strip this down to close to just what’s needed to run the application, meaningfully shrinking both image size and the number of components that could carry a vulnerability. The right choice depends on the application — some runtimes genuinely need more of a full OS environment than others — but defaulting to the smallest base image that actually works is a reasonable starting position rather than an afterthought.

Common mistakes that bloat images

  • Installing unnecessary packages, or not cleaning up package manager caches and temporary files after installing what’s actually needed, leaving build artifacts in the final image that were only ever needed transiently.
  • Poor layer ordering, copying application source code before installing dependencies, which invalidates the dependency-install layer’s cache on every single code change, even though the dependencies themselves didn’t change.
  • Not using a .dockerignore file, which lets unnecessary local files — build artifacts, local environment files, version control metadata — get copied into the build context and potentially into the image itself.
  • Baking secrets or credentials into image layers, which is both a bloat problem and a genuine security one, since image layers can often be inspected even after a value is “removed” in a later layer — a mistake covered in more detail in Secrets Management in CI/CD Pipelines.

Why this matters beyond disk space

Smaller images aren’t just tidier — they start faster (less data to pull and unpack, which matters directly for the compute models discussed in Serverless vs. Containers vs. VMs), transfer faster through CI/CD pipelines and across networks, and reduce the attack surface an attacker would have if a running container were ever compromised. None of this is achieved by accident; it’s a direct, traceable result of how deliberately the Dockerfile was written.

Key takeaway

Multi-stage builds and minimal base images are the two techniques that do the most to shrink container images, by separating build-time tooling from what actually ships and starting from as little as the application genuinely needs. Thoughtful layer ordering is what makes the resulting build fast to iterate on locally, not just fast to run in production — both are the product of deliberate Dockerfile design, not a default outcome.

This article explains general containerization concepts; specific tooling and syntax vary by platform. See our disclaimer.