---
title: "docker/build-push-action: the cached, pinned configuration"
description: "How to configure docker/build-push-action in GitHub Actions: BuildKit via setup-buildx, cache-from/cache-to type=gha layer caching, SHA-pinned. Copyable YAML."
url: https://starsling.dev/github-actions/docker-build-push-action
canonicalUrl: https://starsling.dev/github-actions/docker-build-push-action
---

# The right way to configure docker/build-push-action

GitHub Action - Docker. Last updated: 2026-07-10

docker/build-push-action builds and pushes a Docker image in one step; the two decisions that matter are creating a docker-container builder with docker/setup-buildx-action and turning on the layer cache with cache-from/cache-to: type=gha.

## Recommended configuration

setup-buildx-action creates the docker-container builder that the type=gha cache backend needs - the Engine's default docker driver cannot export to it - and mode=max caches intermediate stages so a multi-stage Dockerfile restores its builder layers instead of rebuilding them. Every action is pinned to a commit SHA.

```yaml
- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

- name: Build and push
  uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
  with:
    context: .
    push: ${{ github.event_name != 'pull_request' }}
    tags: ${{ steps.meta.outputs.tags }}
    labels: ${{ steps.meta.outputs.labels }}
    cache-from: type=gha
    cache-to: type=gha,mode=max
```

## What each line buys you

| Config | What it does | Impact |
|---|---|---|
| cache-from / cache-to: type=gha, mode=max | Stores build layers in the GitHub Actions cache and restores them next run; mode=max also caches intermediate build stages. | Turns a full image rebuild into a layer restore on a cache hit - the build's dominant cost. Speedup is workload-dependent; unmeasured on your repo. |
| docker/setup-buildx-action | Creates a docker-container builder. The Engine's default docker driver cannot export to the type=gha cache backend; this one can. | Enables the cache above; no direct speedup on its own. |
| docker/metadata-action | Derives tags and OCI labels from the event (branch, tag, sha). | unmeasured |
| uses: ...@<40-char-sha> | Pins every action to an immutable commit instead of a mutable @v tag. | Security: blocks a mutable-tag supply-chain swap. No speed impact. |

## Companion actions

### docker/setup-buildx-action

Creates a docker-container builder. Required before build-push-action for the type=gha cache to work at all: the default docker driver cannot export to that backend.

```yaml
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
```

Docs: https://github.com/docker/setup-buildx-action

### docker/login-action

Authenticates to the registry with the built-in GITHUB_TOKEN. Guard it with `if: github.event_name != 'pull_request'` so fork PRs, which can't push, still build.

```yaml
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}
```

Docs: https://github.com/docker/login-action

### docker/metadata-action

Derives tags and OCI labels from the event instead of a hand-maintained tag expression. Feed its outputs into build-push-action's tags/labels.

```yaml
- id: meta
  uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
  with:
    images: ghcr.io/${{ github.repository }}
    tags: |
      type=ref,event=branch
      type=semver,pattern={{version}}
      type=sha,format=long
```

Docs: https://github.com/docker/metadata-action

## Avoid these

### Building without a layer cache

Every run rebuilds every layer from scratch, so the base-image install and dependency layers pay full cost on each push - even when nothing about them changed.

```yaml
- uses: docker/build-push-action@v7
  with:
    context: .
    push: true
    tags: ghcr.io/acme/app:latest
    # no cache-from / cache-to: every layer rebuilds from scratch, every run
```

### Raw docker build && docker push, on the default driver

BuildKit does back the Engine's default `docker` driver, but that driver cannot export to the `type=gha` cache backend - so there is no cache to restore from. The tag is a floating `:latest` on top of that: two problems in three lines. `setup-buildx-action` (which creates a `docker-container` builder) plus `build-push-action` fixes both.

```yaml
- run: |
    docker build -t ghcr.io/acme/app:latest .
    docker push ghcr.io/acme/app:latest
    # the default docker driver can't export to type=gha, so there's no layer cache
```

### Floating :latest and unpinned images

A floating `:latest` base image resolves to a different image over time, so builds stop being reproducible and every layer below the `FROM` misses the cache once the base moves. An unpinned service image in Compose (`postgres:latest`) is not a build input, so it costs you reproducibility rather than cache hits - your tests silently run against a different Postgres. Pin both to a real version (`node:22-bookworm-slim`, `postgres:16.4`).

