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.

cache-to decides where BuildKit writes layers. Two of the three backends draw on the same 10 GB repository 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.
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/secretspath 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.
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 and 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 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.


