Docker Caching in GitHub Actions
Traditional Docker caching with actions/cache + docker save/docker load works, but it is usually slower than modern Buildx-native caching:
- It creates large tarballs.
- Upload/download time grows quickly.
- You can burn through GitHub’s 10 GB cache quota.
For most projects, use Docker Buildx cache exporters/importers directly.
1) GitHub Actions Cache Backend (type=gha)
type=gha is usually a good default.
Why it is better than tarball caching
- Layer-aware: only changed layers are uploaded.
- Faster: no
docker save/docker loadplumbing. - Simple: fewer workflow steps to maintain.
- Managed by GitHub: old entries are evicted automatically.
Basic workflow example
- name: Set up Docker 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 }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
Recommended branch scoping
Without a scope, different branches may fight over the same cache.
cache-from: type=gha,scope=${{ github.ref_name }}
cache-to: type=gha,mode=max,scope=${{ github.ref_name }}
A common pattern is:
- per-branch scope for feature branches
- shared scope (for example
main) for your default branch to maximize reuse
2) Registry Cache Backend (type=registry)
Use this when:
- images are very large,
- you need cache persistence beyond GitHub cache eviction,
- or you want to bypass repository-level cache limits.
Why teams choose registry cache
- No GitHub cache quota pressure (cache lives in your registry).
- Portable across runners and systems as long as they can access the registry.
- Durable until you delete or expire the cache tag.
GHCR example
- name: Log into GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
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
Keep cache in a dedicated tag (like
:buildcache) so release tags stay clean.
3) mode=min vs mode=max
mode=mincaches only layers used by the final exported image.mode=maxcaches intermediate layers too (great for multi-stage Dockerfiles).
If your Dockerfile has build stages for dependencies, compilation, or tests, prefer mode=max.
4) Practical Patterns
A) PR builds + mainline builds
- PRs: use
type=ghawith branch scope. - Main: use
type=ghawith stable scope ortype=registryfor long-term reuse.
B) Multi-arch builds
With linux/amd64,linux/arm64, registry cache often gives better long-term behavior for larger artifacts.
C) Keep Dockerfiles cache-friendly
- Copy lockfiles first, install dependencies, then copy app source.
- Use
.dockerignoreaggressively. - Avoid invalidating early layers with frequently changing files.
5) Quick Comparison
| Feature | actions/cache + tarball | type=gha | type=registry |
|---|---|---|---|
| Speed | Slowest | Fast | Fast |
| Granularity | Whole tarball | Layer-based | Layer-based |
| Storage location | GH Actions cache | GH Actions cache | Container registry |
| 10GB repo cache limit | Yes | Yes | No (registry policy applies) |
| Setup complexity | Highest | Low | Low–Medium (auth required) |
6) Troubleshooting Checklist
- Cache misses every run
- Check you are using the same
scope/refas intended. - Verify Dockerfile step ordering is stable.
- Check you are using the same
- Cache exports but no speedup
- Ensure expensive layers happen before frequently changing
COPYsteps. - Switch to
mode=max.
- Ensure expensive layers happen before frequently changing
- Registry cache grows too much
- Add retention/cleanup policy for
:buildcachetags.
- Add retention/cleanup policy for
- Permission errors with GHCR
- Confirm workflow token/package permissions allow write access.
Suggested Default
For most repos:
- Start with
type=gha+mode=max. - Add branch scoping.
- Move to
type=registrywhen scale, retention, or cache size requires it.
This keeps workflows simple while improving build times in most repos.