What Is the Difference Between cache-from and cache-to?
cache-from tells a Docker build where to read cache and cache-to where to write it. Set one without the other and the build reads a cache nothing refreshes.
cache-from is the import side of a Docker build and cache-to is the export side: the first tells BuildKit where to read an existing layer cache, the second tells it where to write the cache the build produces. A build that sets only cache-from imports whatever already exists and publishes nothing, so the workflow reads as correctly configured while the cache it depends on goes stale and eventually disappears.
Answer
Both flags take the same backend syntax, and neither one implies the other. --cache-from and --cache-to are documented as separate options on docker buildx build, each accepting a comma-separated backend specification, in the buildx build reference, checked on 2026-08-13. The equivalent inputs on docker/build-push-action carry the same names and the same values.
Four configurations show up in real workflows, and three of them are broken:
- Both flags, same reference. The build reads the cache written by the previous run and writes a new one over it. This is the working configuration.
cache-fromonly. The import succeeds for as long as something at that reference exists, and nothing refreshes it. GitHub removes cache entries that have not been accessed in 7 days and evicts the least recently used entries once a repository passes its 10 GB cache allowance, per the GitHub dependency caching reference, checked on 2026-08-13. A registry reference has no such expiry, so it keeps serving the layers it held when it was last written and the hit rate decays as the Dockerfile changes.cache-toonly. Every build pays the export and no build reads the result.- Both flags, different references. The build reads reference A and writes reference B, which is what a copy-paste edit to one line produces. The job log shows a successful export and a cold build on every run.
The backend value is where most of the behavior lives. These are the common ones, from the cache storage backends reference, checked on 2026-08-13:
| Backend value | Valid on | Where the cache lives | What the next run sees |
|---|---|---|---|
type=inline | cache-to only | Embedded in the image config that the build pushes | Read it back with cache-from type=registry,ref=<image tag>. Min mode only |
type=registry,ref=<ref> | both | A separate tag in a container registry | Any builder with pull access to that registry, on any machine |
type=gha | both | The GitHub Actions cache service for the repository | The same branch and its base branch, under GitHub's cache scope rules and the 10 GB repository allowance |
type=local,dest=<dir> / src=<dir> | both | A directory on the runner disk | Nothing, unless a separate cache action carries the directory between jobs |
type=s3 / type=azblob | both | A bucket in your own cloud account | Whatever your bucket retention policy leaves in place |
Two asymmetries in that table cause most of the confusion. type=inline is an export format and has no import counterpart, so the import line for an inline cache names the image tag through the registry backend. And type=local uses dest= when it writes and src= when it reads, so the two lines are not copies of each other.
One prerequisite sits under all of it. Docker documents the default docker driver as supporting inline cache export only, so a workflow that wants the registry, gha, local, or s3 backend has to build on a docker-container, kubernetes, or remote builder, per the builder drivers reference, checked on 2026-08-13. That is what docker/setup-buildx-action creates when a workflow adds it.
Detail
mode=min and mode=max
mode is a parameter on cache-to, and it decides how much of the build gets exported. The default mode=min exports 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 and then discarded, per the cache storage backends reference, checked on 2026-08-13.
For a single-stage Dockerfile the two modes are close to identical. For the multi-stage pattern that most application images use, the difference is the entire point of the cache. A Dockerfile that compiles in a builder stage and then copies one binary into a small runtime image exports almost nothing useful under mode=min, because the expensive stage never appears in the final image. The next build re-runs the compile, the job log shows a cache export that completed successfully, and the cache reference on disk is a few megabytes when the build stage was gigabytes.
The cost of mode=max is transfer volume. Every intermediate layer is written on export and is a candidate for download on import, so the amount of data moved scales with the number of build stages instead of just the size of the final image. The worked example under What the two lines cost turns that into a per-build and per-month figure for one registry cache size.
Setting both flags against the same reference
This is the working shape for a registry cache on GitHub Actions. The cache reference is separate from the image tags, the pull request reads the branch cache first and falls back to the default branch cache, and the export uses mode=max so the build stage is included.
name: docker
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: warp-ubuntu-latest-x64-8x
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: ${{ github.event_name == 'push' }}
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
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=maxThree details in that block are worth copying. cache-from takes a list and the entries are tried in order, which is how a pull request gets a warm start before its own branch cache exists. The cache tag is cache-<branch> rather than an image tag, so cache blobs never end up in the tag list your deploys read. And cache-to names the branch reference, which means a pull request writes its own cache and leaves the default branch cache alone.
Add ignore-error=true to the cache-to value when a failed export should leave the job green, for example when the registry credentials on a fork pull request cannot write.
The remote builder alternative
The other way to answer the question is to stop moving the cache. A remote Docker builder keeps the layer cache on the builder's own disk, so the import and export lines leave the workflow entirely:
- - uses: docker/setup-buildx-action@v3
-
- - uses: docker/build-push-action@v6
+ - uses: Warpbuilds/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:cache-main
- cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:cache-main,mode=max
+ profile-name: "super-fast-builder"With Docker Builders the cache-to and cache-from options are not required, because a cached builder keeps the layers and reuses them for later builds. The Docker builders documentation is the reference for every action input, and the builder cache has a 10 day TTL, so a profile that goes unused for more than 10 days is reset. For dependency caching that has nothing to do with Docker layers, the WarpBuild caching documentation covers the drop-in replacement for actions/cache.
What the two lines cost
Import and export are runner minutes, and they are the reason the flags are worth thinking about at all. The assumptions here are stated so you can substitute your own from your job log: a 3 GB mode=max registry cache that takes 1 minute to import and 1.5 minutes to export, 500 builds per month, on warp-ubuntu-latest-x64-8x at $0.016 per minute from the pricing page, checked on 2026-08-13.
| Line | Minutes per build | Minutes per month | Cost per month |
|---|---|---|---|
| Cache import | 1.0 | 500 | $8.00 |
| Cache export | 1.5 | 750 | $12.00 |
| Cache transfer, total | 2.5 | 1,250 | $20.00 |
A remote builder replaces both lines with builder session minutes. The 16 vCPU, 32 GB, 100 GB disk profile is $0.06 per minute, billed per session from when the builder action starts until the job completes, and concurrent jobs on the same profile share one session, from the Docker builders documentation, checked on 2026-08-13. The runner is billed separately from the builder, and the builder size is independent of the runner size. Linux runners are the usual host for a Docker build job.
The Docker builds on GitHub Actions hub covers the rest of the pipeline, and the buildx cache backends guide works through which backend fits a given repository.
Related Questions
What happens if I set cache-from but not cache-to?
The build imports whatever cache already sits at that reference and exports nothing, so the reference stops being refreshed while the Dockerfile and the lockfiles move on. On type=gha the entry disappears once GitHub evicts it under the 7 day access window or the 10 GB repository allowance, per the GitHub dependency caching reference. On type=registry the tag keeps serving layers from whenever it was last written, so the hit rate falls run by run with no error in the job log. Registry cache covers what that tag actually holds.
Does mode=max work with every cache backend?
No. Inline cache supports min mode only, because the cache metadata is embedded in the image config that gets pushed, which is covered in inline cache. The registry, gha, local, s3, and azblob backends all accept mode=max, which exports the layers of every build stage instead of only the layers of the final stage.
Do I still need both flags on a remote Docker builder?
No. With WarpBuild Docker Builders the cache-to and cache-from options are not required, because a cached builder keeps the layers on its own disk and reuses them for later builds. Removing both flags removes the import step and the export step from the job. The buildx cache backends guide compares that setup against a tuned registry cache, and the Docker builds on GitHub Actions hub puts it in the context of a full pipeline.
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.