# Docker COPY --from stage not found in CI

> A docker COPY --from stage not found is not an error at all: BuildKit treats the unknown name as an image and you get a registry denial instead.

Source: https://latchkey.dev/learn/docker/docker-copy-from-stage-name-does-not-exist  
Updated: 2026-09-21

A docker COPY --from stage not found does not produce a message about stages, because BuildKit does not treat an unknown name as a mistake. It treats it as an image reference and goes to a registry for it, so the line in your log is an authorization failure for a repository you have never heard of. A numeric index behaves completely differently and does say what is wrong.

## What this error means

A multi stage build fails on a COPY line with a registry denial naming a repository that looks like your stage name, usually under docker.io/library. Credentials are fine, the registry is fine, and adding a login step changes nothing. The only hint that this is a Dockerfile problem is a parenthetical suggestion at the end, which appears only for small typos. 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
--- COPY --from=buider, where the stage is named builder
ERROR: failed to build: failed to solve: buider: failed to resolve source metadata for docker.io/library/buider:latest: pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed (did you mean builder?)
--- COPY --from=7 in a file with two stages
ERROR: failed to build: failed to solve: invalid stage index 7
```

## Common causes

### The stage name is misspelled or was renamed elsewhere

The dominant cause, and the one the registry message disguises. A rename that updated the `FROM ... AS` line and missed one `COPY --from` leaves a build that fails at the registry. In our experience this survives review more often than a fresh typo, because the diff looks like a rename and the missed caller is somewhere else in the file.

### The stage exists in a different Dockerfile

A team that splits one Dockerfile into several, or that copies a stanza between services, ends up with a COPY naming a stage that is real somewhere else. Nothing in the message hints at this, because BuildKit has no notion of other Dockerfiles. The name simply is not among the stages of the file being built.

### A numeric index that no longer points where it did

Indexes count from zero over the stages in the file, so inserting a stage anywhere above a `COPY --from=2` silently repoints it, and deleting one puts it out of range. Out of range at least fails loudly with "invalid stage index". Repointed does not fail at all; it copies from the wrong stage and you find out later.

### You meant an image and the reference is wrong

Since naming an image is a legitimate use of `--from`, some of these messages are exactly what they say: a real pull failure for a real reference you got wrong, or one that needs credentials this job does not have. The suggestion at the end is the discriminator. If it names one of your stages, you have a typo; if there is no suggestion, check whether you meant an image.

## How to fix it

### Read the last words of the line first

1. If the message ends with a suggestion naming one of your stages, fix the spelling and stop reading the registry text.
2. If there is no suggestion, compare the name against your `FROM ... AS` lines by hand, because the typo may be too large to suggest.
3. Only then treat it as a real pull problem and look at credentials.

```Terminal
grep -nE '^\s*FROM .* [Aa][Ss] |^\s*COPY --from=' Dockerfile
```

### Name every stage and never copy from an index

Indexes are the only form of this reference that can silently point somewhere else after an unrelated edit. Names cannot: a name either matches a stage or turns into an image reference that fails loudly. The cost of naming every stage is one word per FROM line.

```Dockerfile
FROM golang:1.25 AS build
WORKDIR /src
COPY . .
RUN go build -o /out/api ./cmd/api

