# docker/build-push-action: layer cache backends, Buildx and multi-platform images

> Use docker/build-push-action v7 well: the inputs that matter, how it pairs with buildx, QEMU and metadata-action, and which layer cache to pick.

Source: https://latchkey.dev/learn/actions/docker-build-push-action  
Updated: 2026-11-12

docker/build-push-action runs a BuildKit build through Buildx from a workflow step: you give it a context, tags and `push: true`, and it builds and pushes to whichever registry an earlier step logged in to. Whether it is fast depends on `cache-from` and `cache-to`, and the usual `type=gha` backend shares the repository's 10 GB Actions cache with everything else you cache.

Most image builds in GitHub Actions are the same five steps: check out, set up QEMU, set up Buildx, log in, build and push. The step that does the work takes 33 inputs in `action.yml` at v7.4.0, and the workflow below sets seven of them. The rest matter when something specific goes wrong, which is usually the cache or a second platform.

The cache input has the most consequence. The usual `type=gha` backend draws on the same 10 GB repository cache as your dependency caches, so a large layer cache competes with them for space. Inputs and defaults below are from `action.yml` and the README at v7.4.0, read 2026-11-12.

## Five steps, with metadata-action tags

The workflow below is the README's pattern plus metadata-action, with each action pinned to the commit of its current release. `docker/metadata-action` turns the Git ref into tags and labels, so a push to `main` tags `main`, a tag `v1.2.3` tags `1.2.3`, `1.2` and `latest`, and a pull request tags `pr-2` without pushing.

One default catches people out. Without a `context` input the action builds from the Git context, `https://github.com/<owner>/<repo>.git#<ref>`, fetched by BuildKit itself. The README warns that any file a previous step changed is ignored, `.dockerignore` processing included. If an earlier step generates files the Dockerfile copies, check out and set `context: .` as below.

```.github/workflows/image.yml
name: image
on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
      - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
      - uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4.8.0
        if: github.event_name != 'pull_request'
        with:
          username: ${{ vars.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - id: meta
        uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
        with:
          images: user/app
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
      - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha,scope=app
          cache-to: type=gha,scope=app,mode=max
```

## The inputs that matter

List inputs take one entry per line; inputs marked List/CSV in the README also accept a comma-separated string. Defaults below are the ones `action.yml` declares.

| Input | Default | What it does | Watch for |
| --- | --- | --- | --- |
| `context` | Git context | The build context: a path or a URL | Set `.` when earlier steps change files |
| `file` | `{context}/Dockerfile` | Path to the Dockerfile | Set it when the Dockerfile is not at the context root |
| `push` | `false` | Shorthand for `--output=type=registry` | Needs a login step for the registry in each tag |
| `load` | `false` | Shorthand for `--output=type=docker` | Multi-platform load needs the containerd image store |
| `tags`, `labels` | none | Image names and OCI labels | Feed both from `docker/metadata-action` outputs |
| `platforms` | none | Target platforms, such as `linux/amd64,linux/arm64` | Foreign platforms need QEMU or native nodes |
| `build-args` | none | Values for `ARG` instructions | Not for secrets: they persist in the final image |
| `secrets` | none | `id=value` pairs for `RUN --mount=type=secret` | Mounted at `/run/secrets/<id>` by default |
| `cache-from`, `cache-to` | none | Where BuildKit reads and writes layer cache | See the backends below |
| `provenance`, `sbom` | provenance on since v4 | Attach SLSA provenance or an SBOM attestation | `provenance: false` if a consumer cannot read OCI multi-platform images |
| `outputs` | none | Any exporter, such as `type=local,dest=out` | For artifacts that are not an image |

> Build arguments persisting in the image is from Docker's build secrets page; the `/run/secrets` path is from Docker's GitHub Actions secrets page. Both read 2026-11-12.

## How it pairs with setup-buildx, setup-qemu and metadata-action

`docker/setup-buildx-action` creates and boots a builder with the `docker-container` driver. The build-push README calls it "not required but recommended", and in practice you want it: Docker's docs say the `gha` and `registry` cache backends work with the default `docker` driver only when the containerd image store is enabled, and Docker's multi-platform page says the stock runner setup needs `docker/setup-docker-action` to turn it on.

`docker/setup-qemu-action` registers emulators so one amd64 runner can build arm64 layers. It is only needed when `platforms` names an architecture the runner is not. Emulated `RUN` steps can be slow, which is why Docker also publishes a reusable workflow that splits the build across native runners (below).

`docker/metadata-action` has no build role at all: it reads the event and ref and writes `tags`, `labels` and `annotations` outputs. Its `type=semver` rules only produce tags on a tag push with a valid semver tag, and in its default `auto` mode it adds `latest` for tag-based rules such as `type=semver` and `type=ref,event=tag`.

## Cache backends: gha, registry and local

A fresh runner starts with an empty layer store, so without a cache every `RUN` executes again. `cache-to` exports layers somewhere that outlives the machine, and `cache-from` imports them at the start of the next build. `mode=min`, the default, exports only the final image's layers; `mode=max` exports every intermediate stage, which is what makes multi-stage builds hit.

`type=gha` stores blobs in the repository's GitHub Actions cache. GitHub limits that cache to 10 GB per repository by default, removes entries not accessed in 7 days, and evicts oldest-accessed entries first once you are over; raising the limit is billed and needs a payment method on file. The backend's `scope` defaults to `buildkit`, so two images built in one repository overwrite each other's cache unless each gets its own scope. Events such as `issue_comment` and `pull_request_target` get read-only cache access in the default-branch context, and the export then fails with [error writing layer blob: failed to reserve cache](/learn/failures/buildx-failed-to-reserve-cache-gha).

