# Docker invalid mount config in CI

> Docker invalid mount config comes from the daemon validating a container mount. BuildKit cache mounts use different wording, shown here.

Source: https://latchkey.dev/learn/docker/docker-invalid-mount-config-cache-id-in-ci  
Updated: 2026-09-21

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.

```Reconstructed from the formatting in the moby daemon mount validator, whose own test fixtures the first line is quoted from; the three BuildKit lines below are captured locally on Docker 29.6.2 and buildx v0.35.0-desktop.2, 2026-09-21
--- 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?)
```

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

1. Find the command that failed. A build, or a run.
2. If it was a run, take the path or type the daemon quoted and check it on the host in the same step.
3. If it was a build, the message will name a token from your mount. Fix that token; there is nothing else to investigate.

```.github/workflows/ci.yml
- run: ls -la ./config || echo "bind source is not here"
- run: docker compose -f compose.yaml up -d
```

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

```Dockerfile
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
    npm ci --prefer-offline
```

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

```compose.yaml
services:
  api:
    volumes:
      - ${GITHUB_WORKSPACE}/config:/etc/api:ro
```

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

```Terminal
docker compose -f compose.yaml -f compose.ci.yaml config | grep -A4 volumes:
```

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

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

> Read in moby/moby at commit 72752cf (daemon/volume/mounts/validate.go and daemon/errors.go) and moby/buildkit at commit 99bd9de on 2026-09-21, with the four build cases run locally.

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

> All four built on this machine on 2026-09-21, Docker 29.6.2 with buildx v0.35.0-desktop.2, on a two line Dockerfile.

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

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

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

## FAQ

### Does BuildKit ever say invalid mount config?

Not for a Dockerfile mount. We broke a cache mount four ways and got messages about an unexpected key, an invalid mount target, an unsupported sharing value and an unsupported mount type. The invalid mount config phrasing belongs to the daemon mount parser, which validates container mounts.

### Can two cache mounts with the same id conflict?

No. The id is the lookup key for the cache directory, and two mounts that share one share the directory, which is usually the intent. Declaring different sharing modes on the same id changes only whether the builder locks or copies, and does not produce an error.

### What is the default cache id if I do not set one?

The cleaned target path. That means two mounts with the same target and no id are the same cache automatically, and it is why most Dockerfiles never need to set an id at all. Set one when two different targets should share storage.

### Why does my cache mount complain about a target of slash?

Because you left the target out. The target is resolved by joining the working directory with the value you gave, and an absent value leaves the root. BuildKit refuses a mount at the root and quotes it, so the message names a path you never typed.

## References

- [moby: the mount validator that formats invalid mount config](https://github.com/moby/moby/blob/master/daemon/volume/mounts/validate.go)
- [BuildKit: cache mount flag parsing and its error set](https://github.com/moby/buildkit/blob/master/frontend/dockerfile/instructions/commands_runmount.go)
- [BuildKit: where an omitted cache id defaults to the target path](https://github.com/moby/buildkit/blob/master/frontend/dockerfile/dockerfile2llb/convert_runmount.go)
- [Docker docs: RUN --mount=type=cache](https://docs.docker.com/reference/dockerfile/#run---mounttypecache)

---

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
