Docker Build Cache Invalidation: Why Builds Are Slow and How to Speed Them Up

Was this helpful?

Every team has that moment: you change one line of application code and your “quick rebuild” turns into a 12-minute pilgrimage through dependency installs, OS package updates, and a suspicious amount of “Sending build context.”

It’s rarely Docker being “slow.” It’s your cache being invalidated—sometimes correctly, sometimes by accident, sometimes because your CI system treats caches like embarrassing secrets. Let’s make builds predictably fast without cargo-culting random flags.

Table of contents

The mental model: what the cache actually is

Docker build caching looks like magic until you treat it like a filesystem and a compiler at the same time. The cache is not “a general memory of your build.” It’s a set of immutable build results addressed by inputs. Change inputs, you get a different output. No drama. No exceptions. Only tears.

Layers are content-addressed results of instructions

Traditional Docker builds execute your Dockerfile line by line. Each instruction produces a filesystem snapshot (“layer”) and metadata. The cache key for that instruction is effectively a digest of:

  • The instruction itself (including exact strings, ARG values in scope, environment variables in scope).
  • The parent layer digest (what came before).
  • For COPY/ADD, the content and metadata of files copied into the build.

If any of those change, that instruction is a cache miss, and every instruction after it will also miss because their parent layer changed. That’s the “invalidates the rest of the build” effect.

Build context is part of the input surface area

When you run docker build ., Docker sends a build context to the builder. If your context is huge, the build can be slow even before it starts building. Also, when you COPY . ., you’re telling Docker: “hash basically everything.” One changed timestamp or generated file can poison your cache.

BuildKit changes the game, but not the physics

BuildKit is the newer build engine. It adds parallelization, better caching, cache exports/imports, and features like cache mounts and secrets. It does not magically make bad Dockerfiles good. It just makes the consequences arrive faster.

Paraphrased idea (attribution): Gene Kim often argues that reliability comes from feedback loops and fast recovery, not heroics. Docker caching is a feedback loop problem too.

Interesting facts and historical context (you can use at parties)

  1. Docker’s early build engine (the “classic builder”) was single-threaded and fairly literal; BuildKit later introduced a DAG-based build graph that can parallelize independent steps.
  2. Layer caching predates Docker; union filesystems and copy-on-write snapshots were used in various forms long before containers became fashionable.
  3. BuildKit’s RUN --mount=type=cache changed how we treat package manager caches: you can keep them without baking them into the final image.
  4. Historically, many CI systems ran Docker builds in privileged mode with local disk caches; modern ephemeral runners made “cache reuse” a deliberate design choice rather than an accident.
  5. The .dockerignore file exists because people kept sending gigabytes of junk (like node_modules) to the daemon and then blamed Docker.
  6. Multi-stage builds popularized a clean separation between “build-time dependencies” and “runtime image,” which also makes cache strategy more intentional.
  7. OCI image specs made images more portable, but portability doesn’t mean your caches follow you—cache locality is still a practical constraint.
  8. Docker’s cache keys are sensitive to instruction ordering; a tiny re-ordering can take you from “seconds” to “minutes” without changing what the image does.
  9. Remote cache export/import (e.g., via Buildx) is effectively “build artifact caching,” similar in spirit to Bazel or compiler caches, but with container layers as artifacts.

Why you get cache misses: the real mechanics

1) You changed something earlier than you thought

The most common slow-build surprise is changing a file that’s used in an early COPY. If you COPY . . before installing dependencies, any code change forces a reinstall. That’s not Docker being mean. That’s your Dockerfile being naive.

2) Your build context is unstable

Generated files. Git metadata. Local build artifacts. Editor swap files. CI checkout differences. These can all change the content hash of your COPY inputs. If those files are in your context and not ignored, they are part of the cache key.

3) You’re using “always-changing” instructions

RUN apt-get update is a classic footgun in caching terms. Even if Docker tries to reuse cache, you probably don’t want it to reuse a layer that was created with a package index from three weeks ago. You’ve got competing goals: speed versus freshness. Pick deliberately.

