Docker: the --mount option requires BuildKit
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.
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 enabledReproduced on a Latchkey runner
Docker version 29.7.2, build a7dcaa6
github.com/docker/buildx v0.36.1 1d8dde89b8aba914e05e45366770736fea1fd690
docker.io/library/alpine:3
--- the Dockerfile
FROM alpine:3
RUN --mount=type=cache,target=/mnt/cachedemo \
echo "written by the build" >/mnt/cachedemo/stamp && ls -l /mnt/cachedemo
--- control: the same file through BuildKit
#6 naming to docker.io/library/mount-demo-buildkit:latest done
#6 unpacking to docker.io/library/mount-demo-buildkit:latest 0.0s done
#6 DONE 0.2s
--- DOCKER_BUILDKIT=0 docker build, the legacy builder
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
[latchkey-bash-wrapper] BEGIN sidecar POST (boot_wait=30s max_time=320s url=http://localhost/diagnose socket=/run/latchkey-self-heal/sock)
[latchkey-bash-wrapper] END sidecar POST ok (attempts=1 http=200)The runner diagnosed the failure and did not retry it; this failure needs the fix below.
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 |
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
- Grep the workflow, the scripts it calls and any Makefile for the variable.
- Check job-level and step-level env blocks, which override a shell export.
- Remove the setting rather than overriding it later in the job.
grep -rn "DOCKER_BUILDKIT" .github/ scripts/ Makefile 2>/dev/nullSet 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.
- uses: docker/setup-buildx-action@v4
- uses: docker/build-push-action@v7
with:
context: .
push: falseCall 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.
# 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.
# syntax=docker/dockerfile:1
FROM alpine:3
RUN --mount=type=cache,target=/var/cache/apk apk add --no-cache curlThe 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.
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.
- uses: docker/setup-buildx-action@v4
- uses: docker/build-push-action@v7
with:
context: .
cache-from: type=gha
cache-to: type=gha,mode=maxWhat 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.
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.
Frequently asked questions
How do I enable BuildKit in GitHub Actions?
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?
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.