How Do I Cache Docker Layers in GitHub Actions?
Point the build at a remote Docker builder whose layer cache persists on its own disk, or export layers to a registry cache backend if you stay on the runner.
Last verified:
To cache Docker layers in GitHub Actions, either point the build at a remote Docker builder whose layer cache lives on the builder's own disk and outlives the job, or, if the build stays on the runner, configure a Buildx cache backend that exports layers to a registry or cache service and imports them on the next run. The first option removes the cache-to and cache-from flags entirely; the second keeps them and pays export and import minutes on every build.
Answer
Every GitHub Actions job gets a clean machine. The Buildx instance created inside that job starts with an empty layer store, and the store is destroyed with the machine when the job ends. That is why a Dockerfile with perfectly ordered layers still rebuilds every stage on every pull request: there is nothing on disk to reuse.
Two mechanisms fix that, and they differ in where the layers sit between runs.
| Approach | Where layers live between runs | What you configure | Per-build cache transfer |
|---|---|---|---|
| Remote Docker builder | On the builder profile's own disk, outside the job | Warpbuilds/build-push-action@v6 with a profile-name | None. Nothing is exported or imported. |
| Registry cache backend | In a cache tag in a registry you control | docker/setup-buildx-action plus cache-to and cache-from of type=registry | Export and import on every build |
| GitHub Actions cache backend | In the Actions cache service, scoped to the repository | The same two flags with type=gha | Export and import on every build, against the repository cache allowance |
The remote builder shape is the shorter workflow file. The build runs over TLS on a persistent builder VM, the layers it produces stay on that VM, and the next build reuses them because it lands on the same machine.
name: docker
on:
push:
branches: [main]
pull_request:
jobs:
image:
runs-on: warp-ubuntu-latest-x64-2x
steps:
- uses: actions/checkout@v5
- name: Log in to the registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: Warpbuilds/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
profile-name: "app-images"
timeout: 600000Three things about that file are worth reading twice. Warpbuilds/build-push-action@v6 is a drop-in replacement for docker/build-push-action@v6, so the context, push, and tags inputs keep their meaning. profile-name names the builder profile that carries the cache, and it is the only input the swap adds. There is no cache-to and no cache-from, because a cached builder holds the layers itself and reuses them on the next build, per the Docker builders documentation.
Drop the docker/setup-buildx-action step too, when its only purpose was creating a builder. The WarpBuild action provisions the remote builder and wires Buildx to it. If you want to keep your own build commands, Warpbuilds/docker-configure@v1 sets up the builder and hands back its details, and Warpbuilds/bake-action@v6 covers Bake files.
The rates that arithmetic runs on are on the pricing page and in the tables below.
Builders attach to any runner, which matters when your fleet is mixed. The builder actions also work from a GitHub-hosted runner or a laptop with an api-key input.
Detail
The limits worth knowing before you adopt it
Three behaviors decide whether the cache is there when a build asks for it.
The profile cache resets after 10 unused days. The builder cache has a TTL of 10 days, so a builder profile that goes unused for longer is reset automatically and the next build on it runs cold. Active repositories refresh the profile on every merge and never reach the window. A profile attached to a workflow that runs monthly reaches it every time, which makes the persistent cache worth less than the session it bills.
Concurrent builds share an eventually consistent cache. Multiple jobs can run on the same profile in parallel, and they read one shared cache, but layers written by one in-flight build may not be visible to another in-flight build. Synchronization catches later builds up. A matrix of five jobs that all build the same base stage from cold will therefore each build that stage, and the sixth job benefits. Stagger the first warm-up build if that duplication matters, or accept it as a one-time cost per profile.
Builder sessions bill separately from the runner. The runner and the builder are two independent resources and both appear on the invoice. A builder session is measured from when the builder action starts until the job completes, and concurrent jobs sharing a profile are billed as one session from the first job's start to the last job's completion. Multi-arch builds are the exception worth planning for: each architecture runs on its own builder instance, so an amd64 plus arm64 build produces two parallel sessions on the same profile, billed independently.
Two smaller facts follow from those. Builder size is chosen independently of runner size, so a 2 vCPU job runner can drive a 192 vCPU builder profile. And when you need to invalidate deliberately rather than wait for the TTL, the API exposes a cache reset per builder profile, documented in the Docker builders reference and covered in the Docker cache invalidation guide.
Builder sizes and rates
Sizes and per-minute rates come from the Docker builders documentation and the pricing page, checked on 2026-08-13.
| Builder size | Disk | Price per minute | Architectures |
|---|---|---|---|
| 16 vCPU, 32 GB | 100GB | $0.06 | amd64, arm64, multi |
| 32 vCPU, 64 GB | 200GB | $0.12 | amd64, arm64, multi |
| 64 vCPU, 128 GB | 200GB | $0.24 | amd64, arm64, multi |
| 96 vCPU, 192 GB | 600GB | $0.36 | amd64 |
| 96 vCPU, 192 GB | 2TB | $0.52 | amd64 |
| 192 vCPU, 384 GB | 600GB | $0.72 | amd64 |
| 192 vCPU, 384 GB | 2TB | $0.88 | amd64 |
arm64 and multi-arch profiles stop at 64 vCPU. The 96 vCPU and 192 vCPU sizes are available for amd64-only profiles. Two numbers guide the pick: roughly 8 vCPU and 16 GB of builder capacity per concurrent build job, per the concurrency guidance in the documentation, and enough disk to hold the layers of every image the profile serves, since eviction is what quietly turns a warm cache cold. The full catalog with per-size notes is on the remote Docker builder page.
What one warm build costs
The model below is arithmetic on stated assumptions rather than a measurement. It reuses the step durations from the worked model in the Docker layer caching guide: a 1.8 GB Node.js service image that occupies its job runner for 8.5 minutes with cold layers and 3.2 minutes with warm ones. Four builds share a builder session, with starts staggered by half a minute, so one session of 4.7 minutes carries four builds and each build is attributed 1.175 builder minutes.
GitHub rate from the GitHub Actions minute multipliers reference, checked on 2026-08-13. WarpBuild rates from the pricing page, checked on 2026-08-13.
| Line | GitHub-hosted 4-core Linux larger runner, cold layers | warp-ubuntu-latest-x64-2x plus a 16 vCPU builder profile, warm layers |
|---|---|---|
| Job runner rate per minute | $0.012 | $0.004 |
| Job minutes per build | 8.5 | 3.2 |
| Job cost per build | $0.1020 | $0.0128 |
| Builder rate per minute | none | $0.06 |
| Builder minutes attributed per build | none | 1.175 |
| Builder cost per build | none | $0.0705 |
| Total per build | $0.1020 | $0.0833 |
Reading it straight: on these assumptions the cached path costs $0.0187 less per build and returns the pull request 5.3 minutes sooner. Session sharing is the lever. At one build per session the builder line rises to $0.192 per build and the cached path costs more than the baseline, so the profile earns its rate when builds arrive in bursts and share it.
For a same-shape list price reference without any cache in the picture, warp-ubuntu-latest-x64-4x at 4 vCPU and 16 GB costs $0.008 per minute against $0.012 per minute for GitHub's 4-core Linux larger runner: 33 percent lower list price (GitHub pricing, checked 2026-08-13).
If the build stays on the runner
Some jobs cannot move the build off the runner: a Dockerfile that mounts paths produced earlier in the job, a compliance rule that pins the build to one machine, or a workflow you are not ready to change. Those keep in-job Buildx and ship the cache in and out.
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
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=maxmode=max exports intermediate stages as well as the final image, which is what makes a multi-stage Dockerfile reusable. The cost is transfer time on both ends of every build, and that number is the one to measure first, because it decides whether the backend pays for itself. WarpBuild Cache can serve as that backend through a local proxy, described in the caching documentation; the same page recommends the builder route for new work. The Buildx cache backend guide compares registry, GitHub Actions, and local backends on transfer cost and eviction.
Related Questions
Do I still need cache-from and cache-to with a remote Docker builder?
No. When the build runs on a builder profile, the layer cache sits on the builder's own disk and is reused automatically, so the cache-to and cache-from options are not required. Remove the docker/setup-buildx-action step as well when its only job was to create a builder, because the WarpBuild action configures the remote builder for you. The Docker layer caching guide shows the before and after of that swap on a full workflow file.
How long does a builder profile keep its layer cache?
The builder cache has a TTL of 10 days. A builder profile that goes unused for more than 10 days is reset automatically, and the next build on that profile runs cold and repopulates the cache. A profile that serves an active repository refreshes itself on every merge and never reaches the window. For deliberate resets and for the Dockerfile changes that invalidate layers early, see the Docker cache invalidation guide.
Do concurrent builds share the layer cache?
Yes, and the shared cache is eventually consistent. Layers written by one build may not be visible to another build running at the same moment, and they become available to later builds once synchronization completes. Two jobs racing on the same Dockerfile can therefore each pay for the same layer once. Sizing for that fan-out is covered on the remote Docker builder page, which lists the per-size capacity alongside the rates.
Do I pay for the GitHub Actions runner and the Docker builder separately?
Yes. The runner and the builder are two separate resources on two lines. Builders bill per session, measured from when the builder action starts until the job completes, and concurrent jobs on one profile share a single session. Per-minute rates for both halves are on the pricing page, and what is a remote Docker builder defines the underlying terms.
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.