4) ARG/ENV changes invalidate more than you think

ARG values in scope contribute to cache keys. So do ENV settings for subsequent instructions. If you set ENV BUILD_DATE=... early, congratulations: you invalidated your cache on every build, as designed.

5) Different builders, different caches

Local developer machine caches are not your CI caches. Even in CI, different runners do not share caches unless you explicitly export/import them. People assume “the cache is in the registry.” No. The image is. The cache might not be.

6) You’re building for multiple platforms

Multi-arch builds (linux/amd64 and linux/arm64) produce different layers. Cache reuse across architectures is limited, and some steps (like compiling native dependencies) are inherently platform-specific.

Joke #1: Docker caching is like memory—amazing when it works, and somehow it forgets exactly the thing you needed five seconds ago.

Fast diagnosis playbook (first/second/third checks)

This is the “stop guessing” flow. Run it when builds slow down and Slack starts smelling like panic.

First: identify where the time is going (context vs build steps)

  • Check build context upload time: if “Sending build context” is slow, you have a context problem, not a dependency problem.
  • Turn on plain progress: read which step is slow, and whether it was cached.

Second: confirm cache is even being used

  • Look for “CACHED” (BuildKit) or “Using cache” (classic builder).
  • Confirm builder and BuildKit settings: you might be building with different engines locally vs CI.
  • Confirm cache import/export in CI: ephemeral runners start empty unless you feed them cache.

Third: find the first cache miss and fix the layer order

  • Find the earliest step that misses; everything after it is collateral damage.
  • Look for early COPY . . or changing ARGs.
  • Fix with a stable dependency layer: copy only lockfiles first, install dependencies, then copy the rest.

Fourth: if still slow, look for storage and network bottlenecks

  • Registry pulls/pushes throttled? DNS flaky? Corporate proxy rewriting TLS?
  • Builder disk full or on slow storage? Overlay2 on a tiny VM disk is an expensive hobby.

Practical tasks: commands, output, and decisions (12+)

Each task below has: a command, what typical output means, and what decision you make next. Run these on a developer machine or CI runner (where possible). Commands are intentionally boring. Boring is good.

Task 1: Verify BuildKit is enabled

cr0x@server:~$ docker version --format '{{.Server.Version}}'
27.3.1
cr0x@server:~$ echo $DOCKER_BUILDKIT
1

What it means: Modern Docker versions support BuildKit; DOCKER_BUILDKIT=1 means the CLI will use it for builds.

Decision: If BuildKit isn’t enabled, enable it (locally and in CI) before doing anything else. Otherwise, you’re optimizing the wrong engine.

Task 2: Run a build with plain progress to see cache hits

cr0x@server:~$ docker build --progress=plain -t demo:cache-test .
#1 [internal] load build definition from Dockerfile
#1 DONE 0.0s
#2 [internal] load .dockerignore
#2 DONE 0.0s
#3 [internal] load metadata for docker.io/library/alpine:3.20
#3 DONE 0.6s
#4 [1/6] FROM docker.io/library/alpine:3.20@sha256:...
#4 CACHED
#5 [2/6] RUN apk add --no-cache bash
#5 CACHED
#6 [3/6] COPY . /app
#6 DONE 0.3s
#7 [4/6] RUN make -C /app build
#7 DONE 24.8s

What it means: Steps marked CACHED reused cache; steps without it executed. Here, COPY was not cached and the build step took 24.8s.

Decision: Fix the earliest non-cached step that shouldn’t change often (usually dependency install or toolchain download).

Task 3: Measure build context size (the silent killer)

cr0x@server:~$ docker build --no-cache --progress=plain -t demo:nocache .
#1 [internal] load build definition from Dockerfile
#1 DONE 0.0s
#2 [internal] load .dockerignore
#2 DONE 0.0s
#3 [internal] load build context
#3 transferring context: 1.42GB 12.1s done
#3 DONE 12.2s