FROM gcr.io/distroless/static AS runtime
COPY --from=build /out/api /api
```

### Make the stage names impossible to mistake for images

Since an unmatched name becomes an image reference, stage names that look nothing like image names turn a silent registry trip into an obvious mistake when you read the failing line. A prefix is enough, and it also makes the grep above trivially reliable.

```Dockerfile
FROM node:22 AS stage-deps
FROM stage-deps AS stage-build
FROM nginx:1.29 AS stage-runtime
COPY --from=stage-build /src/dist /usr/share/nginx/html
```

### Log in before the build when the job also pulls real images

This does not fix a typo, but it removes the ambiguity from the failure: with credentials present, a denial for a repository named after your stage is unmistakably a name problem rather than an authentication one. That is worth having in any job that pulls private base images anyway.

```.github/workflows/build.yml
- uses: docker/login-action@v3
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}
```

## How to prevent it

- Give every stage an explicit name and never reference one by index.
- Prefix stage names so an unmatched one cannot be mistaken for an image reference in a log.
- Grep for `COPY --from=` after any rename that touches a Dockerfile.
- Keep credentials present in jobs that build, so a registry denial is always about the name and not about auth.

## A name and a number take different paths

When BuildKit turns your COPY into a command it first tries to read the `--from` value as an integer. If that works it looks up the stage by position and returns "invalid stage index N" when the position does not exist. If it does not parse as an integer, it looks the name up among the stages, and when there is no match it does not stop. It builds a placeholder state whose base name is the string you wrote, marks it unregistered, and lets the normal image resolution machinery take it from there.

That design is deliberate, because `COPY --from=alpine:3.20 /etc/ssl /etc/ssl` is a supported and useful thing to write. The consequence is that a typo in a stage name is indistinguishable, at the point of the mistake, from naming an image on purpose. You find out when the registry declines to hand over a repository named after your typo.

| Value after `--from=` | How BuildKit resolves it | What the log says when it fails |
| --- | --- | --- |
| A name matching a stage | Copies from that stage | Nothing, this is the normal path |
| A name matching no stage | Registered as an image reference and pulled | A registry denial for that name, plus a suggestion if the typo is small |
| An integer within range | The stage at that position, counting from zero | Nothing, this is the normal path |
| An integer out of range | Rejected immediately | `invalid stage index 7` |
| A value containing a variable | Rejected before anything else | A sentence telling you to define an alias stage instead |

> Traced in moby/buildkit at commit 99bd9de on 2026-09-21, in frontend/dockerfile/dockerfile2llb/convert.go, and both failing cases were run locally.

## The parenthetical is the only thing that names your real problem

BuildKit wraps the image resolution failure with the original name and then runs it through the same suggestion helper used for unknown instructions, comparing against both the stage names in your file and a list of common image names. A match under three edits appends " (did you mean builder?)" to the end of a line that is otherwise entirely about a registry.

So the useful information is the last four words of a long message about authorization. Miss it and you will spend the afternoon on credentials. Write a stage name four or more edits away from the typo, or misspell it in a way that happens to be close to a real image name, and you will not get the hint at all.

## Case does not matter, and a forward reference fails only when the stage is built

Stage names are matched case insensitively. The lookup lowercases both the name you wrote and the names of the stages before comparing, so `COPY --from=Builder` finds a stage declared `AS builder`. The suggestion helper, on the other hand, is invoked case sensitively here, which is why a case difference resolves fine but a case difference combined with a typo can produce a message with no hint.

A stage declared later in the file is a different matter, and the folklore in both directions is wrong. The copy dispatcher has one guard: if the stage you named has not been dispatched yet, it returns "cannot copy from stage ..., it needs to be defined before current stage ...". Stages are dispatched in the order the file declares them, so a stage that copies from one below it does hit that guard, when it is dispatched at all. That is the qualifier that matters: with no `--target`, the build targets the last stage in the file and dispatches only the stages it depends on, so a forward reference sitting in a stage nothing depends on is never reached and never complained about. A green build is not evidence that the reference resolved; it may be evidence that the stage was skipped.

```Dockerfile
# the forward reference is only refused when this stage is actually built
FROM scratch AS first
COPY --from=later /x /        # nothing depends on "first", so nothing dispatches it
FROM scratch AS later

# and the case of the name does not matter
FROM golang:1.25 AS Builder
FROM scratch
COPY --from=builder /out/api /api
```

> Read in moby/buildkit at commit 99bd9de on 2026-09-21: the guard in frontend/dockerfile/dockerfile2llb/convert.go, the dispatch loop that marks a stage dispatched at the top of its own turn, and the reachability walk that starts from the last declared stage when no target is given.

## Why no recorded run backs this page

The interesting half of this failure is a registry refusing an anonymous pull, and a recorded Latchkey run would only prove that Docker Hub refuses anonymous pulls from us too. Worse, the exact denial text comes from the registry, so a run would pin this page to whatever Docker Hub was saying that afternoon rather than to anything about builds.

The claim that matters is that an unknown stage name is turned into an image reference, and that one lives in a single function we can point at and that we exercised locally in both directions, with both messages kept in the transcript. The case rule and the forward reference rule are read from that same function rather than measured, because what they turn on is which stages the build dispatches, and a passing build cannot tell you that. A runner would not have made any of those observations truer.

## FAQ

### Why does Docker try to pull my stage name from Docker Hub?

Because `COPY --from` accepts an image reference as well as a stage name, and BuildKit cannot tell a typo from a deliberate reference. A value that matches no stage is registered as an image and resolved normally, which sends it to the default registry under the library namespace.

### What does invalid stage index mean in a Docker build?

You used a number in `--from` and the number is outside the range of stages in the file. Stages count from zero, so a two stage Dockerfile has indexes 0 and 1 only. Unlike a bad name this fails immediately and says so, because a number cannot be an image reference.

### Does COPY --from care about the case of the stage name?

No. The lookup lowercases both sides before comparing, so a stage declared `AS Builder` is found by `--from=builder`. The suggestion helper that adds the "did you mean" hint is case sensitive, which is why a mixed case typo can resolve without a hint.

### Do I have to define a stage before the COPY that uses it?

Yes, for any stage the build actually dispatches. The copy dispatcher refuses a source stage that has not been dispatched yet, and stages are dispatched in declaration order. A forward reference can still pass unnoticed, because with no target the build only dispatches the last stage and what it depends on, so a stage nothing depends on is never reached. Reordering will not fix a name that matches no stage at all.

## References

- [BuildKit: where a COPY --from value becomes an index, a stage or an image](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: Dockerfile reference, COPY --from](https://docs.docker.com/reference/dockerfile/#copy---from)
- [Docker docs: multi-stage builds](https://docs.docker.com/build/building/multi-stage/)

---

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
