# Docker target stage could not be found in CI

> Docker target stage could not be found ignores case in the lookup but not in the hint, so a near miss gets a suggestion and a far one does not.

Source: https://latchkey.dev/learn/docker/docker-target-stage-not-found  
Updated: 2026-09-21

Docker target stage could not be found means the name you passed to the target flag matches none of the stages in the file being built, and the lookup that decided that ignores case. Whether you also get a suggestion depends on how far your spelling is from a real stage name, which is why two very similar mistakes produce messages of different lengths.

## What this error means

A build that works without the target flag fails instantly with it, naming the value you passed in quotes. Sometimes a parenthetical names a real stage and sometimes it does not, with no obvious pattern. Nothing is pulled and no step runs, because the failure happens while BuildKit is resolving which stage you asked for. Both blocks below were captured on this machine.

```Captured locally on Docker 29.6.2 and buildx v0.35.0-desktop.2, 2026-09-21
--- --target prod against a Dockerfile whose stage is named production
ERROR: failed to build: failed to solve: target stage "prod" could not be found
--- --target productio against the same file
ERROR: failed to build: failed to solve: target stage "productio" could not be found (did you mean production?)
```

## Common causes

### The workflow and the Dockerfile use different words for the same stage

The dominant cause in CI, and the one that arrives without a suggestion because abbreviations are far from full words. A matrix leg, a variable or a script says one thing and the Dockerfile says another, and nothing checks that they agree until the build runs.

### The stage was renamed and one caller was missed

A rename that updated the Dockerfile and not the workflow, or the other way round. This one usually does get a suggestion, because a rename tends to be a small edit, so if your message ends with a parenthetical naming a real stage this is very likely what happened.

### You are building a different Dockerfile than you think

A file flag pointing at the wrong path, or a build context that resolves the default Dockerfile somewhere unexpected, gives you a file whose stages are genuinely different. The message is then correct and unhelpful. Printing the stage names of the file actually being built settles it.

### The stage is created by a build argument and does not exist unconditionally

Patterns that switch a base image or a stage name through an argument can leave a target that exists only for some argument values. The build then works in one matrix leg and not another, with a message that names a stage which does exist somewhere in the file.

## How to fix it

### Print the stages of the file you are actually building

1. Use the same file and context the failing command used, not the ones you assume.
2. List the stage names and compare them to the target value literally.
3. If the list is empty or unexpected, your file or context flag is the problem rather than the name.

```Terminal
grep -nE '^\s*FROM .+ [Aa][Ss] ' ./services/api/Dockerfile
```

### Keep the stage name in one place

The failure is a disagreement between two files, so the fix is to stop having two. Define the name once in the workflow and pass it to both the build and anything else that needs it, so a rename is a single edit.

```.github/workflows/build.yml
jobs:
  image:
    strategy:
      matrix:
        stage: [development, production]
    steps:
      - uses: docker/build-push-action@v7
        with:
          context: .
          target: ${{ matrix.stage }}
          tags: acme/api:${{ matrix.stage }}
```

### Read the presence or absence of the hint as a signal

A suggestion naming a real stage means you have a typo and the fix is two characters. No suggestion means your value is far from every stage name, which points at two places using different vocabulary rather than at a spelling mistake. Treating those as the same problem is what makes this take an hour.

### Name stages after what they produce, and stop abbreviating

Short names are the ones that lose the hint, so the cheapest structural fix is to avoid them. Full words also make the grep above reliable and make a workflow that references them readable without the Dockerfile open beside it.

```Dockerfile
FROM node:22 AS dependencies
FROM dependencies AS build
FROM nginx:1.29 AS production
```

## How to prevent it

- Define each stage name once and pass it into the build rather than typing it twice.
- Use full words for stage names so the suggestion helper can still reach them.
- Pin the file and context inputs on the build action so the stages you list are the ones being built.
- Grep for the old name after any stage rename, including in workflow files.

## Two different rules, applied one after the other