What it means: You’re shipping 1.42GB to the builder every time. That’s not a build; it’s a moving company.

Decision: Add/repair .dockerignore. If you can’t get the context under control, no caching trick will save you.

Task 4: Confirm what is in your build context (quick and dirty)

cr0x@server:~$ tar -czf - . | wc -c
1523489123

What it means: This approximates the compressed context size. If it’s huge, you likely included node_modules, build outputs, or vendor directories.

Decision: Tighten .dockerignore and avoid COPY . . until you’ve staged stable layers.

Task 5: Check Docker disk usage and whether the cache is being evicted

cr0x@server:~$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          48        11        19.3GB    12.7GB (65%)
Containers      7         1         412MB     388MB (94%)
Local Volumes   23        8         6.1GB     2.4GB (39%)
Build Cache     214       0         18.9GB    18.9GB (100%)

What it means: Lots of build cache exists but none is active; it may be stale, or your builds are not referencing it due to changes in inputs or builder.

Decision: If the disk is constrained, set a cache policy rather than periodic scorched-earth docker system prune -a. If cache is never active, fix Dockerfile order or CI caching.

Task 6: Inspect builder instances (buildx) and confirm which one you use

cr0x@server:~$ docker buildx ls
NAME/NODE       DRIVER/ENDPOINT             STATUS    BUILDKIT   PLATFORMS
default         docker
  default       default                     running   v0.16.0    linux/amd64,linux/arm64
ci-builder*     docker-container
  ci-builder0   unix:///var/run/docker.sock running   v0.16.0    linux/amd64

What it means: You have multiple builders. Each builder can have its own cache. If CI uses ci-builder but your dev machine uses default, cache behavior differs.

Decision: Standardize on one builder for CI and document it. If you need remote cache, prefer a container driver builder with explicit cache export/import.

Task 7: Identify the first cache miss precisely (rebuild twice)

cr0x@server:~$ docker build --progress=plain -t demo:twice .
#6 [3/8] COPY package.json package-lock.json /app/
#6 CACHED
#7 [4/8] RUN npm ci
#7 CACHED
#8 [5/8] COPY . /app
#8 DONE 0.4s
#9 [6/8] RUN npm test
#9 DONE 18.2s

What it means: Dependency steps were cached, but test step ran. That’s normal if tests depend on code.

Decision: If npm ci is cached, you’ve already won most of the battle. If it isn’t, re-order copies and isolate lockfiles.

Task 8: Confirm which files invalidate your dependency layer

cr0x@server:~$ git status --porcelain
 M src/app.js
?? dist/bundle.js
?? .DS_Store

What it means: Generated outputs (dist/) and OS junk (.DS_Store) are in the working tree.

Decision: Ignore generated artifacts in .dockerignore (and probably .gitignore) so they don’t trigger cache misses when you do broad copies.

Task 9: Test that .dockerignore is actually applied

cr0x@server:~$ printf "dist\nnode_modules\n.git\n.DS_Store\n" > .dockerignore
cr0x@server:~$ docker build --progress=plain -t demo:ignore-test .
#3 [internal] load build context
#3 transferring context: 24.7MB 0.3s done
#3 DONE 0.3s

What it means: Context transfer dropped from “pain” to “fine.”

Decision: Keep .dockerignore reviewed like production code. It is production code.

Task 10: See image layer history to spot cache-busting patterns

cr0x@server:~$ docker history --no-trunc demo:cache-test | head -n 8
IMAGE          CREATED        CREATED BY                                      SIZE
sha256:...     2 minutes ago  RUN /bin/sh -c make -C /app build               312MB
sha256:...     2 minutes ago  COPY . /app                                     18.4MB
sha256:...     10 minutes ago RUN /bin/sh -c apk add --no-cache bash          8.2MB
sha256:...     10 minutes ago FROM alpine:3.20                                7.8MB

What it means: A huge RUN make layer suggests you’re producing build artifacts inside the image. That’s fine for build stages, questionable for runtime stages.

