Documentation · Caching
The Docker layer cache
Turn it on with one line. An OCI registry beside the fleet that BuildKit pushes layers to, so an unchanged layer is not rebuilt on the next job.
Building an image in CI usually means rebuilding every layer, because the builder starts empty on every job. The layer cache gives BuildKit somewhere to put those layers that survives between jobs.
It is an OCI registry we run beside the fleet, reachable from a job and from nowhere else. Layers are addressed by digest, so a layer that has not changed is not rebuilt and not re-uploaded.
Unlike the Actions cache, this one is metered by what it stores. The pricing page has the rate.
Add one step before your build:
- uses: runners-io/setup-docker-builder@v1Every docker build, docker buildx build and docker/build-push-action step after it in the same job reads its layers from the cache and writes new ones back. Nothing else in the workflow changes: no flags, no hostname, no credential.
jobs:
image:
runs-on: ghwarp-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: runners-io/setup-docker-builder@v1
- run: docker build -t app .
- run: docker run --rm app ./run-testsThe second time that job runs, the layers that have not changed come from the cache instead of being built again.
It checks the cache is there. The job must have been offered one, hold a credential for it, and get an answer from it. If any of that is missing, the step says which and changes nothing.
It makes sure the builder can export a cache. On our runners that is the default builder, which both caches to our registry and sees images an earlier step in the same job built — so a build
FROMan image you just built keeps working and is cached. (On an older Docker whose image store cannot do both, the step falls back to a container builder; see the note below.)It gives each build its own cache. One per image, worked out from the Dockerfile, the target and the local build context, so two images built in one job do not overwrite each other's.
It leaves alone any build that manages its own cache. A build that passes
--cache-fromor--cache-to, or that runs on another builder, is not changed.
A cache that cannot be reached never fails a build. The build runs without it, and is only slower.
If a build is FROM an image an earlier step in the same job produced, it keeps working. On our runners it is also cached, like any other build. On an older Docker whose image store cannot both cache and see local images, that one build runs without the layer cache (the step says so) rather than failing — every other build in the job still uses the cache.
One case the older-Docker fallback does not catch: an image you pulled and then gave a new name (docker pull node:22, then docker tag node:22 my-base:1) still looks to the step like an image a registry can serve, so a build FROM my-base:1 fails there with "pull access denied". This does not happen on our runners, which use the newer image store. If you meet it, build FROM the original name (FROM node:22), or pass automatic: false and wire the cache yourself.
On GitHub-hosted runners, or anywhere else, the step does nothing and says so in one notice. A workflow that runs in both places can keep it without a condition.
| Input | Default | What it does |
|---|---|---|
key | one per image | Names the cache. Set it to make builds share one cache that would otherwise get separate ones. |
automatic | true | false sets up the builder only, for wiring the cache yourself with the outputs below. |
| Output | What it holds |
|---|---|
enabled | true when the cache was set up, false when the step did nothing. |
cache-from, cache-to | Values for docker/build-push-action's inputs of the same names. Empty when the step did nothing, which that action reads as no cache. |
To wire it yourself:
- id: layers
uses: runners-io/setup-docker-builder@v1
with:
automatic: false
- uses: docker/build-push-action@v6
with:
context: .
cache-from: ${{ steps.layers.outputs.cache-from }}
cache-to: ${{ steps.layers.outputs.cache-to }}docker buildx bake and docker compose build are not changed by the step. Give them the cache-from and cache-to outputs the same way.
The cache belongs to one repository of one GitHub account, and it is keyed by their GitHub ids as well as their names:
Two repositories in the same account do not share layers.
Two accounts never do.
An account or repository name that is freed and registered again by someone else does not inherit the previous owner's layers.
Renaming a repository starts it a new, empty cache.
Which cache a request reaches comes from the job's own credential, never from the reference it asks for. A job that names another repository's reference, exactly, reaches its own cache and finds nothing there. We test this on the live fleet from two separate GitHub accounts: one builds, and the other asks for those layers by their exact reference and by every digest, and is refused each time.
A build looks for layers its own branch wrote first and then the default branch's, and writes only to its own branch. A build started by an event that may not write the cache, such as pull_request_target, reads the default branch's alone and writes nothing. A feature branch never reads a sibling's.
The Docker layer cache is different from the Actions cache here: nothing deletes its layers by age. They are kept until your organisation's layer cache reaches 10 GiB, and then we delete the layers that no cache tag of yours still points to and that no push has written for seven days. Every repository in the organisation counts towards that one 10 GiB.
A push writes a layer when it uploads it, and when it relies on one already here: then we write the layer again, at most once a day. A pull writes nothing, so a cache that is only ever read ages from the last push that wrote it.
A layer a cache tag still points to is kept, however old it is. Each build that writes its cache points its branch's tag at that build's layers, so the layers an older build needed and this one does not are the ones that can go.
Each branch has tags of its own, and they outlive the branch. The layers a deleted branch's tags point to are kept, and count towards the 10 GiB.
If deleting does not make enough room, new layers are not stored, and the build carries on without them. Layers a tag points to are never deleted to make room, so a cache full of them stays full until you ask us to clear it.
To have layers deleted sooner, ask us, as the privacy policy says.
Because the cache is metered by what it stores, the layers it keeps are the layers you pay for.
It caches layers, not build context and not test fixtures. If your slow step is downloading dependencies rather than building image layers, the Actions cache is the mechanism that helps, and using both is normal.
It does not make an image available to a later job as an image. A job that needs the built image should push it to a registry, or build it again — the layer cache makes that second build fast, but it is not an artefact store.
Where the layer cache holds image layers, a sticky disk holds a whole filesystem across jobs for the same repository. Build tools with their own on-disk caches — Bazel, Gradle, cargo, go build — usually want that rather than this.