Weighing smaller images against lost layer caching
The --squash build flag merges all layers of an image into a single one, producing smaller, cleaner images, but at the cost of the layer caching between builds that speeds up many CI pipelines.
Table of Contents
- 1. Why Dockerfiles End Up With Many Layers
- 2. What --squash Technically Does
- 3. Enabling Squash With BuildKit and buildx
- 4. Benefit: Smaller Images by Removing Intermediate State
- 5. Benefit: No Layer History With Sensitive Intermediate State
- 6. Drawback: No More Layer Caching Between Builds
- 7. Multi-Stage Builds as the Better Alternative in Many Cases
- 8. When Squashing Is Actually the Right Choice
- 9. A Practical Decision Guide
- 10. Summary
- 11. FAQ
1. Why Dockerfiles End Up With Many Layers
Every instruction in a Dockerfile that changes the filesystem state (RUN, COPY, ADD) creates its own layer, which remains stored in the image permanently. Grown Dockerfiles with many individual RUN commands for package installation, configuration, and cleanup quickly add up to 20 or more layers, each of which leaves its own diff in the union filesystem.
This becomes a problem mainly when a large file is created in an early layer and deleted again in a later one, for example a downloaded archive after extraction. Deleting removes the file from the visible filesystem view, but the original layer containing the file remains part of the image and continues contributing to the total size, which is easy to overlook when only looking at the final image size.
2. What --squash Technically Does
The --squash flag instructs the Docker daemon to merge all newly created layers of a regular, layer-based build into a single layer after the build completes. Technically, all filesystem diffs of the individual build steps are computed into one final diff against the base image, so that exactly one new layer results on top of the base image layer.
Importantly, only the layers newly created during this build are merged; the base image layer itself remains a separate layer and is not folded in. Files created in an early build step and deleted again in a later one no longer appear at all in the final squashed layer, because only the end state of the filesystem is relevant.
3. Enabling Squash With BuildKit and buildx
Historically, --squash was an experimental feature of the classic Docker builder and had to be enabled via experimental: true in daemon.json. With the BuildKit builder active by default today, the situation is somewhat different: --squash is also available via docker buildx build, still partly experimental depending on the Docker version, so checking current documentation for your own Docker version is worthwhile.
In practice, a simple invocation is usually enough and integrates easily into existing build scripts or CI jobs without any changes to the rest of the Dockerfile. The build itself takes longer than a normal build, because in addition to actually building the layers, the squash step runs as post-processing.
# Build with squash via the classic builder
docker build --squash -t myapp:squashed .
# Build with squash via buildx (BuildKit)
docker buildx build --squash -t myapp:squashed --load .
# Compare layer count before and after squashing
docker history myapp:latest | wc -l
docker history myapp:squashed | wc -l
4. Benefit: Smaller Images by Removing Intermediate State
The most obvious benefit shows up in Dockerfiles that create and later delete large temporary files without doing so within a single RUN command followed by rm in the same layer. An example is a build step that compiles source code and leaves behind build artifacts of several hundred megabytes, which are removed in a later step because only the compiled binaries are needed in the final image.
In such cases, squashing can reduce image size by several hundred megabytes up to the gigabyte range, depending on how large the intermediately created and deleted files were. For cleanly written Dockerfiles that already avoid such intermediate state via multi-stage builds, the effect is significantly smaller, because there is barely anything left to merge.
5. Benefit: No Layer History With Sensitive Intermediate State
Via docker history or by extracting individual layers with docker save, every intermediate step of a non-squashed image can be traced, including files removed again in a later layer. If a credentials file or an API key is accidentally created in an early RUN command and only deleted in a later step, that value remains extractable from the original layer for anyone with access to the image.
A squashed image reduces this risk because only the final filesystem state is retained as a single layer and intermediate steps can no longer be reconstructed. This is not a substitute for correct secret handling via BuildKit secrets or multi-stage builds, but at best an additional safeguard against accidental mistakes in build logic, not a fundamental solution for handling secrets in the build.
6. Drawback: No More Layer Caching Between Builds
The central drawback of --squash is losing the incremental layer cache between consecutive builds. Normally, Docker detects from unchanged Dockerfile instructions and build contexts which layers can be reused from the local cache or from the registry, cutting builds with only small code changes from minutes down to seconds.
Since a squashed image, from Docker's perspective, has only a single layer on top of the base image, subsequent builds have nothing left to granularly build upon. Even the smallest code change effectively forces the entire build process to run from scratch, which noticeably lengthens build times, especially in CI pipelines with frequent commits.
7. Multi-Stage Builds as the Better Alternative in Many Cases
For most use cases where squashing is considered because of large intermediate artifacts, a multi-stage build is the cleaner solution. Build dependencies, compilers, and temporary artifacts end up in a separate build stage, from which in the end only the actually needed files are copied into a lean runtime stage, without intermediate steps ever landing in the final image.
The decisive difference from --squash is that multi-stage builds fully preserve layer caching within each individual stage. If only the application code changes but not the build dependencies, the cache for the installation steps still applies, while at the same time producing a final image just as lean as with squashing, only without its cache drawback.
# Dockerfile with a multi-stage build instead of squash
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
8. When Squashing Is Actually the Right Choice
Squashing remains sensible for final release images built only once and then distributed unchanged across many environments, for example finished base images for internal teams or distribution images where registry size and pull time matter more than fast repeat builds. Squashing is also a pragmatic interim solution for images built from legacy Dockerfiles that cannot easily be rewritten to multi-stage.
Squashing does not make sense in active development or CI pipelines with frequent rebuilds, where losing the layer cache noticeably increases build times and undermines the whole point of fast feedback cycles. Here, the drawback of the missing cache almost always outweighs the benefit of a smaller image size.
9. A Practical Decision Guide
The rule of thumb: first check whether a multi-stage build solves the actual problem of intermediate state ending up in the final image. In the vast majority of cases that is possible and clearly the better choice, because caching and small images are not mutually exclusive. Squashing then remains a targeted tool for the remaining edge cases, such as one-off release builds or legacy Dockerfiles that cannot be restructured on short notice.
Anyone who wants to combine both approaches can also run a multi-stage build additionally with --squash, but still loses caching for the final build run. In practice this combination rarely pays off, because a clean multi-stage build usually already achieves the desired image size without squashing.
| Approach | Layer caching | Image size | Typical use |
|---|---|---|---|
| Normal build | Fully preserved | Larger with much intermediate state | Active development, frequent rebuilds |
| --squash | Lost for subsequent builds | Much smaller with large intermediate state | Final release images, legacy Dockerfiles |
| Multi-stage build | Fully preserved per stage | Small via selective copying | Recommended default approach |
| Multi-stage + --squash | Lost for final run | Minimal | Rare special cases |
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
Layer Squash: The Essentials at a Glance
Mechanism
--squash merges all newly created build layers into a single layer.
Benefit
Smaller images, no reconstructable history of deleted intermediate state.
Drawback
Layer caching between builds is lost for the squashed layer.
Recommendation
Prefer multi-stage builds generally, squash selectively for release images.