# Docker: the --mount option requires BuildKit

> Fix the --mount option requires buildkit in GitHub Actions: the legacy builder refuses cache and secret mounts, so make the builder explicit.

Source: https://latchkey.dev/learn/docker/docker-mount-requires-buildkit  
Updated: 2026-09-20

The --mount option requires BuildKit is the legacy builder refusing a Dockerfile written for BuildKit, and it is a statement about which builder ran, not about your syntax. Our recorded run built that exact Dockerfile through BuildKit seconds earlier on the same runner, and the legacy builder refused it on the next line.

## What this error means

A build fails on the first `RUN --mount=` line and nothing before it looks wrong: the base image is pulled, the first step runs, and then the build stops with a one-line refusal and a link to the BuildKit documentation. The same Dockerfile builds on a colleague's machine and in another repository, which is the sign that the difference is environmental. In our recorded run the legacy builder announced itself first, printing a deprecation notice that names the environment variable holding BuildKit off, so the log answers the question before the failure arrives.

```Actions log, legacy builder on Docker 29.7.2
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
            BuildKit is currently disabled; enable it by removing the DOCKER_BUILDKIT=0
            environment-variable.

Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM alpine:3
 ---> 294b683cb724
Step 2/2 : RUN --mount=type=cache,target=/mnt/cachedemo     echo "written by the build" >/mnt/cachedemo/stamp && ls -l /mnt/cachedemo
the --mount option requires BuildKit. Refer to https://docs.docker.com/go/buildkit/ to learn how to build images with BuildKit enabled
```

## Common causes

### Something set DOCKER_BUILDKIT=0

The direct cause, and it is usually old. The variable is set in a job-level env block, in a shared shell profile, or in a script that worked around a BuildKit bug years ago. Our recorded run uses exactly this to produce the error on a current Docker, so the presence of a modern daemon proves nothing.

### The build goes through a tool that calls the classic API

Some wrappers, older plugins and language-specific build helpers call the daemon build endpoint directly rather than going through buildx. The host is capable of BuildKit and the build still runs classic, which is the version of this that survives every attempt to set the variable to one.

### The Docker on this runner predates BuildKit by default

On an old self-hosted runner image, or a pinned Docker in a custom image, the classic builder is simply the default. The same Dockerfile then fails on the runner and works everywhere else, and no workflow change fixes it until the image is updated or buildx is installed.

### The build targets Windows containers

The BuildKit documentation notes that the legacy builder is used instead when building Windows containers. That is a documented exception rather than a misconfiguration, so a Dockerfile with cache mounts needs a different approach on that platform, not a different variable.

## How to fix it

### Find what turns BuildKit off

1. Grep the workflow, the scripts it calls and any Makefile for the variable.
2. Check job-level and step-level env blocks, which override a shell export.
3. Remove the setting rather than overriding it later in the job.

```Terminal
grep -rn "DOCKER_BUILDKIT" .github/ scripts/ Makefile 2>/dev/null
```

### Set up a builder for the job

The action-based path removes the ambiguity entirely: the job gets its own builder and the build step uses it, so no shell variable can send the build elsewhere. This is also the path that gives you cache exports, which is usually the reason the mount is in the Dockerfile.

```.github/workflows/ci.yml
- uses: docker/setup-buildx-action@v4
- uses: docker/build-push-action@v7
  with:
    context: .
    push: false
```

### Call buildx directly in a run step

When the build has to stay a shell command, name the builder in the command. It is the same invocation with one word added, and it does not depend on an environment variable surviving whatever the job does next.

```Terminal
# not this
docker build -t app .
# this
docker buildx build -t app --load .
```

### Pin the frontend with a syntax directive

A syntax directive at the top of the Dockerfile pins the frontend that parses it, which keeps newer mount options working across older BuildKit versions. It does not enable BuildKit, so it is a complement to the fixes above rather than an alternative to them.

```Dockerfile
# syntax=docker/dockerfile:1
FROM alpine:3
RUN --mount=type=cache,target=/var/cache/apk apk add --no-cache curl
```

## How to prevent it

- Never set DOCKER_BUILDKIT=0 as a workaround; fix the thing it was working around.
- Create the builder in the job with the setup action, so no step can change it.
- Print the Docker version and the builder once per job that builds an image.
- Keep a syntax directive at the top of any Dockerfile using mount options.

## What decides which builder runs

