docker/build-push-action failed to solve, and what a missing buildx really prints
docker/build-push-action failed to solve is BuildKit reporting that a build it was asked to run did not complete, wrapped once on its way out; the words after the colon are the failure and the four before it are packaging. Skipping docker/setup-buildx-action does not produce this message, which matters because that is the pairing most guidance makes.

What this error means
A docker/build-push-action step fails with a line beginning ERROR: failed to and the temptation is to read the first few words. There are two different wrappers and they are set at different depths. failed to solve: comes from BuildKit's client when the solve request itself returns an error, so the cause is inside the build: a Dockerfile instruction that exited non-zero, a context that could not be read, a base image that could not be resolved. failed to build: comes from the buildx command layer, one level further out, and it carries failures decided before any solve was attempted, which is where a driver that cannot do what you asked lands. A workflow with no buildx setup step produces the second, never the first.
ERROR: failed to build: Cache export is not supported for the docker driver.
Switch to a different driver, or turn on the containerd image store, and try again.Two wrappers, two depths
In buildkit's client/solve.go, a non-nil error from the Solve call is wrapped with failed to solve. In buildx's commands/build.go, the whole build is wrapped with failed to build. Both are single-word wrappers with nothing added, so neither tells you anything on its own, but the depth they sit at does.
A failure that reaches the solve call has already passed driver selection, export validation and option parsing. A failure that never gets that far comes out under the outer wrapper. That is the distinction to make first, and the table is the two of them side by side.
| Prefix | Wrapped where | Kinds of failure it carries |
|---|---|---|
failed to solve: | BuildKit client, around the solve request | A failing RUN, an unreadable context, an unresolvable base image |
failed to build: | The buildx command, around the whole build | A driver that cannot do what was asked, a rejected export or option |
Common causes
A cache-to or multi-platform build ran on the default docker driver
The real version of "buildx was not set up", and it announces itself with a feature name rather than with anything about buildx. The workflow gained a cache-from and cache-to pair, or a second platform, without gaining a setup step, so every build now fails before it starts. Both quoted reports here are exactly this.
The build itself failed and the wrapper is just the outermost line
When the prefix is failed to solve:, the driver was fine and the build was reached. A RUN that exited non-zero, a COPY whose source is not in the context, or a base image that could not be pulled all arrive with that prefix and their own text after it. Nothing about buildx setup is involved.
A setup step exists but this build is not using its builder
The builder input, a conditional on the setup step, or a matrix leg that skips it all leave the build on the ambient default. The Builder info group settles it in one look, and in our experience this is the case people spend longest on because the workflow plainly contains a setup step.
The buildx binary is genuinely absent
Only on images that do not ship it, which in practice means minimal self-hosted runners. The action says so in its own words and names the setup action, and that is the one time the phrase "buildx is not set up" is literally what happened.
How to fix it
Split the line at the first colon and read the right half
- Take the prefix:
failed to solveorfailed to build. - On
failed to solve, ignore the driver entirely and read the build failure that follows. - On
failed to build, read the feature name at the start of the next sentence; it names what was asked for.
Add the setup step when you add the feature that needs it
Cache export and multi-platform builds both require a driver that supports them, so the setup step belongs in the same change as the cache-to or the second platform. Adding it afterwards, once the builds are failing, is the same edit made under pressure.
- uses: docker/setup-buildx-action@v4
- uses: docker/build-push-action@v7
with:
push: true
tags: ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=maxCheck the Builder info group before changing the workflow
- Expand
Builder infoin the failing step. - Read the
driverfield:dockermeans the ambient default,docker-containermeans a buildx builder. - If it says
dockerand your workflow has a setup step, find out why that step did not apply to this job.
Keep inline cache when you deliberately have no builder
Where a job genuinely should stay on the default driver, type=inline is the one cache export it accepts, because the check in build/opt.go exempts it. It is weaker than a registry or Actions cache, but it does not fail the build.
- uses: docker/build-push-action@v7
with:
push: true
tags: ghcr.io/${{ github.repository }}:latest
cache-to: type=inlineWhat a missing setup-buildx step actually does
The action's own README is unusually direct about this: setup-buildx is described as not required but recommended, for multi-platform builds, cache export and similar. Without it, docker buildx build still runs, using the ambient default builder, which on a GitHub-hosted runner is the docker driver backed by the daemon. Ordinary single-platform builds and pushes work.
What stops working is anything the docker driver lacks. The check lives in build/opt.go, which refuses a non-inline cache-to and refuses multi-platform exports, and the sentence it produces is formatted by notSupported in build/utils.go from the feature name and the driver name. Those feature names are constants in driver/features.go, so the first words of the sentence are a fixed vocabulary rather than free text.
| Feature name in the message | Asked for by | Available on the docker driver |
|---|---|---|
Cache export | A cache-to that is not type=inline | No |
Multi-platform build | More than one entry in platforms | No |
OCI exporter | An outputs entry of type=oci | No |
Docker exporter | An outputs entry writing a docker tarball | No |
The one message the action writes about buildx itself
There is exactly one, and it fires on a condition most people are not in. src/main.ts calls toolkit.buildx.isAvailable(), which runs the buildx command with no arguments and checks the exit code, and on failure calls core.setFailed with a sentence naming the setup action. That is about buildx not being installed at all, not about a setup step being absent from the workflow.
On GitHub-hosted runners buildx ships with the Docker installation, so this message is rare there and common on minimal self-hosted images. Seeing it means the binary is missing; not seeing it tells you nothing about whether a builder was set up.
Docker buildx is required. See https://github.com/docker/setup-buildx-action to set up buildx.Confirming which builder the build actually used
The action prints this for you and it is the fastest way to end the argument. Before it builds anything it opens a group called Builder info and dumps the inspected builder as JSON, driver field included. A driver of docker means no buildx builder was created for this job; docker-container means one was.
That group is also the place to check when a workflow does have a setup step but the build still behaves as though it does not, which happens when the build step names a builder input that does not match, or when the setup step was skipped by an if.
Why there is no recorded run on this page
The useful content here is a boundary between two error wrappers and a list of features one driver does not have, and both are properties of published source rather than of any particular run. A reproduction would show our repository being told that the docker driver cannot export cache, which is a sentence assembled from two constants and a feature name, and recording it would add a repository name and a timestamp to something that is true without either. The two third-party reports quoted here hit it on their own workflows, which is the evidence that matters.
How to prevent it
- Add the setup step in the same commit as the cache or platform option that needs it.
- Read the
Builder infogroup in code review when a Docker workflow changes drivers or builders. - Keep the prefix in bug reports, because
failed to buildandfailed to solvesend readers in opposite directions. - On self-hosted images, check that buildx is installed rather than assuming the Docker package brought it.
Frequently asked questions
Does docker/build-push-action require docker/setup-buildx-action?
docker driver, and ordinary single-platform builds and pushes work normally.What is the difference between "failed to build" and "failed to solve"?
failed to solve is added by the BuildKit client around the solve request, so the build was reached. failed to build is added by the buildx command around the whole operation and carries failures decided before any solve, such as a driver that cannot perform a requested export.Why does my build fail only after I added cache-to?
docker driver does not support cache export for anything other than type=inline, and the check runs before the build. The message names the feature, Cache export, and the driver. Adding a setup step gives you a docker-container builder that supports it.How do I tell which builder a build actually used?
Builder info group that the action prints before building. It dumps the inspected builder as JSON, and the driver field is the answer: docker is the ambient default, docker-container is one created by a setup step.Related guides
References
- docker/build-push-action: README, stating that setup-buildx is recommended rather than required
- docker/build-push-action: src/main.ts, the buildx availability check and the Builder info group
- docker/buildx: build/utils.go, the notSupported message assembled from a feature and a driver
- moby/buildkit: client/solve.go, where the failed to solve wrapper is added
- CivicDataLab/DataSpaceBackend#119: the driver failure quoted here, traced to a missing setup step
- Docker documentation
- Docker build cache
- GitHub Actions documentation