Decision: Use multi-stage builds so the big compile layers stay in a builder stage and don’t pollute runtime images (and pushes).

Task 11: Validate cache export/import in CI (buildx)

cr0x@server:~$ docker buildx build --progress=plain \
  --cache-from=type=local,src=/tmp/buildkit-cache \
  --cache-to=type=local,dest=/tmp/buildkit-cache,mode=max \
  -t demo:cache-export --load .
#10 [4/8] RUN npm ci
#10 CACHED
#11 exporting cache
#11 DONE 0.8s

What it means: Your build reused a local cache directory and exported updates back into it.

Decision: In CI, persist that directory between runs using your CI cache mechanism. If you can’t persist disk, export to a registry cache instead.

Task 12: Detect if your build is pulling base images every time

cr0x@server:~$ docker images alpine --digests
REPOSITORY   TAG    DIGEST                                                                    IMAGE ID       CREATED       SIZE
alpine       3.20   sha256:4bcff6...                                                          11f7b3...      3 weeks ago   7.8MB

What it means: The base image exists locally with a specific digest.

Decision: If CI is re-pulling base layers constantly, consider a runner image pre-warmed with common bases, or rely on registry-side layer caching closer to the runner.

Task 13: Confirm whether --no-cache is being used accidentally

cr0x@server:~$ grep -R --line-number "docker build" .github/workflows 2>/dev/null | head
.github/workflows/ci.yml:42:      run: docker build --no-cache -t org/app:${GITHUB_SHA} .

What it means: Someone forced cold builds in CI. Sometimes it’s for “freshness.” Often it’s superstition.

Decision: Remove --no-cache unless you have a specific security/compliance reason and you’ve accepted the cost.

Task 14: Check for cache-busting build arguments

cr0x@server:~$ grep -nE 'ARG|BUILD_DATE|GIT_SHA|CACHE_BUST' Dockerfile
5:ARG BUILD_DATE
6:ARG GIT_SHA
7:ENV BUILD_DATE=${BUILD_DATE}

What it means: If BUILD_DATE changes every build and is used early, you invalidate cache for everything after.

Decision: Move volatile metadata to the end, or into labels in the final stage only.

Dockerfile design that keeps caches alive

Cache is not something you “turn on.” It’s something you earn by making inputs stable. The fastest Dockerfile is usually the one that admits what changes often and what doesn’t.

Rule 1: Separate dependency definition from application source

If dependencies are defined by lockfiles, copy those first and install dependencies before copying the entire repo. That way, code changes don’t force dependency reinstalls.

Bad pattern: copy everything, then install. It guarantees cache misses on dependency installation.

Good pattern: copy only the dependency manifests, install, then copy the rest.

Rule 2: Keep volatile args out of early layers

Yes, you want the Git SHA in the image. No, you don’t want it to destroy caching for your entire dependency chain. Put labels late, ideally in the final stage only.

Rule 3: Stop baking caches into images; mount them instead

BuildKit cache mounts let you reuse package manager caches across builds without bloating final layers. This is where BuildKit is genuinely transformative.

Rule 4: Use multi-stage builds as a caching tool, not just a slimming tool

Multi-stage builds let you pin heavy, slow operations (compilers, dependency builds) into a stage that rarely changes, while keeping runtime stage minimal. It also keeps pushes smaller and faster, which matters more than people admit.

Rule 5: Pin base images deliberately

If you use floating tags like ubuntu:latest, you will eventually get cache churn, surprise upgrades, and “works on my machine” archaeology. Pin to a digest for reproducibility when it matters; pin to a stable minor tag when you want controlled updates.

Rule 6: Use .dockerignore like you mean it

Ignore .git, build outputs, local caches, and dependency directories that should be installed in-image. Your build context should look like source code, not like your laptop’s life story.

BuildKit: the modern cache engine and how to use it

BuildKit is where Docker builds stopped being purely sequential “run instructions” and became something closer to a build system. But you need to use its features intentionally.

