Skip to content
Latchkey

docker buildx Cache Backends: Which to Use in CI

A CI runner starts with an empty Docker cache every time. buildx cache backends persist layers between runs, and choosing the wrong one silently evicts every other cache in your repository.

Docker layer caching works locally because your daemon keeps layers between builds. A CI runner has no such history, so every layer rebuilds unless you configure an external cache backend.

The backends differ mainly in where they store data and what that storage costs you. The default choice, type=gha, is convenient and shares a hard 10 GB budget with every other cache in the repository.

The backends

BackendStored inBest forLimitation
type=ghaGitHub Actions cacheSmall and medium imagesShares the 10 GB repository cap
type=registryA container registryLarge images, cross-repo sharingRegistry storage cost and auth
type=localRunner filesystemSelf-hosted with persistent diskUseless on ephemeral runners
type=inlineEmbedded in the imageSimple single-stage buildsFinal stage only, no intermediates
type=s3S3 or compatibleLarge scale, own storageMore configuration, credentials

mode=min against mode=max

.github/workflows/ci.yml
# min (default): only the final stage layers
cache-to: type=gha,mode=min

# max: intermediate stages too. What makes multi-stage builds benefit
cache-to: type=gha,mode=max

Registry cache for anything substantial

.github/workflows/ci.yml
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/build-push-action@v6
  with:
    context: .
    tags: ghcr.io/${{ github.repository }}:latest
    cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache
    cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=max

permissions:
  contents: read
  packages: write   # required or the cache push is rejected

Verify the cache is hitting

Terminal
docker buildx build --progress=plain . 2>&1 | grep -E "CACHED|importing cache" | head -20

# CACHED on the dependency install layer means it works.
# No CACHED lines at all means cache-from is not matching.

Frequently asked questions

Why is Docker not caching layers in CI?
Because a runner starts with an empty daemon every run. Local builds reuse layers your daemon already has; CI has none unless you configure an external cache backend with cache-from and cache-to.
Should I use type=gha or type=registry?
type=gha for small to medium images where you will not approach the 10 GB Actions cache cap. type=registry for large images, cross-repository sharing, or when you already push to a registry.
What does mode=max do?
Caches intermediate build stages as well as the final image. For multi-stage Dockerfiles this is what makes caching worthwhile, since the expensive build stage is otherwise discarded and not cached at all.
Does the Docker layer cache count against the Actions cache limit?
Yes, when using type=gha. It shares the 10 GB per-repository budget with every other cache, so a large layer cache can evict your dependency caches and slow down unrelated jobs.

Related guides

References

Run this faster and cheaper on Latchkey managed runners - self-healing included. Start free → 30-day trial · No credit card