Docker invalid mount config in CI
Docker invalid mount config is the moby daemon refusing a mount you asked it to give a container, and it names the mount type it rejected. It is not a build error: a malformed RUN --mount=type=cache in a Dockerfile is refused by BuildKit in completely different words, which is why searching this phrase leads people to cache mount advice that does not apply.

What this error means
A step fails with a sentence containing the words invalid mount config, usually followed by a type in quotes and a reason. If the failing command was docker run, docker compose up, or a job service container, this is the daemon and the page below applies to you. If it was a build, the message you actually have is one of the BuildKit ones in the table, which say nothing about a mount config. The BuildKit block below is four builds on this machine.
--- what the daemon says about a container mount it will not accept
invalid mount config for type "bind": bind source path does not exist: /tmp/definitely-no-such-path
--- what BuildKit says about a broken cache mount in a Dockerfile, captured here
ERROR: failed to build: failed to solve: unexpected key 'targt' in 'targt=/x' (did you mean target?)
ERROR: failed to build: failed to solve: invalid mount target "/"
ERROR: failed to build: failed to solve: unsupported sharing value "lock" (did you mean locked?)Which program is validating your mount
The word mount covers two unrelated things in Docker. One is a mount you give a container at run time, validated by the daemon, which owns the phrase invalid mount config and formats it with the mount type in quotes followed by the specific reason. The other is a mount a Dockerfile asks for during a build step, validated by the BuildKit Dockerfile frontend, which has its own vocabulary and never uses that phrase.
We checked the build side by breaking a cache mount four different ways on this machine. None of the four produced anything resembling invalid mount config. If your message has that phrase, the command that produced it was not a build, and the fix is in a compose file, a run command or a service container definition.
| What you were doing | Program that validates it | Vocabulary it uses |
|---|---|---|
docker run -v or --mount | The moby daemon, its mount parser | invalid mount config for type "bind": ... |
A compose volumes: entry | The moby daemon, via the compose client | The same, reached through a different caller |
| A duplicate destination on a container | The moby daemon, its error set | Duplicate mount point: <path> |
RUN --mount=type=cache in a Dockerfile | BuildKit, the Dockerfile frontend | unexpected key, invalid mount target, unsupported mount type |
| A cache sharing value the solver rejects | BuildKit, its mount manager | invalid cache sharing option: <value> |
Common causes
You are reading a container mount error and looking at build advice
The dominant cause of arriving on a page like this one. The phrase appears in a compose run, a docker run, or a job service container, and the word mount sends the search toward cache mounts. Check which command failed before you change a Dockerfile.
A bind source that does not exist on the host
The commonest genuine daemon case, and in CI it is usually a relative path resolved against a working directory that is not what the compose file assumes. The message names the path, so it is quick to confirm with a listing in the same step.
A misspelled key or value in a cache mount
On the build side, this is what actually happens. A key typo, a sharing value typo and a mount type typo all fail at parse time and all three name the token and suggest the correct one when it is close enough. There is nothing to debug beyond reading the quoted token.
A cache mount with no target at all
Worth calling out separately because the message misdirects. Leaving out the target gives you a complaint about a mount at the root, which reads as though you asked for something absurd. You did not: you asked for nothing and the default filled in.
A mount destination declared twice on the same container
The daemon has a distinct error for this, which names the duplicated path rather than the mount type. It is easy to produce with compose file merging, where a base file and an override both define a volume for the same path in different forms.
How to fix it
Identify the command, then the vocabulary
- Find the command that failed. A build, or a run.
- If it was a run, take the path or type the daemon quoted and check it on the host in the same step.
- If it was a build, the message will name a token from your mount. Fix that token; there is nothing else to investigate.
- run: ls -la ./config || echo "bind source is not here"
- run: docker compose -f compose.yaml up -dWrite cache mounts with the target first and the id only when you need it
The target is required and everything else has a sensible default, so putting it first makes an omission obvious when you read the line. Add an id only when two mounts with different targets should share one cache, which is the only case where the default is not what you want.
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
npm ci --prefer-offlineMake bind sources absolute and prove they exist
A relative bind source is resolved against the directory the client was run from, which in CI is set by the job rather than by the compose file. Making them absolute, or asserting them in the step before, removes a whole class of daemon mount errors from a pipeline.
services:
api:
volumes:
- ${GITHUB_WORKSPACE}/config:/etc/api:roRender merged compose configuration to find duplicate destinations
Duplicate mount points usually come from file merging rather than from one file, so reading either file alone will not show them. Rendering the merged result is the only view where the duplicate is visible, and it costs nothing to run in CI.
docker compose -f compose.yaml -f compose.ci.yaml config | grep -A4 volumes:What a cache mount actually rejects, measured four ways
Every one of these fails at parse time, before any step runs, and every one of them names the exact token that was wrong. Three of the four carry a suggestion, because cache mount keys go through the same distance based helper as unknown instructions do.
The most confusing of the four is the one for a missing target. There is no key called target in the mount, so the target resolves to the working directory joined with an empty string, which comes out as the root. BuildKit then refuses a mount at the root and quotes it. A message about a slash you never typed is really a message about a key you left out.
| Mount we wrote | What BuildKit printed |
|---|---|
type=cache,targt=/x | unexpected key 'targt' in 'targt=/x' (did you mean target?) |
type=cache,id=npm with no target | invalid mount target "/" |
type=cache,target=/x,sharing=lock | unsupported sharing value "lock" (did you mean locked?) |
type=cahce,target=/x | unsupported mount type "cahce" (did you mean cache?) |
The cache id is not something you have to get right
A cache mount has an optional id, and when you leave it out BuildKit fills it in with the cleaned target path. Two mounts with the same target and no id are therefore the same cache, deliberately. That is the behavior most people want and it is why omitting the id is fine.
Two mounts that share an id but declare different sharing modes do not conflict, either. The sharing mode only changes whether the builder takes a lock or makes a private copy when it hands the directory over; the lookup key is the id. So a page telling you that a duplicate cache id causes an invalid mount config is describing something that does not happen, in words the build side does not use.
# these two are the same cache, because the id defaults to the target
RUN --mount=type=cache,target=/root/.cache/go-build go build ./...
RUN --mount=type=cache,target=/root/.cache/go-build go test ./...
# and this is how to give two caches the same storage deliberately
RUN --mount=type=cache,id=gomod,target=/go/pkg/mod,sharing=locked go mod downloadWhy no recorded run backs this page
Both halves of this page fail before anything interesting happens. The build side is a parse error, decided from one line of a Dockerfile in milliseconds, which is why four of them fit in one table. The daemon side is a validation error on a request, decided before a container exists.
A recorded run would also have to pick a side, and the point of the page is that there are two. What it needed instead was the four build cases run and captured so a reader can match their own message, and the daemon wording quoted from the place moby itself asserts it. Neither of those is improved by running on a hosted runner.
How to prevent it
- Keep bind sources absolute in any compose file a CI job will run.
- Render merged compose configuration in CI so duplicate destinations are visible before they fail.
- Put the target first in every cache mount so a missing one is obvious on sight.
- When you search a Docker error, note which command produced it before you trust the results.