```dockerfile
# Dockerfile
FROM node:latest          # moves under you: a different Node per build

# The same problem in docker-compose.yml, for your test services:
#   image: postgres:latest
```

### Unpinned action references

A @v7 tag or @master branch can be repointed at malicious code after you adopt it (the tj-actions/changed-files compromise did exactly this). Pin to a full commit SHA with the version in a trailing comment.

```yaml
- uses: docker/build-push-action@v7   # a tag anyone with push can move
- uses: docker/login-action@master     # branch-floating: worst case
```

## Seen in the wild

Real workflow files from public projects.

### immich-app/immich - .github/workflows/cli.yml

```yaml
      - name: Build and push image
        uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
        with:
          file: packages/cli/Dockerfile
          platforms: linux/amd64,linux/arm64
          push: ${{ github.event_name == 'release' }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
```

Key lines: `uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0`, `cache-from: type=gha`, `cache-to: type=gha,mode=max`.

A SHA-pinned build-push-action step that reads and writes the GitHub Actions cache with type=gha and mode=max for a multi-arch CLI image.

Source: [immich-app/immich `.github/workflows/cli.yml` lines 109-116](https://github.com/immich-app/immich/blob/557189d7a8c182b3f4271bb31db2228f3844b68b/.github/workflows/cli.yml#L109-L116) at commit `557189d`.

### mastodon/mastodon - .github/workflows/build-container-image.yml

```yaml
        uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
        with:
          context: .
          file: ${{ inputs.file_to_build }}
          build-args: |
            MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }}
            MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}
            SOURCE_COMMIT=${{ github.sha }}
          platforms: ${{ matrix.platform }}
          provenance: false
          push: ${{ inputs.push_to_images != '' }}
          cache-from: ${{ inputs.cache && format('type=gha,scope=build-{0}-{1}', hashFiles(inputs.file_to_build), env.PLATFORM_PAIR) || '' }}
          cache-to: ${{ inputs.cache && format('type=gha,mode=max,scope=build-{0}-{1}', hashFiles(inputs.file_to_build), env.PLATFORM_PAIR) || '' }}
```

Key lines: `uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6`, `cache-from: ${{ inputs.cache && format('type=gha,scope=build-{0}-{1}', hashFiles(inputs.file_to_build), env.PLATFORM_PAIR) || '' }}`, `cache-to: ${{ inputs.cache && format('type=gha,mode=max,scope=build-{0}-{1}', hashFiles(inputs.file_to_build), env.PLATFORM_PAIR) || '' }}`.

A SHA-pinned build-push-action whose gha cache is scoped per Dockerfile hash and platform, so parallel matrix legs never overwrite one another's layers.

Source: [mastodon/mastodon `.github/workflows/build-container-image.yml` lines 80-92](https://github.com/mastodon/mastodon/blob/ea3d8a82c244cccb3c3a7673c206e3668fd3a8d3/.github/workflows/build-container-image.yml#L80-L92) at commit `ea3d8a8`.

### qdrant/qdrant - .github/workflows/integration-tests.yml

```yaml
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
      - name: Install dependencies
        run: sudo apt-get install -y clang jq
      - name: Docker build
        uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
        with:
          context: tools/schema2openapi
          tags: schema2openapi
          cache-from: |
            type=gha,scope=${{ github.ref }}-schema2openapi
            type=gha,scope=${{ github.base_ref }}-schema2openapi
          cache-to: type=gha,mode=max,scope=${{ github.ref }}-schema2openapi
```

Key lines: `uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0`, `uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0`, `type=gha,scope=${{ github.ref }}-schema2openapi`, `type=gha,scope=${{ github.base_ref }}-schema2openapi`, `cache-to: type=gha,mode=max,scope=${{ github.ref }}-schema2openapi`.

Buildx first, then a SHA-pinned build-push-action on the pull-request job of a vector database. Its cache-from lists two gha scopes, the branch's own and the base branch's, so the first build on a new branch still starts from a warm cache.

Source: [qdrant/qdrant `.github/workflows/integration-tests.yml` lines 127-139](https://github.com/qdrant/qdrant/blob/44ad62f8cd69642be5afa6441612525e24a0d063/.github/workflows/integration-tests.yml#L127-L139) at commit `44ad62f`.

## Verify on your repo

Hand this prompt to your coding agent to audit and fix your Docker CI in your own repo:

Inspect this repo's .github/workflows for how it builds Docker images. For every job that builds an image, check that it (1) sets up BuildKit with docker/setup-buildx-action before the build, (2) uses docker/build-push-action with cache-from: type=gha and cache-to: type=gha,mode=max (or a registry cache), (3) derives tags from docker/metadata-action rather than a floating :latest, and (4) pins every `uses:` to a full commit SHA. Flag any raw `docker build`/`docker push` shell steps, any missing cache, any :latest tag, and any unpinned action ref. Fix them, then show me the diff and open a PR rather than applying it blindly.

Ground these changes in the upstream docs before you edit: https://docs.docker.com/build/cache/backends/gha/, https://github.com/docker/metadata-action. If you cannot fetch them, say so rather than guessing, and cite what you used in the PR description.

Prefer to check by hand:

- Find your Docker build: `grep -rnE 'docker/build-push-action|docker build' .github/workflows/`.
- Confirm a `docker/setup-buildx-action` step runs before the build - without it the type=gha cache is silently unavailable.
- Check the build step has both `cache-from: type=gha` and `cache-to: type=gha,mode=max` (or a registry cache); a build with neither rebuilds every layer.
- Read a warm run's log: BuildKit prints `CACHED` on the layers it restored. If every layer says it ran, the cache isn't hitting - check that the base image is pinned, not :latest.
- Confirm every `uses:` is a 40-character commit SHA with the version in a trailing comment.

## FAQ

### What is the difference between cache-from and cache-to?

`cache-from` is the read side: it restores layers from an existing cache before the build. `cache-to` is the write side: it exports the layers this build produced. You need both - `cache-from` alone restores a cache nothing ever writes, so the first run misses and every run after it misses too. See /ci/docker for the whole Docker workflow this pair sits in.

### mode=max or mode=min for cache-to?

`mode=min` (the default) caches only the layers of the final image. `mode=max` also caches the intermediate stages, which is what a multi-stage Dockerfile needs: without it the builder stage - where the dependency install and compile happen - rebuilds every run even though the final image restored fine. Use `mode=max` unless cache size is the binding constraint.

### gha cache vs registry cache - which should I use?

`type=gha` is the simplest on GitHub-hosted runners - one `setup-buildx-action` step, no registry credentials or storage to manage - but the Actions cache is 10 GB per repository by default, evicts least-recently-used entries when it fills, and drops any entry unused for 7 days. Use a registry cache (`type=registry,ref=...:buildcache`) when your layer cache outgrows that budget, or when you want a cache that survives Actions-cache eviction on a repo that builds infrequently.

### Why is my Docker build not using the cache?

The usual causes: no `docker/setup-buildx-action` step (the default docker driver cannot export to `type=gha`, so the cache is ignored), only `cache-from` without `cache-to` (nothing is written for the next run to restore), a base image on `:latest` (it moves, so the first layer always misses), or `mode=min` on a multi-stage build (intermediate stages aren't cached). Fix the one that applies and BuildKit will print `CACHED` on the restored layers.

### Should I pin docker/build-push-action to a version or a SHA?

Pin to a full commit SHA with the version in a trailing comment (`docker/build-push-action@<sha> # v7.x`). A bare `@v7` tag is mutable - anyone who can push to the action's repo can move it - so shipping one in a template would contradict the pin-action-shas practice. The SHA is immutable; the comment keeps Dependabot's upgrade chain readable.

## Related

- [Docker CI on GitHub Actions - the whole workflow](https://starsling.dev/ci/docker)
- [Pin GitHub Actions to a commit SHA](https://starsling.dev/best-practices/github-actions/pin-action-shas)
- [Cache dependencies in GitHub Actions](https://starsling.dev/best-practices/github-actions/cache-dependencies)

## Sources

1. [docker/build-push-action](https://github.com/docker/build-push-action)
2. [Docker docs - GitHub Actions cache backend (type=gha)](https://docs.docker.com/build/cache/backends/gha/)
3. [Docker docs - Cache management with GitHub Actions](https://docs.docker.com/build/ci/github-actions/cache/)
4. [docker/metadata-action](https://github.com/docker/metadata-action)
5. [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action)
6. [GitHub docs - Publishing Docker images to GHCR](https://docs.github.com/en/actions/publishing-packages/publishing-docker-images)
7. [GitHub docs - Security hardening: pin third-party actions to a full SHA](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions)
