You’re in the middle of a Docker multi-stage build. Everything looks right — the Dockerfile is clean, the stage names are correct — and then you get this:
COPY failed: file not found in build context or excluded by .dockerignore: stat app: file does not existOr worse, a cryptic reference error that points to a stage that clearly exists. You’ve double-checked the name. It’s there. So what’s going on?
The COPY --from directive is one of Docker’s most useful features for keeping final images lean, but it’s also one of the more common sources of confusing build failures. This post covers the exact causes, the patterns that trigger them, and how to fix each one.
What COPY –from Actually Does
In a multi-stage build, Docker processes each FROM instruction as a separate build stage. Each stage runs in isolation — it gets its own filesystem, its own working directory, and its own set of environment variables. Nothing from one stage automatically carries over to the next unless you explicitly copy it.
COPY --from= lets you copy files from a previous build stage (or from an external image) into the current stage. The basic syntax looks like this:
FROM node:20 AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM nginx:alpine AS runner
COPY --from=builder /app/dist /usr/share/nginx/html
The builder stage compiles the app. The runner stage copies only the compiled output, leaving behind node_modules and all the intermediate build artifacts. Final image stays slim. This pattern is the reason production Node.js images can be under 50MB while the build environment needs several hundred.
When this breaks, it’s almost always one of a few specific causes.
Cause 1: The Stage Name Doesn’t Match
This is the most common mistake. Docker stage names are case-sensitive. A stage named Builder is not the same as builder.
FROM node:20 AS Builder # capital B
FROM nginx:alpine
COPY --from=builder /app/dist . # lowercase b — this will fail
The error message you’ll see isn’t always obvious about the root cause. Docker might say the file isn’t found in the build context, or it might say the stage reference is invalid. Neither message directly tells you “the stage name doesn’t match.”
Fix: Make sure the name in AS exactly matches the name in --from=. Stick to lowercase and use hyphens if you need separators — build-deps, runtime, test-runner. Avoid camelCase or PascalCase in stage names entirely.
Cause 2: The Stage Was Skipped by Target Build
This one catches people when they use docker build --target. If you specify a target stage, Docker only builds up to and including that stage. Any stages that come after your target in the Dockerfile — or any stages that aren’t in the dependency chain of your target — get skipped entirely.
FROM golang:1.22 AS builder
RUN go build -o /app/server .
FROM alpine AS certs
RUN apk add --no-cache ca-certificates
FROM scratch AS runner
COPY --from=builder /app/server /server
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
ENTRYPOINT ["/server"]
If you run:
docker build --target builder .Only the builder stage runs. The certs stage is never executed. If you later reference certs somewhere expecting it to be cached, it won’t be there.
This matters most in CI pipelines where --target is used to build only the test stage or the build stage for inspection, but then a later step assumes all stages have been built.
Fix: Either don’t use --target for that build, or make sure the stage you’re copying from is upstream of your target in the dependency chain.
Cause 3: Incorrect File Path in the Source Stage
The path you reference in COPY --from= must exist in that stage at the time of the copy. If the source file wasn’t created where you think it was, or if it was created in a different working directory, the copy will fail.
FROM node:20 AS builder
WORKDIR /app
RUN npm run build
Output is in /app/dist
FROM nginx:alpine
COPY --from=builder /dist . # wrong — should be /app/dist
This is especially common when the build tool has its own output directory config and it doesn’t match your assumed path. Next.js defaults to .next. Vite defaults to dist. Go puts binaries wherever you tell it to in the build command.
Fix: Add a RUN ls or RUN find /app -type f step in the source stage to verify the actual output paths before referencing them in a COPY --from.
Remove that debug line once you’ve confirmed the path. It’s also worth running the source stage interactively:
docker build --target builder -t debug-build .
docker run --rm debug-build find /app -type f
Cause 4: BuildKit and Legacy Builder Differences
If you’re on Docker Engine 23+ or Docker Desktop, BuildKit is the default builder. On older versions or in certain CI environments, the legacy builder may still be active.
BuildKit handles multi-stage dependency resolution differently. With BuildKit, stages that aren’t needed for the final target are pruned automatically. If you have a stage that’s conditionally used, or if your COPY --from references a stage that BuildKit determines is unreachable in the current build graph, it may skip that stage entirely.
You can verify which builder you’re using:
docker info | grep -i "builder"To explicitly enable BuildKit:
DOCKER_BUILDKIT=1 docker build .Or set it permanently in Docker’s daemon config:
{
"features": {
"buildkit": true
}
}
If you’re on a CI system like GitHub Actions and seeing inconsistent behavior between local and CI builds, check whether DOCKER_BUILDKIT is set in the environment. A Dockerfile that works locally with BuildKit may fail in a CI environment running the legacy builder, or vice versa, because the two handle stage pruning and caching differently.
Cause 5: COPY –from an External Image
COPY --from also works with external images, not just named stages:
COPY --from=golang:1.22 /usr/local/go /usr/local/goThis pulls the specified image and copies from it. If the image doesn’t exist locally and can’t be pulled — due to a network issue, a wrong tag, or a private registry that requires authentication — the build fails. The error message in this case usually makes the cause clear, but it’s worth knowing that --from isn’t limited to named stages. You can pull any layer from any accessible image this way.
Debugging Checklist
When a COPY --from fails, run through this in order:
- Check the stage name — exact match, case-sensitive
- Check the
--targetflag — is the referenced stage in the dependency chain? - Verify the source path — build the source stage separately and inspect it
- Check BuildKit behavior — especially on older Docker versions or non-standard CI environments
- Check external image availability — if
--fromreferences an image tag, confirm it exists and is accessible
Most failures fall into the first three categories. The error messages Docker gives for COPY --from failures are often misleading — they say the file wasn’t found in the build context, which is technically true but hides the real cause: the stage reference itself failed before Docker even tried to locate the file.
A Working Example
Here’s a full Dockerfile that avoids each of the pitfalls above:
# syntax=docker/dockerfile:1
FROM node:20-slim AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-slim AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM node:20-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
EXPOSE 3000
CMD ["node", "server.js"]
Key things this example does right:
- Stage names are lowercase and match exactly in every
COPY --fromreference - Each
COPY --fromreferences the correct path, which you’d verify by inspecting thedepsandbuilderstages independently - The
runnerstage has explicit dependencies on bothdepsandbuilder, so BuildKit will not prune either one - No
--targetflag confusion — building the finalrunnertarget causes both upstream stages to run automatically
Summary
The COPY --from not found error in Docker multi-stage builds usually comes down to one of four things: a case mismatch in stage names, a stage being skipped by --target, a wrong source path, or a BuildKit behavior difference. Run through the checklist, inspect the source stage independently if needed, and the fix is usually one line.
The debugging trick of adding RUN find /app -type f in your source stage before the copy has saved time on more than one incident. Build caching means it adds almost nothing to repeated builds, and it makes the actual output paths explicit rather than assumed.