`type=registry` writes a separate cache image to a registry you already push to. It does not touch the Actions cache, and it needs push access from every job that exports. `type=local` writes a directory you save with `actions/cache`; it lands in the same 10 GB, and the directory grows on every run unless you pass `reset=true` (Buildx 0.35.0 or later). Measured numbers are on [Docker layer caching in GitHub Actions](/learn/speed/docker-layer-caching-in-github-actions) and [actions/cache vs buildx cache](/learn/speed/actions-cache-vs-buildx-cache).

```.github/workflows/image.yml, cache inputs
# GitHub Actions cache, one scope per image
cache-from: type=gha,scope=api
cache-to: type=gha,scope=api,mode=max

# A cache image beside the real one in your registry
cache-from: type=registry,ref=user/app:buildcache
cache-to: type=registry,ref=user/app:buildcache,mode=max

# A directory saved and restored by actions/cache
cache-from: type=local,src=${{ runner.temp }}/.buildx-cache
cache-to: type=local,dest=${{ runner.temp }}/.buildx-cache,mode=max,reset=true
```

> The 10 GB default, the 7-day eviction and the billed increase are from GitHub's dependency caching reference; the scope, mode and read-only behavior from Docker's gha cache backend and cache management pages; `reset=true` from Docker's local cache page. All read 2026-11-12.

## Multi-platform builds

List the platforms and push: the result is one tag pointing at an index with a manifest per platform. Pushing works on the default runner setup. Loading a multi-platform image into the runner's local image store does not, until you enable the containerd image store with `docker/setup-docker-action`; Docker's multi-platform page shows the daemon config.

In our experience emulation cost grows with how much the Dockerfile compiles. Docker's answer is the `docker/github-builder` reusable workflow: with `distribute: true`, its default, it runs one platform per runner and assembles the manifest at the end, sending Linux Arm platforms to `ubuntu-24.04-arm` and the rest to `ubuntu-24.04`. It is a reusable workflow, so it replaces your job rather than a step in it.

```.github/workflows/image.yml
jobs:
  build:
    uses: docker/github-builder/.github/workflows/build.yml@v1
    permissions:
      contents: read
      id-token: write
    with:
      output: image
      push: true
      platforms: linux/amd64,linux/arm64
      meta-images: user/app
      meta-tags: |
        type=ref,event=branch
        type=semver,pattern={{version}}
    secrets:
      registry-auths: |
        - registry: docker.io
          username: ${{ vars.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
```

## v4, v6 and v7 can break an unchanged workflow

Every major since v4 has a release note on the repository. v4, v6 and v7 can each break a workflow whose YAML did not change.

| Major | Released | What changed | What to do |
| --- | --- | --- | --- |
| v4 | 2023-01-30 | Provenance attestation on by default, which needs registry and runtime support for OCI multi-platform images | Set `provenance: false` if a consumer such as Cloud Run or Lambda rejects the image |
| v5 | 2023-09-12 | Node 20 runtime, runner v2.308.0 or later | Update self-hosted runners |
| v6 | 2024-06-17 | Build summary and a build record uploaded as an artifact | `DOCKER_BUILD_SUMMARY: false` to disable; filter `!*.dockerbuild` in `actions/download-artifact` |
| v7 | 2026-03-05 | Node 24 runtime, runner v2.327.1 or later; `DOCKER_BUILD_NO_SUMMARY` and `DOCKER_BUILD_EXPORT_RETENTION_DAYS` removed | Use `DOCKER_BUILD_SUMMARY` and `DOCKER_BUILD_RECORD_RETENTION_DAYS` |

> From the v4.0.0, v5.0.0, v6.0.0 and v7.0.0 release notes and the v7.4.0 README, read 2026-11-12.

## FAQ

### Do I need actions/checkout with docker/build-push-action?

Not for the default Git context: BuildKit fetches the repository at the triggering ref itself and uses the job's token for it. You do need checkout, and `context: .`, when an earlier step creates or edits files the build should see, because the Git context ignores those changes.

### How do I pass secrets to docker/build-push-action?

Use the `secrets` input with `id=value` lines and read them in the Dockerfile with `RUN --mount=type=secret,id=<id>`. Docker mounts each secret at `/run/secrets/<id>` for that instruction only. Do not use `build-args` for credentials, because build arguments persist in the final image.

### Why is Docker layer caching not working in GitHub Actions?

The common causes are a missing `cache-to`, `mode=min` on a multi-stage build, two images sharing the default `buildkit` scope, and a trigger with read-only cache access. Count the `CACHED` lines in the build log rather than timing it; [Docker layer caching not working](/learn/speed/docker-layer-caching-not-working) walks through each case.

### Why does docker/build-push-action fail with "failed to reserve cache"?

The image usually built and pushed; the gha cache export was refused. It happens on triggers that get read-only cache access, such as `pull_request_target`, and when jobs race for the same entry. Keep `cache-from: type=gha` there and drop `cache-to`, or add `ignore-error=true` if the export is optional.

## References

- [docker/build-push-action README and action.yml at v7.4.0 (read 2026-11-12)](https://github.com/docker/build-push-action/blob/v7.4.0/action.yml)
- [Docker docs: GitHub Actions cache backend](https://docs.docker.com/build/cache/backends/gha/)
- [Docker docs: cache management with GitHub Actions](https://docs.docker.com/build/ci/github-actions/cache/)
- [GitHub docs: dependency caching usage limits and eviction](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching)
- [Docker docs: multi-platform images with GitHub Actions](https://docs.docker.com/build/ci/github-actions/multi-platform/)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
