## 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
```text
Docker build cache not hitting in CI: how to debug cache keys
```

## Use 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
```bash
# 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
```bash
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
```bash
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
```bash
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/list
```
Expected: 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-to` with 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
