Registry Cache

A registry cache stores Docker build layers as a separate image in a container registry, so any builder able to pull from that registry reuses them.

A registry cache is a Docker layer cache stored as an image in a container registry, kept under its own tag and separate from the application image the build produces. Any builder that can pull from that registry can read it, so a build running on a machine that has never seen the project reuses layers produced by an earlier run on a different machine.

That property is what makes the registry backend common in GitHub Actions. A job usually starts on a machine with an empty local build cache, so the cache has to live somewhere the next machine can reach.

Definition

BuildKit, the engine behind current Docker builds, can export its layer cache to an external backend instead of leaving it on local disk. type=registry is one of those backends, documented in the registry cache backend reference, checked on 2026-08-13. The backend writes the cache as a normal registry artifact, so registry authentication, storage quotas, replication, and retention rules apply to it exactly as they apply to an application image.

The exported artifact carries two kinds of content. The first is the layer blobs themselves. The second is a cache config blob, media type application/vnd.buildkit.cacheconfig.v0, which records the dependency graph of the build: which step produced which layer, and the digests of the inputs that step consumed. The config blob is what turns a pile of blobs into a usable cache, because it lets the next build decide whether a step it is about to run matches a step already recorded.

Import is lazy by design. --cache-from type=registry,ref=... fetches the manifest and the config blob first, compares step digests against the graph it is building, and downloads a layer blob only for the steps that match. A build with no matching steps pays for two small metadata requests rather than a full cache download.

One prerequisite trips people up. Docker documents the registry backend as unsupported by the default docker driver, so the build has to run on a builder created with the docker-container, kubernetes, or remote driver, per the builder drivers reference, checked on 2026-08-13.

The backend accepts these parameters, from the same registry backend reference:

ParameterValuesDefaultWhat it controls
refimage referencerequiredThe repository and tag the cache is written to and read from
modemin, maxminWhether every stage is exported or only the final one
oci-mediatypestrue, falsetrueWhether the cache manifest uses OCI media types
image-manifesttrue, falsefalseExports the cache as an OCI image manifest rather than a manifest list
compressiongzip, estargz, zstdgzipBlob compression applied to exported layers
compression-levelintegerbackend specificCompression effort for the chosen algorithm
force-compressiontrue, falsefalseRecompresses layers that already exist in another format
ignore-errortrue, falsefalseWhether a failed cache export also fails the build

image-manifest=true matters for registries that reject a manifest list under a tag. It has to be paired with oci-mediatypes=true, and it is the usual fix when an export succeeds against one registry and fails against another with the same workflow file.

A registry cache is distinct from the metadata carried inside a pushed image, which the inline cache term covers. The registry backend writes a dedicated artifact and can therefore record intermediate stages; inline metadata rides along with the image and is limited to what that image already contains.

Example

This workflow builds an image, pushes it to the GitHub Container Registry, and keeps its layer cache under a second tag in the same repository. docker/setup-buildx-action creates a docker-container builder, which satisfies the driver requirement above.

name: image
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - 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: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache
          cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=max

The first run finds nothing at the :buildcache reference, executes every step, then pushes two artifacts. The second run lands on a fresh machine with an empty local cache, resolves the same reference, and imports the layers whose step digests still match. Those steps report CACHED in the build log:

 => CACHED [build 3/5] RUN go mod download
 => CACHED [build 4/5] COPY . .
 => [build 5/5] RUN go build -o /out/app ./cmd/app

After a successful run the registry repository holds two tags with different jobs:

TagContentsTop artifactPulled by
:<sha>Application image layers and image configImage manifest or indexA container runtime, at deploy time
:buildcacheExported layer blobs and the BuildKit cache config blobManifest list by default, image manifest under image-manifest=trueThe next build, through cache-from

The :buildcache tag is a build input rather than a runnable image, so docker run against it fails. Treat it as a storage line item: mode=max on a multi-stage Dockerfile writes every stage, which is why teams often set a registry lifecycle rule on the cache tag.

Branch strategy is the other decision the reference encodes. A single shared tag means concurrent branches overwrite each other's cache. Giving each branch its own tag and reading from both the branch tag and the default branch keeps the two apart, since cache-from may be given more than once:

          cache-from: |
            type=registry,ref=ghcr.io/${{ github.repository }}:cache-${{ github.ref_name }}
            type=registry,ref=ghcr.io/${{ github.repository }}:cache-main
          cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:cache-${{ github.ref_name }},mode=max

Access control follows registry permissions. Writing the cache needs push rights on the repository, reading it needs pull rights, and a workflow triggered by a fork pull request usually has read-only credentials. Passing ignore-error=true on cache-to lets those runs read the cache and skip the export instead of failing the build.

FAQ

Where does a registry cache actually live?

In a container registry, as its own tag next to the application image. The cache tag holds the exported layer blobs plus a BuildKit cache config blob that records which build step produced which layer, so it is pulled like any other artifact and never runs as a container.

Why does my registry cache export fail with the default builder?

Docker documents the registry backend as unsupported on the default docker driver. Create a builder on the docker-container, kubernetes, or remote driver first, which is what docker/setup-buildx-action does when it creates a docker-container builder for the job.

What does mode=max change?

The default mode=min exports only the layers of the final stage of the resulting image. mode=max exports the layers of every stage, including build stages whose output is copied forward, so a multi-stage Dockerfile can reuse its compile stage on the next run.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.