---
title: "Docker Layer Caching in GitHub Actions | StarSling"
description: "Configure docker/build-push-action with cache-from and cache-to type=gha so CI restores unchanged image layers instead of rebuilding every layer on every run."
url: https://starsling.dev/github-actions/optimizations/use-docker-layer-caching
canonicalUrl: https://starsling.dev/github-actions/optimizations/use-docker-layer-caching
---

# Use Docker layer caching in GitHub Actions

[GitHub Actions](https://starsling.dev/github-actions) / [Optimizations](https://starsling.dev/github-actions/optimizations) / Use Docker layer caching in GitHub Actions

- [How StarSling works](https://starsling.dev/)

Rule: ci.cache.docker-layers. Detection mode: static. Last updated: 2026-08-20

A Docker build in CI with layer caching restores every unchanged layer from cache and rebuilds from the first changed instruction; a plain docker build on an ephemeral runner starts from zero every run.

## Table of contents

- [Do this](#do-this)
- [Avoid this](#avoid-this)
- [How to detect it](#how-to-detect-it)
- [Tradeoffs and safety](#tradeoffs)
- [Verify it worked](#verify)
- [Related pages](#related-pages)
- [Sources](#sources)

<a id="do-this"></a>

## Do this

GitHub-hosted runners are ephemeral, so the local layer cache that makes docker build fast on a laptop is empty at the start of every CI run. Each run re-executes every Dockerfile instruction: base image pull, apt-get, dependency install, compile. Wiring BuildKit's GitHub Actions cache backend into docker/build-push-action persists layers between runs, so a change to application code reuses every layer above it and the build starts from the first instruction whose inputs changed.

_.github/workflows/ci.yml_

```yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - name: Build image
        uses: docker/build-push-action@v6
        with:
          context: .
          tags: myapp:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
```

<a id="avoid-this"></a>

## Avoid this

_.github/workflows/ci.yml_

```yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .
```

<a id="how-to-detect-it"></a>

## How to detect it

1. Search for plain builds: `grep -rn 'docker build\|docker compose build' .github/workflows/` - a bare docker build on a hosted runner rebuilds every layer every run.
2. Search for the action: `grep -rn 'docker/build-push-action' .github/workflows/` and check each use for `cache-from` and `cache-to`; the action builds uncached when both are absent.

<a id="tradeoffs"></a>

## Tradeoffs and safety

- The GitHub Actions cache backend shares the repository's 10 GB cache allowance with actions/cache; mode=max stores every layer (best hit rate, biggest footprint), mode=min stores final-stage layers. Watch eviction on repositories with many large caches.
- Cache entries follow GitHub's branch scoping: a pull request restores from its own branch and the default branch. A first build on a new branch still pays the cold cost once.
- Order Dockerfile instructions so what changes most often comes last; layer caching restores prefixes, and a COPY . . near the top invalidates everything below it on any file change.
- Never write secrets into image layers to make them cacheable; use build secrets (`--mount=type=secret`), which stay out of layers and out of the cache.

<a id="verify"></a>

## Verify it worked

Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to run this audit and open the fix as a reviewable PR:

```
Audit this repository's Docker builds in CI and add layer caching where it is missing.

1. grep .github/workflows/ for docker build, docker compose build, and
   docker/build-push-action. For each build, note whether cache-from and cache-to are set.
2. For uncached builds, convert to docker/setup-buildx-action + docker/build-push-action
   with cache-from: type=gha and cache-to: type=gha,mode=max, preserving the existing
   context, tags, and push behavior exactly.
3. Read the Dockerfile: if a COPY of the whole source tree sits above dependency
   installation, propose reordering so dependency layers cache independently.
4. Read the Docker GitHub Actions cache docs linked on this page before editing.
5. Show the full diff and open a pull request; do not apply changes blindly. In the PR
   body, state how to verify: re-run the same commit and look for CACHED instructions.
```

Confirm the change landed:

1. Re-run the workflow on the same commit; the build log should show `CACHED` for unchanged instructions instead of executing them.
2. Change one application source file and confirm the build reuses the base and dependency layers, re-executing from the first instruction that copies the changed file.
3. Compare the build step's duration before and after on a warm cache; dependency-heavy images typically drop from minutes to well under one.

<a id="related-pages"></a>

## Related pages

- [Optimize Turborepo caching in GitHub Actions](https://starsling.dev/github-actions/optimizations/optimize-turborepo)
- [GitHub Actions cache: dependencies, keys, and cache hits](https://starsling.dev/best-practices/github-actions/cache-dependencies)
- [docker/build-push-action: a copyable workflow](https://starsling.dev/github-actions/docker-build-push-action)
- [Docker CI, end to end](https://starsling.dev/ci/docker)

<a id="sources"></a>

## Sources

- [Docker: GitHub Actions cache](https://docs.docker.com/build/ci/github-actions/cache/)
- [Docker: cache backends (gha)](https://docs.docker.com/build/cache/backends/gha/)
- [docker/build-push-action](https://github.com/docker/build-push-action)
