Title: GitHub Actions Docker cache: faster image builds
URL: https://codemany.com/articles/docker-layer-caching-github-actions
Description: GitHub Actions Docker cache cuts image build time. Compare cache-from, the gha cache backend and a local registry mirror, with copy-paste workflow steps.
All pages as Markdown: https://codemany.com/llms.txt

---

[Home](https://codemany.com/) › [Articles](https://codemany.com/articles)

# Docker layer caching in GitHub Actions

A GitHub Actions Docker cache saves image layers from one build so the next build can reuse them. You can store those layers in a registry, in the GitHub Actions cache, or on a warm machine that keeps them.

## Why Docker builds are slow in CI

On your laptop, a second `docker build` is fast. Docker kept the layers from the first one.

A GitHub-hosted runner is a fresh machine for each job. Its layer cache starts empty. So every build pulls the base image again and runs every step again: `apt-get install`, `npm ci`, compiling. That is why Docker builds feel slow in CI.

## How layer caching works

A Dockerfile is a list of steps. Each step makes a layer. Docker gives each layer a key based on the step and the files it used.

If the key matches a saved layer, Docker skips the step. Once one step changes, every step after it runs again. So order matters:

```
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci                 # cached until the lock file changes
COPY . .                   # your code changes often, so copy it last
RUN npm run build
```

Put things that rarely change at the top. Put your code near the bottom. Then the slow install step stays cached.

A `.dockerignore` file helps too. It keeps files like `node_modules` and `.git` out of the build. Fewer files means fewer changes, so more layers stay cached.

Multi-stage builds help as well. Build in one stage, then copy only the result into a small final image. The heavy build stage can stay cached while the final image stays small.

## Option 1: cache-from and cache-to

BuildKit can export its cache to a registry and import it next time. You use `docker cache-from` and `cache-to`, through `docker/build-push-action`:

```
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: ghcr.io/my-org/my-app:latest
    cache-from: type=registry,ref=ghcr.io/my-org/my-app:buildcache
    cache-to: type=registry,ref=ghcr.io/my-org/my-app:buildcache,mode=max
```

`mode=max` saves every layer, not just the final ones. This works on any runner. The cost is time: each build downloads and uploads the cache over the network.

You also need write access to the registry. For GitHub's registry, give the job `packages: write` and log in first with `docker/login-action`.

## Option 2: the GitHub Actions cache backend

BuildKit can also store layers in the GitHub Actions cache. It needs no registry:

```
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
  with:
    context: .
    push: false
    cache-from: type=gha
    cache-to: type=gha,mode=max
```

This is the easiest way to cache a Docker image in GitHub Actions. But it shares your repo's cache space with every other cache. Big images can push other caches out. Read [GitHub Actions cache, explained](https://codemany.com/articles/github-actions-cache) for the limits and eviction rules.

## Option 3: a warm machine with a registry mirror

The third way is to stop starting from zero. Run the build on a machine that keeps its Docker cache between jobs. That is a self-hosted runner.

On your own machine, a plain `docker build` uses the Docker engine's own cache, which stays on disk. Layers from the last build are still there. No download, no upload. One catch: `docker/setup-buildx-action` makes a new builder per job and removes it after. Set `driver: docker` on it to build with the engine and keep its cache.

Codemany runs GitHub Actions jobs on your own Mac or Linux machines. On Linux, it adds a Docker Hub mirror on the machine:

- BuildKit's `docker.io` pulls go to the local mirror first.
- If the mirror is down or lacks an image, BuildKit pulls from Docker Hub. The build never fails because of the mirror.
- The mirror only pulls public images, with no login. It never holds your credentials or private images.
- Layers expire a week after their last pull, and the total size is capped.

Note one tradeoff. If you turn on Codemany's opt-in VM per job on Linux, each job starts in a fresh VM, so its caches start cold. The default host mode keeps them warm. See [local cache](https://codemany.com/features/local-cache) and [Docker in Docker](https://codemany.com/articles/github-actions-docker-in-docker).

## Which one to pick

| Option | Good for | Watch out for |
| --- | --- | --- |
| cache-from / cache-to | Any runner, big caches | Network time each build |
| GitHub Actions cache | Small images, no registry | Shares the repo's cache limit |
| Warm machine | Frequent builds | You run the machine |

You can mix them, too. Many teams start with `type=gha`. When builds are still slow, they move to a warm machine. In our nightly [benchmarks](https://codemany.com/benchmarks), jobs on Codemany run 1.5×–4.3× faster on private code than on GitHub's 2-core runners. If your whole pipeline is slow, read [why GitHub Actions is slow](https://codemany.com/problems/github-actions-slow).

Whatever you pick, check that it works. Look at the build log. Cached steps show `CACHED` next to them. If every step runs each time, your key or your Dockerfile order needs work.

## Questions

### Does GitHub Actions cache Docker layers by default?

No. Each fresh hosted runner starts with an empty layer cache.

### What is a registry mirror?

A local copy of image pulls, so base images come from the machine, not Docker Hub.

### Does Codemany mirror Docker Hub?

Yes, on Linux. It points BuildKit's docker.io pulls at a local mirror, and falls back to Docker Hub.

## Related

Read next: [GitHub Actions cache](https://codemany.com/articles/github-actions-cache), [local cache](https://codemany.com/features/local-cache), [Docker in Docker](https://codemany.com/articles/github-actions-docker-in-docker), [slow GitHub Actions](https://codemany.com/problems/github-actions-slow) and the [benchmarks](https://codemany.com/benchmarks).

[Start a 14-day trial](https://codemany.com/docs/get-started) No card needed. $20 a month for 2 machines after that.
