Skip to content
Latchkey LogoLatchkey home

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.

Two error wrappers at different depths, with the failures each one carries
The two wrappers are quoted from buildx commands/build.go and buildkit client/solve.go. The driver sentence is quoted from CivicDataLab/DataSpaceBackend#119.

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.

Actions log, quoted from CivicDataLab/DataSpaceBackend#119
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.

PrefixWrapped whereKinds of failure it carries
failed to solve:BuildKit client, around the solve requestA failing RUN, an unreadable context, an unresolvable base image
failed to build:The buildx command, around the whole buildA 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

  1. Take the prefix: failed to solve or failed to build.
  2. On failed to solve, ignore the driver entirely and read the build failure that follows.
  3. 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.

.github/workflows/docker.yml (illustrative)
      - 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=max

Check the Builder info group before changing the workflow

  1. Expand Builder info in the failing step.
  2. Read the driver field: docker means the ambient default, docker-container means a buildx builder.
  3. If it says docker and 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.

.github/workflows/docker.yml (illustrative)
      - uses: docker/build-push-action@v7
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:latest
          cache-to: type=inline

What 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 messageAsked for byAvailable on the docker driver
Cache exportA cache-to that is not type=inlineNo
Multi-platform buildMore than one entry in platformsNo
OCI exporterAn outputs entry of type=ociNo
Docker exporterAn outputs entry writing a docker tarballNo

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/build-push-action, src/main.ts
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 info group in code review when a Docker workflow changes drivers or builders.
  • Keep the prefix in bug reports, because failed to build and failed to solve send 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?
No. Its README says the setup action is not required but recommended, for multi-platform builds and cache export among other things. Without it the build runs on the ambient default builder, which uses the docker driver, and ordinary single-platform builds and pushes work normally.
What is the difference between "failed to build" and "failed to solve"?
They are wrappers at different depths. 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?
Because the 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?
Expand the 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

Reaching for cache-to means you wanted a layer cache. Latchkey keeps one on the runner between jobs. Start free → 30-day trial · No credit card