BuildKit keeps its stages in a map keyed by the lowercased name, and looks yours up the same way. So case cannot be the reason your target was not found: `--target PRODUCTION` finds a stage declared `AS production`, and always has. If you have been changing case to fix this, you have been changing something that was never involved.

When the lookup misses, the error goes through a suggestion helper that measures edit distance against the list of stage names, and that comparison is case sensitive. It appends a hint only when something is fewer than three edits away. Those two rules disagree about case on purpose, and the consequence is that a mixed case near miss can resolve fine while a mixed case typo gets no hint.

| Value passed to `--target` | Stage declared in the file | What we got |
| --- | --- | --- |
| `production` | `production` | Built |
| `productio` | `production` | Not found, with a suggestion naming production |
| `prod` | `production` | Not found, with no suggestion |

> The first two rows and the third were run on this machine on 2026-09-21. The distance rule is read in the suggestion helper in moby/buildkit at commit 99bd9de.

## Why short names lose the hint exactly when you want it most

Abbreviations are the common mistake in CI, because a workflow variable says prod and a Dockerfile says production, or a matrix leg is called dev and the stage is called development. Those are five and eight edits apart, which is far outside the cutoff, so the message gives no hint at all.

So the absence of a suggestion carries real information, and it is the opposite of what people assume. It does not mean the file has no stages or that BuildKit is confused. It means your value is nowhere near any stage name, which usually means two places in your repository disagree about vocabulary rather than about spelling.

```Dockerfile
# the pairing that produces a hintless failure
# workflow:  --target prod
# Dockerfile: FROM nginx:1.29 AS production

# make the two agree, and keep the name in one place
FROM nginx:1.29 AS production
```

## The target flag and COPY --from do not fail the same way

They use the same lookup, but they do different things when it misses. The target flag has nowhere else to go, so it reports the stage name and stops, which is the message on this page. A `COPY --from` with an unknown name is treated as an image reference and sent to a registry, so it fails much later with an authorization error naming a repository.

That is worth knowing when a build has both. A workflow that sets a target and a Dockerfile that copies between stages can fail in either of two completely different looking ways from the same underlying rename, and only one of them mentions stages.

## Why no recorded run backs this page

This failure is decided from one flag and one file, before anything is fetched. We ran all three cases in the table in a few seconds on a laptop, and a Latchkey runner would produce the same three lines at the cost of three jobs.

The claim that needed care is the asymmetry between the lookup and the hint, and that came from reading the two functions and then confirming both halves by running them. A recorded run of one of the three would have shown a single message and left the reader to guess which rule produced it, which is the opposite of what this page is for.

## FAQ

### Is the docker build target flag case sensitive?

No. BuildKit stores stages under their lowercased names and lowercases your value before looking it up, so a difference in case has never caused this error. The suggestion that sometimes follows the error is case sensitive, which is the only place case matters.

### Why do I sometimes get did you mean and sometimes not?

The hint is a Levenshtein comparison against the stage names with a cutoff of three edits. We ran both sides of it: productio against production is one edit and gets the hint, prod against production is far more and gets nothing. Absence of a hint means your value is nowhere near a real stage name.

### Does COPY --from give me the same error for an unknown stage?

No, and that catches people out. An unknown name in COPY --from is treated as an image reference and sent to a registry, so it fails later with an authorization error for a repository named after your typo. Only the target flag produces a message about stages.

### Why does the target work locally and not in CI?

Almost always because the two are building different files or passing different values. Check the file and context inputs on the build action, then print the stage names of that exact file. A stage created conditionally by a build argument is the other common version of this.

## References

- [BuildKit: the target resolution that reports this error](https://github.com/moby/buildkit/blob/master/frontend/dockerfile/dockerfile2llb/convert.go)
- [BuildKit: the suggestion helper and its three edit cutoff](https://github.com/moby/buildkit/blob/master/util/suggest/error.go)
- [Docker docs: multi-stage builds and stopping at a stage](https://docs.docker.com/build/building/multi-stage/)
- [Docker docs: docker buildx build and the target flag](https://docs.docker.com/reference/cli/docker/buildx/build/#target)

---

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