Mounts are a BuildKit feature. The Dockerfile reference we read on 2026-09-20 describes `RUN --mount` as creating filesystem mounts the build can access, and the cache, secret, ssh and bind types are all part of that frontend, with nothing equivalent in the classic builder. So the refusal is accurate: the builder that read your file cannot do what the line asks.

Which builder read it is decided outside the Dockerfile. On our recorded run the runner had Docker 29.7.2 and a working BuildKit, and a single environment variable was enough to send the same file to the other builder.

| What selects the builder | Result | Where it usually comes from |
| --- | --- | --- |
| `DOCKER_BUILDKIT=0` | Legacy builder, mounts refused | A job-level env block, or a script nobody has reread |
| `DOCKER_BUILDKIT=1` or the default | BuildKit, mounts work | Current Docker with the buildx plugin present |
| `docker buildx build` | BuildKit, explicitly | The buildx plugin or the setup action |
| A Windows container build | Legacy builder | The documented exception, not a setting |

> Rows one to three are what our run on a `latchkey-small` runner recorded on 2026-09-20. The Windows exception is quoted from the BuildKit documentation, read the same day.

## The legacy builder is still there, and says so

It is worth knowing that this error is still reachable on a current Docker. Our run was on Docker 29.7.2 with buildx v0.36.1 installed, and setting the variable to zero produced a full classic build: the context upload line, numbered steps, and the refusal on the mount. The builder also printed its own deprecation notice, which states that the legacy builder is deprecated and will be removed in a future release and that BuildKit is currently disabled.

That notice is the fastest diagnosis on the page. If your log has it, stop looking at the Dockerfile: something in the environment turned BuildKit off, and the error is downstream of that.

```Terminal
grep -rn "DOCKER_BUILDKIT" .github/ scripts/ Makefile 2>/dev/null
# and in the job, before the build:
docker version --format '{{.Server.Version}}'
```

## Make the builder explicit in the workflow

The durable fix in Actions is to stop inheriting a builder. `docker/setup-buildx-action@v4` creates a builder for the job and `docker/build-push-action@v7` uses it, so no environment variable in a shell step can change what builds your image. It also gives you the cache backends, which is usually why the Dockerfile has a cache mount in the first place.

If you are calling `docker build` directly in a run step, call `docker buildx build` instead. It is the same flags for everything on this page, and it removes the ambiguity rather than papering over it with an environment variable that the next script can unset.

```.github/workflows/ci.yml
- uses: docker/setup-buildx-action@v4
- uses: docker/build-push-action@v7
  with:
    context: .
    cache-from: type=gha
    cache-to: type=gha,mode=max
```

## What the runner does about it

No repair, and the recorded run shows why one would be wrong. The wrapper posted the failure to the sidecar and the sidecar answered; nothing was changed. A runner that quietly re-ran your build under a different builder would be changing the thing you asked for, and cache mounts are not the only difference between the two builders: output formats, secrets handling and caching all differ.

## FAQ

### How do I enable BuildKit in GitHub Actions?

Add `docker/setup-buildx-action@v4` before your build step and build with `docker/build-push-action@v7`, or call `docker buildx build` in a run step. Both make the builder explicit for that job. Setting `DOCKER_BUILDKIT=1` also works but is fragile, because any later step or script can set it back.

### What does DOCKER_BUILDKIT=0 do?

It sends `docker build` to the classic builder instead of BuildKit. On our recorded run, on Docker 29.7.2, that produced a classic build with a context upload and numbered steps, and the builder refused the cache mount. The same Dockerfile had built through BuildKit on the same runner moments earlier.

### Is the legacy builder still available in current Docker?

It was on the runner we recorded on 2026-09-20, running Docker 29.7.2. The builder printed its own notice saying the legacy builder is deprecated and will be removed in a future release, and that BuildKit is currently disabled, so it is reachable today and should not be something you rely on.

### Do I need setup-buildx-action to use RUN --mount?

Not strictly, since a current Docker with the buildx plugin already defaults to BuildKit. You need it when you want the choice to be explicit and unaffected by the environment, and when you want the cache backends the action configures. On a runner image you do not control, explicit is worth the extra step.

## References

- [Docker docs: Dockerfile reference, RUN --mount](https://docs.docker.com/reference/dockerfile/)
- [Docker docs: BuildKit, the default builder and its exceptions](https://docs.docker.com/build/buildkit/)
- [Docker docs: optimizing builds with cache mounts](https://docs.docker.com/build/cache/optimize/)
- [backstage/backstage#16536: the same refusal inside a CI job](https://github.com/backstage/backstage/issues/16536)

---

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