BuildKit’s best weapons for caching

  • Cache mounts: reuse package manager caches without committing them into layers.
  • Secret mounts: fetch private dependencies without leaking tokens into image layers (also helps caching by avoiding “token changed” hacks).
  • Cache export/import: make CI builds fast even on fresh runners.
  • Better progress output: diagnosing cache behavior is less guessy.

Cache mounts: fast builds, clean images

Classic Docker taught people to delete package manager caches to keep images small. That’s fine for runtime images. It’s terrible for build speed if you rebuild frequently. Cache mounts let you keep the cache outside image layers.

If you build languages like Go, Rust, Java, Node, Python, you can cache module downloads and compilation caches. The exact mounts differ, but the principle is the same: keep mutable caches out of immutable layers.

Remote cache: your CI runner has amnesia

Ephemeral CI runners start from zero. If your builds are slow in CI but fast locally, it’s usually because your local machine has a warm cache and the runner doesn’t.

Exporting cache to local persistent storage is simplest. When that’s not available, export to a registry-backed cache. It’s not free: it increases registry traffic. But it’s often cheaper than paying engineers to stare at progress bars.

Joke #2: The only thing more ephemeral than a CI runner is the confidence of someone who just added --no-cache “to be safe.”

CI realities: remote caches, ephemeral runners, and sanity

CI is where cache strategies go to die—unless you design for it. The key difference between local builds and CI builds is not speed. It’s persistence. Developers have persistent disks. CI runners often do not.

Choose one of three CI caching strategies

  1. Runner-local persistent cache: works when runners are long-lived. Easy, fast, but less reproducible across pool changes.
  2. CI artifact cache: store BuildKit local cache directory as a CI cache artifact. Works well; depends on CI cache size and eviction policies.
  3. Registry cache: export/import cache via registry. Portable, works across runners, but increases pull/push traffic and can stress registries.

Don’t confuse image layers with cache layers

Pushing an image does not automatically mean you can reuse its build cache next run. Some cache can be inferred from existing images (especially if you rebuild the exact same Dockerfile and base), but reliable reuse in CI typically needs explicit cache export/import.

Network is part of the build

When builds are slow, people blame “Docker cache.” Then you look and see the slow step is pulling dependencies from the internet through a proxy that does TLS inspection and occasionally forgets how certificates work.

In those environments, local mirrors and dependency proxies are not a luxury. They’re build infrastructure.

Three corporate-world mini-stories from the trenches

Mini-story #1: The incident caused by a wrong assumption

The team had a reasonable goal: keep base images up to date. They used FROM debian:stable and ran apt-get update && apt-get upgrade -y during builds. Someone asked about cache; the answer was, “It’s fine, Docker caches layers.” That assumption walked into production like it owned the place.

Then a new CI cluster rolled out with ephemeral runners. Overnight, build times jumped. The pipeline started timing out, engineers restarted jobs, and concurrency spikes slammed the artifact repository and OS package mirrors. The mirrors throttled, builds retried, and the feedback loop got worse: slow builds caused more retries, which made builds slower.

The root of the problem wasn’t Docker. It was treating “stable” as stable and treating caching as global. debian:stable moved. apt-get update changed output. And the runners had no warm cache. Every build was a cold start plus a full distro refresh.

The fix was unglamorous: pin base images to a digest for the release branch, stop upgrading the entire OS during image build, and rebuild base images on a schedule with controlled rollouts. They also exported BuildKit cache to a shared cache backend. Build times became predictable again, and “predictable” is more important than “fast” when the deploy clock is ticking.

Mini-story #2: The optimization that backfired

A different org tried to speed up builds by collapsing Dockerfile steps. They took five RUN instructions and merged them into one mega-command to “reduce layers.” The image looked cleaner, and someone posted a screenshot of docker history like it was a fitness transformation.

The first week was fine. Then a single dependency changed—one package version bump. Because everything was in one RUN, the cache miss forced re-running a long chain: system packages, language runtime install, dependency downloads, build tooling setup. The cache had fewer entry points, so it was less reusable. They optimized for layer count, not rebuild time.

