Skip to content
Latchkey

How to Cache Docker Layers in GitHub Actions

Every CI job starts with an empty Docker cache unless you give it one. That single fact explains most slow container builds, and it is fixable in about six lines.

A Docker build on a fresh runner has no layer cache, so every layer rebuilds regardless of whether anything changed. Locally you never see this because your daemon keeps layers between builds.

buildx supports external cache backends that persist layers between CI runs. The setup is short; the part worth understanding is which backend to use, because the obvious choice runs into the repository cache cap on any non-trivial image.

The minimal working setup

.github/workflows/ci.yml
- uses: docker/setup-buildx-action@v3

- uses: docker/build-push-action@v6
  with:
    context: .
    push: false
    tags: app:ci
    cache-from: type=gha
    cache-to: type=gha,mode=max

Choosing a cache backend

BackendStored inBest forWatch out for
type=ghaActions cacheSmall and medium imagesCounts against the 10 GB repository cap
type=registryA container registryLarge images, shared across reposRegistry storage cost and auth
type=localRunner diskSelf-hosted with persistent diskUseless on ephemeral runners
type=inlineThe image itselfSimple single-stage buildsFinal stage only, no intermediates

Registry cache for anything substantial

.github/workflows/ci.yml
- uses: docker/login-action@v3
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/build-push-action@v6
  with:
    context: .
    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

permissions:
  contents: read
  packages: write

Order your Dockerfile so the cache can work

No cache backend helps if every build invalidates the first layer. Copy dependency manifests and install before copying source, so a source change does not invalidate the install layer.

Dockerfile
# cache-hostile: any source change reinstalls everything
COPY . .
RUN npm ci

# cache-friendly: install only re-runs when the lockfile changes
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

Verify it is actually hitting

.github/workflows/ci.yml
- run: docker buildx build --progress=plain . 2>&1 | grep -E "CACHED|DONE" | head -20

# CACHED on the install layer means it is working.
# All layers rebuilding means cache-from is not matching.

Measure before you optimise

Pipeline optimisation usually targets the step people assume is slow. Get the real per-step timings first, because the answer is frequently dependency install or a cold cache rather than the build itself.

Terminal
# per-job timings for the last 20 runs
gh run list --limit 20 --json databaseId,conclusion,createdAt,updatedAt \
  --jq '.[] | "\(.conclusion)\t\(.createdAt)\t\(.updatedAt)"'

# per-step timing inside one run
gh run view <run-id> --log | grep -E "^\S+\s+.*Run |##\[group\]" | head -40

Frequently asked questions

Why is my Docker build slow in GitHub Actions but fast locally?
Your local daemon keeps layers between builds; a CI runner starts empty every time. Without an external cache backend every layer rebuilds on every run regardless of what changed.
What is the difference between mode=min and mode=max?
mode=min caches only the layers in the final image; mode=max also caches intermediate stages. For multi-stage builds mode=max is what makes caching worthwhile, at the cost of a larger cache.
Does the GitHub Actions cache have a size limit?
Yes, 10 GB per repository, shared across all caches. A mode=max Docker layer cache for a large image can consume it and evict your dependency caches, which shows up as unrelated jobs slowing down. Use a registry cache for large images.
Should I use type=gha or type=registry?
type=gha for small and medium images where you will not approach the 10 GB cap. type=registry for large images, for sharing a cache across repositories, or when you are already pushing to a registry anyway.

Related guides

References

Run this faster and cheaper on Latchkey managed runners - self-healing included. Start free → 30-day trial · No credit card