The right way to configure docker/build-push-action
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 docker/build-push-action 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. [sources]
- 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=maxWhat each line of the Docker build buys you
- Config
cache-from / cache-to: type=gha, mode=maxWhat it doesStores build layers in the GitHub Actions cache and restores them next run; mode=max also caches intermediate build stages.ImpactTurns 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. - Config
docker/setup-buildx-actionWhat it doesCreates a docker-container builder. The Engine's default docker driver cannot export to the type=gha cache backend; this one can.ImpactEnables the cache above; no direct speedup on its own. - Config
docker/metadata-actionWhat it doesDerives tags and OCI labels from the event (branch, tag, sha).Impactunmeasured - Config
uses: …@<40-char-sha>What it doesPins every action to an immutable commit instead of a mutable @v tag.ImpactSecurity: blocks a mutable-tag supply-chain swap. No speed impact.
Actions that pair with build-push-action
build-push-action rarely runs alone. These three set up the builder, authenticate the push, and derive tags - each pinned like the block above.
docker/setup-buildx-action
Docs for docker/setup-buildx-action (opens in new tab)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.
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0docker/login-action
Docs for docker/login-action (opens in new tab)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.
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}docker/metadata-action
Docs for docker/metadata-action (opens in new tab)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.
- 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=longDocker build anti-patterns to avoid
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.
- 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 runRaw 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.
- 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 cacheFloating :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
FROM node:latest # moves under you: a different Node per build
# The same problem in docker-compose.yml, for your test services:
# image: postgres:latestUnpinned 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.
- uses: docker/build-push-action@v7 # a tag anyone with push can move
- uses: docker/login-action@master # branch-floating: worst caseSeen in the wild
Real workflow files from public projects.
- 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 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) || '' }} - 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 }}-schema2openapiVerify on your repo
Hand this prompt to your coding agent (Claude Code, Cursor, and the like) 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-actionstep runs before the build - without it the type=gha cache is silently unavailable.Check the build step has both
cache-from: type=ghaandcache-to: type=gha,mode=max(or a registry cache); a build with neither rebuilds every layer.Read a warm run's log: BuildKit prints
CACHEDon 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.
Automate this with StarSling.
Rather than audit this by hand, install the StarSling GitHub App: it opens a pull request to move your workflows onto StarSling runners, then its agents keep reading your jobs, run logs, and machine telemetry and open pull requests that fix what this page covers, along with caching, parallelization, and test sharding across the rest of your pipeline. Every change arrives as a PR you review and merge.
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
Where to go next.
Docker CI on GitHub Actions - the whole workflow
Want the complete, copyable workflow, not just this one action? Start here.
Pin GitHub Actions to a commit SHA
Why the template pins every action to a 40-char SHA, and how to convert your own.
Cache dependencies in GitHub Actions
The dependency-store analog of Docker layer caching for npm, pip, cargo, and Go.
Sources
1docker/build-push-action (opens in new tab)
2Docker docs · GitHub Actions cache backend (type=gha) (opens in new tab)
3Docker docs · Cache management with GitHub Actions (opens in new tab)
4docker/metadata-action (opens in new tab)
5docker/setup-buildx-action (opens in new tab)
6GitHub docs · Publishing Docker images to GHCR (opens in new tab)
7GitHub docs · Security hardening: pin third-party actions to a full SHA (opens in new tab)
Last updated 2026-07-10
Configure it once. StarSling keeps it configured.
One line to install. Faster runs on day one, and agents that open reviewable PRs to keep your pipeline tuned.