# docker/build-push-action failed to solve, and what a missing buildx really prints

> docker/build-push-action failed to solve is a wrapper around a BuildKit error. A missing setup-buildx step prints something else entirely.

Source: https://latchkey.dev/learn/github-actions/docker-build-push-action-buildx-not-set-up  
Updated: 2026-09-21

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.

```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.
```

## 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
```

## 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.

## 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 |

> Both can appear in one run when a build is retried or when several targets are built, so match the prefix to the line you are actually reading rather than to the first one in the step.

## 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 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/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.

## FAQ

### 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.

## References

- [docker/build-push-action: README, stating that setup-buildx is recommended rather than required](https://github.com/docker/build-push-action)
- [docker/build-push-action: src/main.ts, the buildx availability check and the Builder info group](https://github.com/docker/build-push-action/blob/master/src/main.ts)
- [docker/buildx: build/utils.go, the notSupported message assembled from a feature and a driver](https://github.com/docker/buildx/blob/master/build/utils.go)
- [moby/buildkit: client/solve.go, where the failed to solve wrapper is added](https://github.com/moby/buildkit/blob/master/client/solve.go)
- [CivicDataLab/DataSpaceBackend#119: the driver failure quoted here, traced to a missing setup step](https://github.com/CivicDataLab/DataSpaceBackend/issues/119)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
