Skip to content
compiler.dev

Docker Buildx Cache in GitHub Actions: gha, registry

Last updated

To cache Docker builds in GitHub Actions, use docker/setup-buildx-action and docker/build-push-action with cache-from: type=gha and cache-to: type=gha,mode=max. That stores BuildKit's layer cache in the GitHub Actions cache. Four backends exist (gha, registry, inline, local); this guide shows each, when to pick it, and the limits.

If your build is slow and you are not sure why, start with Docker build slow in CI. For the concepts, see Docker layer caching in CI.

The default setup: type=gha

name: Docker
on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v7
        with:
          context: .
          push: false
          tags: app:ci
          cache-from: type=gha
          cache-to: type=gha,mode=max

Setting up Buildx is required. The Docker docs (checked October 2026) say the default docker driver works with the gha backend only when the containerd image store is enabled; otherwise you need a different driver, and setup-buildx-action creates one. The cache URL and token are filled in automatically inside a workflow.

Parameters that matter

From the Docker gha backend docs:

ParameterDefaultMeaning
scopebuildkitName of the cache object. Use one per image.
modeminmin exports only layers of the final image; max exports all layers of all stages.
ignore-errorfalseDo not fail the build if exporting the cache fails.

Use mode=max for multi-stage builds. With min, intermediate stages (your dependency install, your compile step) are not in the cache, and those are the expensive ones. max stores them at the cost of a larger cache.

Set a scope per image. The default scope is shared, so two images built in one repository overwrite each other's cache:

cache-from: type=gha,scope=web
cache-to: type=gha,scope=web,mode=max

In a matrix, include the matrix value in the scope, for example scope=build-${{ matrix.platform }}.

The docs also describe a newer cache service API (v2) selected by URL or a version setting. If you see cache errors after a GitHub-side change, check that your Buildx and BuildKit versions are current, because older ones target the old API.

Throttling

Heavy cache export can hit API rate limits. The build action passes github-token (default secrets.GITHUB_TOKEN) to Buildx to help avoid throttling; leave it set.

Limits of type=gha

  • It lives in the GitHub Actions cache, so it shares the 10 GB per-repository quota with npm, pip and everything else. A mode=max Docker cache can use a few GB on its own.
  • Entries not used for 7 days are removed, and the least recently used entries go first when the repository is over quota. See cache limits.
  • PRs read the base branch cache and write their own scoped entries. Build on main too so the base cache stays warm.
  • Self-hosted runners may not have access to the GitHub cache service, depending on setup. Check before relying on it; a registry cache avoids the question.

type=registry

Store the cache as an image in a container registry:

- uses: docker/login-action@v3
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v7
  with:
    context: .
    push: true
    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

Use this when the cache is large, when runners are not on GitHub's cache network, or when you want local developers and CI to share a cache. It does not count against the 10 GB Actions quota (registry storage rules apply instead). Pushing and pulling a big cache over the network costs time, so keep the registry close to the runner. Some registries do not support cache manifests; check yours.

type=inline

Embeds cache metadata in the image you push, so cache-from can use the image itself:

cache-from: type=registry,ref=ghcr.io/org/app:latest
cache-to: type=inline

It is the simplest option and needs no extra tag. The trade-off: only mode=min is supported, so intermediate stages of a multi-stage build are not cached. Fine for a single-stage Dockerfile, weak for multi-stage.

type=local

Writes the cache to a directory, which you save with actions/cache:

- uses: actions/cache@v6
  with:
    path: /tmp/.buildx-cache
    key: buildx-${{ runner.os }}-${{ github.sha }}
    restore-keys: |
      buildx-${{ runner.os }}-
- uses: docker/build-push-action@v7
  with:
    context: .
    cache-from: type=local,src=/tmp/.buildx-cache
    cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max

This is the legacy pattern. It never shrinks on its own because each export adds to the directory, so people add a step to move -new over the old directory. type=gha replaces it for most cases. Use local if you need a cache that you control entirely with actions/cache keys.

Which backend?

NeedUse
Simple, small to medium imagestype=gha,mode=max with a scope
Large cache, shared with laptops, or not on GitHub cachetype=registry,mode=max
Single-stage Dockerfile, minimal configtype=inline
Full control through actions/cachetype=local

Cache mounts and why they are lost on fresh runners

RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt gives a build step a persistent directory, so pip does not redownload everything when the layer reruns. That is a great feature on your laptop or on a long-lived builder.

On GitHub-hosted runners it does nothing for you by default. A cache mount lives in BuildKit's local state on the builder, not in the layer cache that cache-to exports. A fresh runner means a fresh BuildKit, so the mount is empty on every job. type=gha and type=registry export layers, not mounts.

Options if you rely on cache mounts:

  1. Treat them as a local optimisation only and rely on layer caching in CI.
  2. Persist the mount directory yourself. Third-party tooling such as the reproducible-containers/buildkit-cache-dance action exists to save and restore cache mount contents through actions/cache. Evaluate it for your security needs before adopting it.
  3. Use a persistent builder. Dedicated build services keep BuildKit state between runs. Depot does this for Docker builds; see the Depot comparison. A self-hosted runner with a persistent disk can also work.

Make the cache hit

  1. Order the Dockerfile so dependency layers come first (see slow Docker builds).
  2. Keep .dockerignore tight.
  3. Use --progress=plain or the build summary to look for CACHED lines.
  4. Build on main so branches have a base to read.

compiler.dev keeps a Docker layer cache on the machine between jobs, so a plain docker build can reuse layers without exporting them. Switch with runs-on: [self-hosted, Linux, compiler-ci]; see pricing.

FAQ

Should I use mode=max or mode=min?

max for multi-stage builds, since it caches intermediate stages. min is smaller and fine for single-stage images.

Why do two images overwrite each other's cache?

They share the default scope buildkit. Give each a unique scope.

Can I use type=gha on self-hosted runners?

It depends on whether the runner can reach GitHub's cache service. Test on a small build, or use a registry cache.

Does the gha cache count against my 10 GB limit?

Yes. It is stored in the same Actions cache.

Do I need setup-buildx-action?

For type=gha and most backends, yes. The default driver does not support them without the containerd image store.

Run this Docker build on compiler.dev 200 free minutes

Start free