Understanding layer by layer where the disk space goes
An image quietly grows from 200 megabytes to over a gigabyte over the course of months, and nobody can immediately say which build step is responsible. docker history gives a first rough overview, the tool dive shows exactly which file in which layer takes up how much space.
Table of Contents
- 1. Why image size actually matters
- 2. docker history as a first overview
- 3. Where docker history hits its limits
- 4. Installing dive
- 5. Using dive interactively
- 6. Automating dive in a CI pipeline
- 7. Common causes of wasted disk space
- 8. A Dockerfile before and after
- 9. The practical analysis workflow combined
- 10. Summary
- 11. FAQ
1. Why image size actually matters
An oversized Docker image is not just a cosmetic annoyance, it has concrete effects on several levels at once: longer push and pull times in CI/CD pipelines, slower deployment cycles especially with frequent releases, higher storage requirements in the registry, and in auto-scaling scenarios noticeably longer time until a new instance is actually ready to serve traffic, because the image has to be fully downloaded before the container can even start.
Every Docker image is made up of a sequence of layers, where every instruction in the Dockerfile that changes the filesystem, such as RUN, COPY, or ADD, creates a new, immutable layer. These layers are stacked on top of each other and merged into a single, consistent view via a union filesystem such as OverlayFS. The problem: data written into one layer remains part of the image size even if it is deleted again in a later layer, because deleted files are only marked with a so-called whiteout marker, not physically removed from previous layers.
2. docker history as a first overview
The command docker history ships with every Docker installation and, without any additional installation, provides a list of every layer in an image, together with the command that created that layer and the size it contributes to the total image size. That makes it the obvious first step whenever an image is unexpectedly large: a glance at the size column immediately shows whether a single layer stands out, for example a RUN apt-get install command contributing several hundred megabytes.
By default, docker history truncates the executed command to a fixed length, which quickly becomes unreadable for complex, multi-line RUN commands with many chained instructions. The --no-trunc option shows the full command for every layer, which is almost always the better choice while troubleshooting, even though the output becomes considerably wider and less convenient for the terminal.
# Overview of all layers in an image
docker history myapp:latest
# Show full commands without truncation
docker history --no-trunc myapp:latest
# Just size and command, no header, for scripting
docker history --no-trunc --format '{{.Size}}\t{{.CreatedBy}}' myapp:latest
3. Where docker history hits its limits
As useful as docker history is for a first overview, it only shows the size per layer as a whole, not which individual files inside that layer take up how much space. A layer weighing 300 megabytes could consist of a single large binary or of thousands of small files, and docker history makes no distinction visible here. This exact lack of detail is precisely why specialized tools such as dive were created.
A second, subtler problem concerns deleted files: if a Dockerfile's RUN instruction downloads a large archive, extracts it, and then deletes it again, that entire sequence shows up as a single layer with the net size difference, provided download, extraction, and deletion happen inside the same RUN command. If those steps are instead spread across several separate RUN commands, the downloaded archive remains fully present in the first layer, even if a later layer appears to remove it again, because whiteout markers only hide the file in the final filesystem view, they do not delete it from the layer itself.
4. Installing dive
The tool dive, an open-source project by Alex Goodman, was built specifically to close that gap: it loads an image, virtually unpacks every layer, and provides an interactive, file-tree-like view where every single file is visible with its size and the layer it was added or modified in. Installation depends on the operating system, either through the respective package manager or directly via the binary from the GitHub release.
On Debian and Ubuntu based systems, dive can be installed via a .deb package, on macOS through Homebrew, and for other Linux distributions a statically linked binary is available that runs without further dependencies. Since dive itself talks to the local Docker engine to load images, it needs to run on the same machine where the Docker daemon is active, or at least have access to the same Docker socket.
# Installation on Debian/Ubuntu via the official .deb package
DIVE_VERSION=$(curl -sL "https://api.github.com/repos/wagoodman/dive/releases/latest" \
| grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
curl -OL https://github.com/wagoodman/dive/releases/download/v${DIVE_VERSION}/dive_${DIVE_VERSION}_linux_amd64.deb
sudo apt install ./dive_${DIVE_VERSION}_linux_amd64.deb
# Installation on macOS
brew install dive
# Check the version
dive --version
5. Using dive interactively
Running dive myapp:latest opens a terminal interface with two main areas: on the left, the list of all layers with their respective size, on the right, the file tree of the currently selected layer. The arrow keys switch between layers, while the file tree on the right updates accordingly and color-codes which files were added, modified, or removed in that layer, which usually makes the cause of a size increase visible within seconds.
Particularly valuable is the built-in efficiency score that dive automatically calculates and displays as a percentage in the header: it relates the sum of actually used, unique file size to the total size of all layers, immediately making visible how much disk space is effectively wasted through files overwritten or deleted multiple times. A value clearly below 90 percent is a strong signal that a closer look at the layer structure is worthwhile.
# Start interactive analysis of a local image
dive myapp:latest
# Key shortcuts inside dive:
# Tab switch between layer list and file tree
# Arrow keys navigate through layers or files
# Ctrl+A show only files changed in the current layer
# Ctrl+U show only unused/wasted files
# Ctrl+F search within the file tree
6. Automating dive in a CI pipeline
Beyond interactive use, dive supports a non-interactive CI mode, activated through the environment variable CI=true. In that mode, dive checks the image against configurable thresholds, such as a minimum efficiency or a maximum share of wasted space, and exits with an error code once those thresholds are not met, which plugs directly into a build pipeline as a quality gate.
Configuration happens through a .dive-ci file in YAML format, which lets you define both the minimum required efficiency value and the maximum allowed amount of wasted space in bytes. That turns image size into a measurable, automatically monitored metric, similar to test coverage or linter warnings, instead of a property that only occasionally gets noticed manually and usually too late, often only once registry costs or deployment times have already visibly increased.
# .dive-ci: thresholds for CI mode
rules:
lowestEfficiency: 0.9
highestWastedBytes: 50MB
highestUserWastedPercent: 0.1
7. Common causes of wasted disk space
By far the most common cause of bloated images is package manager caches that are not cleaned up after installation: an apt-get install without a following rm -rf /var/lib/apt/lists/* in the same RUN command leaves the entire package index in the layer, often several hundred megabytes that serve no purpose whatsoever after installation. The same pattern shows up with npm install without the --production flag, with uncleaned pip caches, and with temporary build artifacts that are never needed in the final image.
A second common mistake is an unfavorable order of Dockerfile instructions, in particular a COPY . . for the entire source tree before dependencies are installed. That does not directly make the image larger, but it destroys the layer cache on every, even the smallest, code change, forcing expensive installation steps to run again on every build, which unnecessarily stretches build times and, over the course of development, also produces more temporary layers overall.
8. A Dockerfile before and after
Combining multi-stage builds with careful cache cleanup inside the same RUN command solves most of the problems described above at once. In a multi-stage build, build tools and intermediate artifacts get installed in a separate build stage that never shows up in the final image, while the last stage only copies the runtime artifacts that are actually needed, without ever leaving a layer with a compiler or build dependencies in the final image.
A concrete example makes the difference clear: instead of putting package installation and cache cleanup in separate RUN commands, which would keep the cache alive in the first layer, both steps get chained together with && in the same RUN command, so only the net size difference ends up in the final layer. Combined with a multi-stage build, this pattern reduces image size in practice by 50 percent or more, without losing any functionality.
# Before: cache stays alive, no multi-stage build
FROM node:20
COPY . .
RUN apt-get update
RUN apt-get install -y build-essential
RUN npm install
RUN npm run build
# After: cache cleanup in the same RUN command, multi-stage build
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
9. The practical analysis workflow combined
In practice, both tools complement each other in a clear, repeatable sequence: docker history --no-trunc delivers the quick first overview and shows which build step is responsible for the single largest layer. If a noticeably large layer turns up, the deeper analysis with dive follows, to determine exactly which files within that layer take up the space and whether they are genuinely needed runtime files or avoidable cache and build clutter.
After optimizing the Dockerfile, another pass with both tools is worthwhile to verify the effect, ideally complemented with a permanent dive CI check that automatically prevents future regressions before a bloated image ever gets pushed to the registry. The table below compares both tools with their respective strengths, as a decision aid for the next image optimization pass.
| Tool | Level of detail | Interactive | Typical use |
|---|---|---|---|
| docker history | Per layer, whole number | No, plain text output | Quick first overview |
| dive (interactive) | Per file within every layer | Yes, terminal UI | Deep manual analysis |
| dive (CI mode) | Efficiency and waste metrics | No, exit code as a gate | Automated quality assurance |
| Multi-stage build | Structural prevention, not an analysis tool | No, part of the Dockerfile | Lasting fix instead of treating symptoms |
Mironsoft
Container infrastructure, CI pipelines and deployment automation
Docker setups that hold up across the team and in production?
We review existing Dockerfiles and Compose stacks for security gaps, bloated images and fragile build pipelines, then build a container infrastructure that builds fast, runs securely and stays understandable across the team.
Dockerfile Review
Systematically optimizing multi-stage builds, layer caching and image size.
Security Audit
Hardening container isolation, secrets handling and image scanning against real attack surfaces.
CI/CD Integration
Building build pipelines, registries and deployment strategies for reproducible releases.
10. Summary
Image Layer Analysis with dive: The Essentials at a Glance
First step
docker history --no-trunc quickly shows which layer takes up the most space.
Deep analysis
dive shows individual files per layer and computes an efficiency score.
Most common cause
Uncleaned package manager caches that should be removed in the same RUN command.
Lasting fix
Multi-stage builds combined with dive as an automated CI quality gate.