Docker build cache not hitting in CI: how to debug cache keys
Debugs Docker build cache misses in CI. Use when CI builds are slow because layers rebuild every run, cache keys do not hit, or cache imports/exports silently fail. Covers cache backends, key stability, and verifying hits. Not for Dockerfile logic errors, registry auth, or local build slowness.
TL;DR
A Docker cache that never hits in CI is almost always a key or backend problem: the cache key changes every run, the cache backend is unreachable, or the export never happened. Check that the cache is actually exported at the end of the build, that the key inputs are stable across runs, and that the import step finds the previous cache. Verify with buildx's cache provenance output, not by timing the build.
Error / query
Docker build cache not hitting in CI: how to debug cache keysUse this skill when
- CI builds take full time on every run with no cache reuse
- You configured caching but builds do not get faster
- Cache import/export steps succeed yet nothing is reused
- Builds are fast locally but slow in CI
Not for this skill when
- The Dockerfile itself is wrong (logic error, not cache)
- The build fails (fix the failure first)
- Registry authentication fails (auth problem)
Steps
Step 1: Confirm the cache export actually runs
# in the CI build step, use:
docker buildx build --cache-to=type=registry,ref=[registry]/[image]:cache --cache-from=type=registry,ref=[registry]/[image]:cache .Expected: the build log shows cache export at the end. If --cache-to is missing or the export step is skipped on failure, there is no cache for the next run to hit; the most common miss is simply never exporting.
Step 2: Check that cache keys are stable across runs
git diff HEAD~1 -- Dockerfile [dependency-files]Expected: the files that feed cache keys (Dockerfile, lockfiles, base image tags) are unchanged between runs. A key that includes a timestamp, commit SHA, or branch name changes every run and can never hit. Pin keys to content hashes of dependency files only.
Step 3: Verify hits in the build log
docker buildx build --progress=plain --cache-from=type=registry,ref=[registry]/[image]:cache . 2>&1 | grep -i "CACHED"Expected: CACHED lines for unchanged layers. Zero CACHED lines with a valid export means the key mismatched (step 2) or the backend returned nothing (step 4).
Step 4: Test the cache backend directly
docker buildx build --cache-from=type=registry,ref=[registry]/[image]:cache --progress=plain . 2>&1 | grep -i "cache" | head -5
curl -s -o /dev/null -w "%{http_code}" https://YOUR-registry/v2/[image]/tags/listExpected: the registry serves the cache tag (200 on the tags list). If the cache ref 404s or auth fails on the cache repo, the import silently yields nothing; fix backend permissions or the ref.
Variant phrasings
"docker buildx cache not working ci"
Steps 1-3. Export missing and unstable keys cover most cases.
"github actions docker cache miss"
Same diagnosis, plus check the actions/cache key inputs for run-varying values.
Why it happens
BuildKit's cache is content-addressed: a layer hits only if its inputs (including the cache key scope) match exactly. CI adds moving parts (fresh runners, timestamps, branch names) that break key stability, and cache backends fail silently on the import side, so the build just runs uncached with no error.
Edge cases and pitfalls
COPY . .before dependency install invalidates everything on any file change; order the Dockerfile so stable layers come first.- Multi-arch builds need per-arch cache or
--cache-towith mode=max; the default minimal export misses intermediate layers. - Registry cache backends count against storage and pull quotas; scope cache refs per branch to avoid unbounded growth.
- Local cache (
type=local) does not survive ephemeral CI runners; use registry or a remote backend for CI.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_M4OM2tQP-RoIa-8KIz0GZQ
Maintainer review
No maintainer verification is recorded for this version.
This records the version a maintainer checked. It does not assert that the version is the latest upstream release.