Docker Layer Squash: When --squash Pays Off and When It Does Not
AI generated
FROM
RUN
Docker · Images · Build
Docker Layer Squash in Detail
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.

16 min read Squash Image Size Build Caching

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.

11. FAQ: Layer Squash: The Essentials at a Glance

1What does the --squash flag do in docker build?
The flag merges all layers newly created during the build into a single layer on top of the base image, so that in the end only the final filesystem state is stored as one diff.
2Does squashing always reduce image size?
Not necessarily. The effect is large when large files were created and deleted again in between. For already cleanly written Dockerfiles without such intermediate state, the reduction is small.
3Why is layer caching lost with squash?
Because the squashed image, from Docker's perspective, has only a single layer on top of the base image, no subsequent build can granularly build on cached individual intermediate steps anymore.
4Is squash a substitute for secure secret handling?
No. Squash only prevents deleted intermediate state from remaining extractable via docker history, but does not replace BuildKit secrets or multi-stage builds as the correct way to keep secrets out of the build.
5Is a multi-stage build always the better choice over squash?
In most cases yes, because multi-stage builds produce small images while fully preserving layer caching per stage. Squash remains relevant mainly for legacy Dockerfiles or one-off release builds.
6Does --squash need to be enabled in daemon.json?
With the classic builder that was sometimes needed via experimental: true. With BuildKit and docker buildx build --squash, exact availability depends on the Docker version, so checking current documentation is advisable.
7Does squash lengthen build time?
Yes, because after actually building the layers, the squash step runs as post-processing, computing all layer diffs into one final diff.
8Does the base image layer survive squashing?
Yes, only the layers newly created during the current build are merged, the base image's own layer remains a separate layer.
9Which images benefit most from squash?
Final release images built only once and then distributed unchanged, as well as legacy Dockerfiles with large intermediate state that cannot be converted to multi-stage on short notice.
10Can I combine multi-stage build and squash?
Technically yes, but caching for the final build run is still lost. In practice this rarely pays off, because a clean multi-stage build usually already achieves the desired size without squash.