It got worse in CI. The mega-step was hard to diagnose. With smaller steps, the logs would have shown “download toolchain” or “install deps” as the slow part. With the mega-step, it was just a 9-minute shell script that sometimes failed with transient network errors. When it failed, it failed late.

They eventually reverted: keep layers meaningful and stable, merge only where it improves caching or correctness, not aesthetics. The final image was still slim using multi-stage builds, and builds were faster because the cache had more reusable boundaries.

Mini-story #3: The boring but correct practice that saved the day

A platform team maintained a “golden path” container build template. It wasn’t fancy. It enforced .dockerignore hygiene, multi-stage builds, and dependency installation keyed off lockfiles. It also enforced that volatile metadata (build timestamp, git SHA) was applied as labels in the final stage only.

When a supply-chain incident hit the industry and everyone started rebuilding images frequently, this team’s pipelines stayed calm. Not because they were lucky—because their rebuilds were incremental. Base images were pinned and rebuilt on schedule; app builds reused dependency layers; CI caches were exported to a persistent store.

Other teams were rebuilding from scratch and competing for bandwidth to pull the same dependencies. This team’s builds mostly hit cache and pulled only what changed. During the scramble, they shipped security patches faster and with fewer “pipeline is red” distractions.

The practice that saved them wasn’t a secret flag. It was treating the Dockerfile like production code and treating caching as an engineered system: inputs, outputs, and lifecycle. Boring. Correct. Effective.

Common mistakes: symptom → root cause → fix

1) Symptom: “Sending build context” takes forever

Root cause: bloated context (node_modules, dist, .git), weak .dockerignore, or building from repo root when only a subdir is needed.

Fix: tighten .dockerignore, build from a narrower directory, or restructure the repo so Docker context is small and stable.

2) Symptom: dependency install runs on every code change

Root cause: COPY . . before dependency install; lockfiles not isolated; dependency layer depends on the whole source tree.

Fix: copy only dependency manifests first (lockfile, package list), install dependencies, then copy source.

3) Symptom: CI is always cold even though local is fast

Root cause: ephemeral runners with no cache persistence; no BuildKit cache export/import.

Fix: export/import cache with buildx; persist local cache directory as CI cache; or use registry cache.

4) Symptom: builds became slow after “cleanup”

Root cause: someone added docker system prune -a on a schedule, or reduced cache retention, evicting build cache constantly.

Fix: set disk budgets; prune selectively; keep builder cache for active branches; avoid full prunes on shared runners.

5) Symptom: caches miss when only metadata changes

Root cause: volatile ARG/ENV (build timestamp, git SHA) defined early and used in cache keys.

Fix: move metadata to the end; use LABEL in final stage; don’t bake time into early layers.

6) Symptom: multi-arch builds are painfully slow

Root cause: building native dependencies for each platform; no per-platform caches; QEMU emulation overhead if cross-building.

Fix: use native builders per architecture when possible; export caches separately per platform; reduce native compilation in Docker build where feasible.

7) Symptom: image pushes are slow even when builds are fast

Root cause: runtime stage includes build artifacts; large layers change frequently; not using multi-stage properly.

Fix: keep runtime image minimal; copy only built outputs; ensure caches/build tooling stay in builder stage.

8) Symptom: “It used to be cached yesterday” mystery

Root cause: different builder instance, different Docker version, changed base image digest, or changed context due to generated files.

Fix: standardize builder; pin bases; verify .dockerignore; check which files changed; avoid floating tags for critical paths.

Checklists / step-by-step plan

Checklist A: Fix a slow build today (30–90 minutes)

  1. Re-run build with --progress=plain and identify the first non-cached slow step.
  2. Measure context transfer size; if it’s >100MB, treat it as a bug.
  3. Add/repair .dockerignore to exclude: .git, node_modules, dist, target, build, editor junk, test artifacts.
  4. Refactor Dockerfile so dependency manifests are copied first; install dependencies; then copy source.
  5. Move volatile args/labels to the final stage and as late as possible.
  6. Rebuild twice; the second run should be dramatically faster and show CACHED for heavy steps.

