Intro
Docker's build cache is one of the most powerful features for speeding up image builds, but it is also one of the most misunderstood. When it works, developers save minutes on every build. When it silently misses, every build starts from scratch and CI pipelines become painfully slow. This guide is for developers, DevOps consultants, and technical startup teams who want to stop guessing and start verifying exactly how the build cache behaves in their own environment.
We will focus on operational safety: observe before changing, limit the blast radius, use placeholders instead of secrets, verify every result, and document how to recover when an expected state is not reached. You will learn the basic commands for inspecting and manipulating the build cache, see practical examples with expected output, and get a cheat sheet of the most useful checks. We will also cover related concepts such as BuildKit, Dockerfile structure, and layer caching, but only where they directly affect cache behavior, troubleshooting, or recovery.
By the end of this article, you will be able to run a small production-like local test, interpret its results, and apply the same commands confidently in a team environment. All examples use plain Docker CLI commands that work on Linux, macOS, and Windows with Docker Desktop.
Version and Environment Inventory
Before running any cache-related commands, confirm the version of Docker and BuildKit you are using. Cache behavior has changed significantly between legacy builder and BuildKit, and some flags or outputs differ across versions. Run the following read-only commands to gather the current state:
docker version --format '{{.Server.Version}}'
docker buildx version
Example output on a typical Docker Desktop installation:
24.0.7
github.com/docker/buildx v0.12.1
Also check which builder is active. BuildKit was enabled by default in Docker 23.0 and later, but it is worth confirming:
docker buildx ls
Expected output includes a line with default * indicating the selected builder. If you still see legacy or no asterisk next to the default builder, consider running docker buildx install to set BuildKit as the standard builder.
For Compose projects, use docker compose version and docker compose config --images to list the images referenced by your services. This gives you a quick inventory before you touch any build commands.
Data Storage and Cache Location
The build cache is stored inside the Docker daemon's data directory, typically under /var/lib/docker on Linux and inside the Docker Desktop VM on macOS/Windows. If you are using the legacy builder, cache is managed inside the Docker overlay filesystem; with BuildKit, cache is stored separately and can even be exported to a remote registry. You can inspect disk usage with:
docker system df
Example output:
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 12 2 3.45GB 1.87GB (54%)
Containers 3 1 45MB 0B (0%)
Local Volumes 5 2 2.1GB 900MB (42%)
Build Cache 48 0 5.2GB 5.2GB (100%)
Notice the Build Cache line. If the reclaimable size is large and you never need those intermediate layers again, you can clear it with docker builder prune, but hold off until you understand what you might lose. We will cover pruning safely later.
For data inside containers, confirm where files are stored before changing containers. A named volume such as app_data:/var/lib/app is managed by Docker and survives container re-creation. A bind mount such as ./data:/var/lib/app maps directly to a host directory and is easier to back up but can cause permission or portability issues across machines. A restart test is the quickest way to verify persistence: stop the container, recreate it, and check whether the expected files are still present. If they disappear, the service was likely writing to the container's ephemeral filesystem instead of the mounted volume.
Safe Configuration Path
When you change build cache settings, do it in the smallest safe step. The most common configuration change is switching between legacy builder and BuildKit. Since Docker 23, BuildKit is default, but many teams still have DOCKER_BUILDKIT=0 in their environment for legacy reasons. Set the environment variable per command rather than globally:
# One-time BuildKit build
DOCKER_BUILDKIT=1 docker build -t myapp:test .
# Back to legacy for this command only
DOCKER_BUILDKIT=0 docker build -t myapp:test-legacy .
To make BuildKit the default for all future shell sessions, add export DOCKER_BUILDKIT=1 to your shell profile, but be aware that some older Dockerfiles with unusual syntax or certain plugins may behave differently. Verify which builder is active after the change:
docker buildx version && docker buildx ls
If you use the legacy builder, the cache is stored inside the Docker daemon and cannot be easily exported. With BuildKit, you can specify a cache storage location using the --cache-from and --cache-to flags, which is especially useful in CI. Example for pulling and pushing cache to a registry:
docker buildx build --cache-from type=registry,ref=myregistry/myapp:cache --cache-to type=registry,ref=myregistry/myapp:cache,mode=max -t myapp:latest .
This tells BuildKit to look for cache layers in the registry image myapp:cache before building, and to push new cache layers after a successful build. The mode=max includes all intermediate layers, not just the final image. For local development, a simpler approach is to rely on the local cache and only avoid --no-cache unless you genuinely need a fully reproducible build.
Configuration File Options
If you prefer a declarative approach, you can set builder options in /etc/docker/daemon.json (Linux) or Docker Desktop settings. For example, to enable BuildKit and set a max cache size, add:
{
"features": {
"buildkit": true
},
"builder": {
"gc": {
"enabled": true,
"defaultKeepStorage": "10GB"
}
}
}
After changing daemon.json, restart the Docker daemon (sudo systemctl restart docker on Linux) and confirm with docker info | grep -i buildkit. For Docker Desktop, you can change these settings in the GUI under Preferences > Docker Engine, then click Apply & Restart.
Always document the exact change and how to revert it. A good practice is to keep configuration files in version control and use a pull request for any change that affects shared build infrastructure.
Verification and Diagnostics
Now that you know your environment, let us verify how the cache is actually working. The most direct way is to run a build twice and observe the output. Create a simple Dockerfile:
FROM alpine:3.19
RUN apk add --no-cache curl
COPY app.sh /app.sh
RUN chmod +x /app.sh
CMD ["/app.sh"]
Run the build twice and compare timing and cache hits:
time docker build -t myapp:test .
time docker build -t myapp:test .
On the second build, with a warm cache, you should see lines like:
=> CACHED [1/4] FROM docker.io/library/alpine:3.19
=> CACHED [2/4] RUN apk add --no-cache curl
=> CACHED [3/4] COPY app.sh /app.sh
=> CACHED [4/4] RUN chmod +x /app.sh
=> exporting to image
=> => exporting layers
=> => writing image sha256:...
The CACHED marker confirms the layer was reused. If you do not see CACHED, the cache missed for that layer, which often means the layer's inputs changed or there is a problem with the cache mechanism.
Detailed Cache Inspection with BuildKit
BuildKit provides a more detailed view of cache usage. Use docker buildx build --progress=plain to see the full log, including cache decisions. For an even deeper look, you can use docker buildx du to see disk usage by cache type:
docker buildx du
Output may resemble:
Total: 2.08GB
Reclaimable: 1.05GB
Shared: 300MB
Private: 1.78GB
To see which Dockerfile lines are causing cache misses, rebuild with a small change and watch the log. For example, if you modify app.sh, only the last three layers rebuild; the RUN apk add layer stays cached. If the RUN apk layer rebuilds even when you did not change that line, then something earlier in the Dockerfile or build context changed, such as file timestamps in the build context.
To see the exact cache key for each instruction, run with verbose output:
docker buildx build --progress=plain --no-cache=false -t myapp:test . 2>&1 | grep -A3 'RUN apk'
The --no-cache=false is the default but explicit. Look for the CacheKey lines in the output. They show the hash of the inputs used to look up cache. If the hash changes, the cache misses.
Using docker image history
For the legacy builder, you can inspect the layers of an existing image to see if a particular layer is likely to be reused. Run:
docker image history myapp:test
Example output:
IMAGE CREATED CREATED BY SIZE COMMENT
0123456789ab 2 minutes ago CMD ["/app.sh"] 0B buildkit.dockerfile.v0
<missing> 2 minutes ago RUN /bin/sh -c chmod +x /app.sh # buildkit 33B buildkit.dockerfile.v0
<missing> 2 minutes ago COPY app.sh /app.sh # buildkit 45B buildkit.dockerfile.v0
<missing> 2 minutes ago RUN /bin/sh -c apk add --no-cache curl # bui… 5.5MB buildkit.dockerfile.v0
<missing> 2 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ADD file:... in / 7.05MB
Layers with <missing> in the IMAGE column are intermediate layers that were not tagged but still exist in the cache. This history helps you understand the order and size of each layer.
Checking Cache in Remote Environments
If you are using a remote builder or CI service, you may need to use docker buildx ls to list builders and docker buildx use <builder> to switch context. To check whether a remote registry contains a cache image, use:
docker manifest inspect myregistry/myapp:cache >/dev/null && echo "Cache image exists" || echo "No cache image"
If the cache image does not exist, the first build will populate it via --cache-to. Subsequent builds should use --cache-from to retrieve it. Always verify that the credentials for the registry are available in the build environment, typically via docker login or CI secrets.
Failure Modes and Recovery
Cache misses are not always benign. They can cause builds to fail, produce unexpected results, or silently consume large amounts of disk space. Here are the most common failure modes and how to recover from them.
Cache Miss Due to Build Context Changes
The entire build context is hashed to determine if a COPY or ADD instruction has changed. If you do COPY . /app, then any change to any file in the current directory (including .git, temporary files, or a large node_modules) will invalidate the cache for that instruction and everything after it. This is the number one reason cache seems to "never work."
Recovery:
- Use a
.dockerignorefile to exclude unnecessary files. Example:
.git
node_modules
*.log
tmp/
- Be specific in
COPYcommands. Instead ofCOPY . /app, copy only the files you need:COPY package.json /app/package.json, thenRUN npm install, thenCOPY src /app/src. This way, changes tosrcdo not invalidate the npm install layer.
Cache Miss Due to Timestamp or Metadata Changes
Even if file contents are identical, changed timestamps or permissions can cause a cache miss because the layer cache key includes metadata. This often happens when files are checked out from version control with current timestamps or when a file is copied from a different filesystem.
Recovery:
- Use
--no-cacheonly for the problematic instruction? Not directly; instead, sort out the source of metadata changes. For Git checkouts, usegit checkout-index -a -fwhich sets timestamps, or use a build system that normalizes file metadata before build. - In BuildKit, you can use
RUN --mount=type=cache,target=/root/.cacheto mount a persistent cache volume for commands likeapt-get,pip, ornpm. This cache is independent of file changes in the build context, so it persists even when other layers rebuild.
Cache Corruption or Inconsistent State
Occasionally, the cache itself can become corrupted or contain stale layers from previous builds of different branches or configurations. This can lead to non-deterministic builds or errors like failed to compute cache key or unknown blob.
Recovery:
- Run
docker builder pruneto remove all build cache. Be careful: this removes all cache for all projects, not just the current one. A safer approach is to prune only unused cache withdocker builder prune --filter until=24hto remove cache older than 24 hours, or usedocker buildx prune --filter=label=project=myappif you label your builds. - If corruption persists, restart the Docker daemon. On Linux,
sudo systemctl restart docker. On Docker Desktop, use the Restart option in the menu. After restart, run a fresh build with--no-cacheonce to rebuild from scratch, then subsequent builds should cache normally.
Running Out of Disk Space
Build cache can consume tens of gigabytes, especially with multiple projects and frequent rebuilds. When the disk fills up, Docker may fail to create new layers, containers may not start, and builds will error with no space left on device.
Recovery:
- Check disk usage with
df -hfor the Docker data directory. - Run
docker system dfto see how much space is reclaimable. - Use
docker builder prunewith a size filter:docker builder prune --keep-storage=10GBto keep at most 10GB of cache. - For a more aggressive cleanup,
docker system prune -aremoves all unused images, containers, and cache, but will force every image to be rebuilt or re-pulled. Schedule regular pruning or use a cron job to avoid surprises.
Dedicated Cache Volumes for Dependency Managers
A frequent pain point is the cache for package managers like npm, pip, or Maven. By default, those caches are inside the layer that runs npm install or pip install, so any change to package.json or requirements.txt invalidates the entire cache. Using a dedicated cache mount preserves the package cache across builds even when dependencies change:
# syntax=docker/dockerfile:1.4
FROM python:3.12
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
This mount keeps the pip cache in a separate location managed by BuildKit. When you change requirements.txt, only the pip install step runs, but it can reuse the downloaded wheels from the cache, drastically reducing install time. Similar mounts exist for apt, yum, go, maven, and others. Always check the syntax version at the top of your Dockerfile; the RUN --mount feature requires BuildKit and a recent Dockerfile frontend.
Operations Checklist
Use this checklist before and after each build to ensure cache behavior is understood and consistent.
Pre-build checks:
- [ ] Confirm Docker version and active builder:
docker versionanddocker buildx ls. Owner: whoever runs the build (e.g., developer or CI admin). Revisit when upgrading Docker. - [ ] Ensure
.dockerignoreis present and excludes unnecessary files. Owner: developer responsible for the Dockerfile. Review whenever the build context changes. - [ ] Check disk space:
df -hon the Docker data directory. Owner: person managing the build machine or CI runner. Check before each large build or weekly. - [ ] Verify that any registry credentials needed for
--cache-fromor--cache-toare available. Owner: CI infrastructure admin. Verify before first use in CI.
During build:
- [ ] Run build with progress plain to capture full logs:
docker buildx build --progress=plain -t myapp:test . - [ ] Note which layers show
CACHEDand which showRUN. Owner: the builder operator. If unexpected misses occur, diagnose immediately. - [ ] If using remote cache, ensure cache image is pulled (
--cache-from) and pushed (--cache-to). Owner: CI pipeline designer. Confirm after every build.
Post-build checks:
- [ ] Check image size and layer count:
docker image ls myapp:testanddocker image history myapp:test. Owner: developer. Review after any significant Dockerfile change. - [ ] Test the image runs correctly:
docker run --rm myapp:test. Owner: QA or developer. Do this before merging the Dockerfile change. - [ ] Document any cache-related issues or workarounds in the project README or team wiki. Owner: team lead or designated maintainer. Update monthly or after each incident.
Periodic maintenance:
- [ ] Run
docker system dfanddocker buildx duto monitor cache size. Owner: DevOps engineer or whoever manages CI resources. Do this weekly. - [ ] Prune unnecessary cache with
docker builder prune --filter until=168h(one week). Owner: same as above. Do this at the same weekly interval. - [ ] Review builder configuration in
daemon.jsonor Docker Desktop settings. Owner: infrastructure maintainer. Do this quarterly or after Docker version upgrades.
Every item should have a named owner, not a group, to ensure accountability. For example, in a small team, the developer who owns the Dockerfile is also the one who reviews cache behavior. In larger organizations, a platform engineer might own the builder infrastructure. The frequency of review depends on how often the project changes; a rapidly changing project might need weekly cache checks, while a stable one can use monthly reviews.
Common Pitfalls and How to Avoid Them
Here are recurring mistakes developers make with Docker build cache and concrete ways to avoid them.
Pitfall 1: Using docker build --no-cache as a Shortcut
Why it happens: When a build fails, developers often add --no-cache to avoid cache-related issues. This kills all cache and makes every later build slow.
How to avoid: Only use --no-cache when you truly need a clean build (e.g., for reproducible builds or after a toolchain upgrade). Otherwise, try to fix the actual cause of the cache miss. If you must bypass cache for a single instruction, consider using RUN --mount=type=cache for that instruction or invalidating only the necessary layer with a comment change.
Pitfall 2: Copying the Entire Build Context
Why it happens: COPY . /app is easy, but it includes everything in the directory, often huge files and directories that change frequently.
How to avoid: Use a .dockerignore file and be specific in COPY commands. Split COPY operations to avoid invalidating the entire layer chain.
Pitfall 3: Not Using BuildKit's Cache Mounts
Why it happens: Many developers are unaware of cache mounts or think they are complex. Without them, package manager caches are lost with every dependency change, causing slow builds.
How to avoid: Use the RUN --mount=type=cache syntax. It is supported in modern Docker versions and significantly speeds up dependency installation. Refer to Docker documentation for examples for npm, pip, apt, etc.
Pitfall 4: Misunderstanding Layer Caching with ARG and ENV
Why it happens: Changing a build argument (ARG) or environment variable (ENV) that is used in an early layer invalidates all subsequent layers. Developers may not realize that these variables are part of the cache key.
How to avoid: Place ARG and ENV as late in the Dockerfile as possible, ideally just before the instruction that uses them. If an argument is used only for metadata, consider using LABEL instead.
Pitfall 5: Ignoring Cache in CI Pipelines
Why it happens: Many CI systems start with a clean environment, so the Docker cache is empty every build. Without remote cache, builds are as slow as a no-cache build.
How to avoid: Use --cache-from and --cache-to with a registry. In CI, include these flags in the build step. For GitHub Actions, use a dedicated action like docker/build-push-action that supports cache backends. For GitLab CI, use cache with key and paths to persist the Docker layer cache, or use --cache-from with the previous image.
Pitfall 6: Overlooking Disk Space
Why it happens: Build cache accumulates silently, and developers rarely check disk usage until the build fails.
How to avoid: Implement periodic pruning as part of routine maintenance. Set alerts on disk usage for build machines. Use docker builder prune --keep-storage to cap cache size.
Conclusion
Docker build cache commands are only useful when each recommendation is version-scoped, observable, and reversible where the technology permits. Copying a command without checking prerequisites and expected output is not an operations procedure; it is a gamble. Throughout this guide, we have emphasized the importance of seeing the current state, changing one thing at a time, and verifying the result with concrete commands.
As a next step, choose one low-risk verification for your own project: run a build twice with progress plain, note which layers are cached, and then deliberately change a file and observe which layers rebuild. Record the before and after state, and share the results with your team. This small experiment will build intuition far better than reading more abstract documentation.
A reliable technical workflow makes failure visible, protects sensitive values, limits changes to the intended resource, and defines recovery verification before an incident forces the decision. Apply that mindset to Docker build cache, and you will spend less time waiting for builds and more time shipping features.