each layer keeps what it added, so deleting files later never shrinks them; multi stage builds do
Study this properly
Free flashcard deck: Docker Compose Recipes For Local Dev - 217 cards
Start studyingDocker images grow because every instruction in a Dockerfile produces a permanent layer, and layers are additive. Removing a file in a later step only marks it as hidden in an upper layer; the bytes still live in every layer beneath. The image you push is the sum of everything those layers ever wrote to the filesystem.
A Docker image is an ordered stack of read-only directories. Each FROM, RUN, COPY, and ADD line freezes the result of that step into a new directory and records the difference against the previous one. When a container starts, the runtime stacks those directories on top of each other and presents the union as a single root filesystem. A file is "gone" only when no directory in the stack contains it. A later rm adds a directory whose only job is to say "this path points to nothing," while the original copy sits untouched in an earlier directory. Tools that report on the final view, such as docker history or a shell inside the running container, see the deletion. Anything that walks the raw layers sees the original file and its size.
The fix is to make sure the cleanup happens before the layer is sealed. A shell pipeline such as apt-get update && apt-get install -y --no-install-recommends pkg && rm -rf /var/lib/apt/lists/* writes the package files and deletes them inside the same step, so the directory that gets frozen contains only the trimmed result. If the install and the cleanup were in separate RUN lines, the install directory would be sealed first, and the second RUN would just add a small "hide" directory on top of a large one. The trick is not the rm; it is the absence of a layer that contains the junk.
A typical Debian-based build might pull in a compiler, headers, and build tools weighing several hundred megabytes. A naive Dockerfile that runs apt-get install, compiles, then runs apt-get purge in a later step often produces an image close to the original install size, say 800 MB. Rewriting it as one RUN that installs, builds, and purges in the same layer can drop the same image into the 200 MB range. Adding a multi-stage build that copies only the compiled binary into a slim base can bring it under 50 MB. The savings come from layers that were never written, not from files that were later deleted.
Single-layer chaining has a cost: Docker cannot cache partial progress inside that step. If the long pipeline changes, every command runs again. Multi-stage builds share that cost for the build stage but keep the final stage small and cacheable. There is also no point in aggressive trimming for images that are pulled once and cached forever on a single host; the bandwidth and storage pressure that motivates slim images mostly matters in registries and across many hosts.
Many people expect docker history to expose bloat because it lists each step's size. Those sizes describe the bytes added by that step, not the bytes present in the image. A small "delete cache" step sitting on top of a large "install packages" step looks harmless in history while contributing to a heavy image. Treat history as a ledger of additions, not as a list of what is currently stored.
Cram I deleted all the build tools from my Docker image. So it should be small now, right?
Rep Not at all. Each line in your Dockerfile creates a layer, and every layer keeps what it added, even if a later line removes it. The cleanup looks like it worked, but the bytes are still in the image.
Cram So a later RUN rm does not actually shrink the image?
Rep Correct. The rm creates a new layer that hides the file. The old layer underneath still has it. The image you push and pull includes every layer you ever stacked.
Cram But docker history shows the file is gone, I checked.
Rep History shows the current view, the last layer wins. The union file system overlays them. To shrink the image, you have to stop adding the bytes in the first place.
Cram So how do real Dockerfiles keep images small?
Rep Two habits. First, chain your RUN commands so the install and the cleanup land in the same layer, so the cache files never get added. Second, use a multi stage build, where the heavy compiler lives in one stage and the final image copies only the binary.
Cram So the trick is doing the cleanup in the same RUN that creates the junk?
Rep Exactly. Apt install, then apt clean, then rm the cache, all in one line. One layer, no leftover bytes. That is the only way to keep the weight off the image.
Cram And multi stage builds, why do they save so much?
Rep Because the final image never contains the toolchain at all. You build in a fat stage with the compiler, then copy just the compiled artifact into a slim stage. The build stage can be a gigabyte, the runtime image can be under fifty megabytes.
Cram So a one gigabyte build can ship as a fifty megabyte image?
Rep That is the point. Multi stage lets you keep the heavy machinery during development and discard it before shipping. The image you pull only carries what your app needs to run.
Cram Then the rule is: one RUN per concern, and multi stage for any toolchain?
Rep That is the cleaner Dockerfile. Combine commands so deletion lands in the same layer, split stages so the final image drops the build chain. Less to push, less to pull, less to scan.
Cram So a huge image is not a mystery, it is a Dockerfile decision?
Rep Yes. Every byte you ship was a byte you asked for in a layer. Pick the layers you keep and the stages you copy. The image you want is the one you build deliberately.