Checklist B: Make CI caching real (half day)

  1. Confirm CI uses BuildKit and a consistent builder (docker buildx).
  2. Pick cache persistence strategy: CI cache artifact or registry cache.
  3. Add --cache-from and --cache-to to CI build steps.
  4. Ensure cache keys include branch or mainline strategy (to avoid poisoning across incompatible changes).
  5. Set retention/eviction policies so the cache isn’t wiped daily.
  6. Add a pipeline metric: build duration breakdown (context time vs build time vs push time).

Checklist C: Keep it fast over months (operational discipline)

  1. Review Dockerfile changes like you review production config: diff for cache invalidation risk.
  2. Pin base images for release branches; update on schedule.
  3. Maintain a standard .dockerignore template per language ecosystem.
  4. Periodically audit builder disk usage; prune with intent, not anger.
  5. Run builds in a controlled environment: stable Docker/BuildKit versions across CI fleet.

FAQ

1) Why does changing one source file rebuild everything after a certain step?

Because the first cache miss changes the parent layer digest. Every subsequent step depends on that digest, so they all miss too. Fix the earliest miss by reordering and narrowing COPY inputs.

2) Does .dockerignore affect caching or only context size?

Both. It reduces transfer time and reduces the set of files that can invalidate COPY steps. Less input entropy means more cache hits.

3) Is it bad to use apt-get update in Dockerfiles?

No, but it’s often misused. Combine update and install in the same layer, and don’t expect stable caching if you want fresh indexes. For reproducibility, pin packages or build from curated base images.

4) Why is my local build fast but CI is slow?

Your laptop has a persistent builder cache. CI runners usually don’t. Export/import BuildKit cache, or provide persistent storage for the builder.

5) Will multi-stage builds make builds faster?

They can. Multi-stage builds let you cache expensive build steps in a dedicated stage and keep runtime images small. Smaller runtime images also push/pull faster, which often dominates CI time.

6) Should I merge RUN steps to reduce layers?

Only when it improves correctness or caching. Fewer layers can mean fewer cache reuse points, which can make rebuilds slower. Optimize for rebuild time and debuggability, not aesthetics.

7) What’s the difference between an image and a cache?

An image is a runnable artifact you push and deploy. A cache is build metadata and intermediate results used to speed up future builds. They overlap sometimes, but relying on that overlap is unreliable in CI.

8) Does changing the base image tag invalidate all caches?

If the resolved digest changes, yes: the parent layer changes, so everything downstream misses. Pin digests for stability when you need predictable caching and rollouts.

9) Are cache mounts safe? Will they leak into the final image?

Cache mounts are not committed into image layers by default. That’s the point. The cache lives on the builder host (or exported cache), not in the runtime filesystem snapshot.

10) What’s the single highest ROI fix for slow Docker builds?

Stop copying the whole repo before dependency installation. Isolate lockfiles, install dependencies in a stable layer, then copy source. Everything else is secondary.

Next steps that actually move the needle

If you want faster builds, stop treating Docker caching like vibes and start treating it like input hashing.

  1. Run one build with --progress=plain and write down the first expensive cache miss.
  2. Measure your context size. If it’s big, fix .dockerignore first. Always.
  3. Refactor your Dockerfile so dependency installation depends only on lockfiles, not the whole source tree.
  4. Enable BuildKit everywhere, then add cache export/import for CI so cold runners stop ruining your day.
  5. Move volatile metadata to the end and keep runtime images small with multi-stage builds.

Do those five things and your builds won’t just be faster—they’ll be predictable. Predictable builds are how you ship on time without bribing the pipeline gods.

← Previous
Docker Compose: Depends_on Lied to You — Proper Readiness Without Hacks
Next →
ZFS vs btrfs: Where btrfs Feels Nice and Where It Hurts

Leave a comment