# Docker COPY failed: file not found in build context, in CI

> Fix docker copy failed: file not found in build context in GitHub Actions: the context, .dockerignore, and the BuildKit wording that names neither.

Source: https://latchkey.dev/learn/docker/docker-copy-failed-file-not-found-in-ci  
Updated: 2026-09-20

Docker copy failed: file not found in build context is the builder telling you the path in your COPY line is not among the files it was handed, which is a narrower claim than the file missing from your repository. BuildKit words the same failure as a cache key it could not compute, and our recorded run produced both messages from one missing directory on one runner.

## What this error means

A build fails on a COPY line, instantly, with no network activity and no layer downloaded. Which sentence you get depends on the builder, not on the mistake: the legacy builder names the path and mentions .dockerignore, while BuildKit reports a cache key it could not compute and quotes the path with a leading slash. The recorded run below ran both on one runner, then a third build where the directory existed and .dockerignore excluded it: BuildKit printed what it had printed for an absent one.

```Actions log, Docker 29.7.2 and buildx v0.36.1
--- legacy builder: DOCKER_BUILDKIT=0 docker build
COPY failed: file not found in build context or excluded by .dockerignore: stat dist/: file does not exist
--- BuildKit, same Dockerfile, same missing directory
ERROR: failed to build: failed to solve: failed to compute cache key: failed to calculate checksum of ref ryg7koya7jiszdax614bj27m4::z15xkmspcmh5p34fv5zhrflzy: "/dist": not found
```

## Common causes

### The artifact was never produced on this runner

The cause our own reproduction models, and the one swarmpit/swarmpit#651 reports: a pipeline building an image whose COPY wanted a jar nothing had built. A skipped step, a cache miss on a build output that a previous run had restored, a job that produced the artifact somewhere else, or a download-artifact step that runs after the build all leave the path absent when the context is packed. It is first here because it is the cheapest to rule out, not because we know it is the commonest.

### The default context is the Git reference, not the runner

The Actions-specific cause, and the one that survives ruling out the first, because the file is there and the build still cannot see it. With no `context:` input the build action uses the Git context, and its README states that any file mutation in the steps preceding the build is ignored. Nothing in the Dockerfile explains it.

### A .dockerignore pattern excludes the path, where the file is read at all

The documentation is explicit that matching files are removed from the context before it is sent to the builder, so an excluded file is absent rather than hidden. Our recorded run shows BuildKit reporting an excluded directory in the words it used for a missing one, and only its CopyIgnoredFile check named the file and the line. The action README also lists ignore-file processing among the things the Git context skips.

### The context root, the case, or a wildcard that matched nothing

A Dockerfile written for a service directory, run from the repository root, looks one level too high, and a path climbing out with `../` cannot be reached at all. Case is the quieter version, fine on a case-insensitive laptop and fatal on a Linux runner. So is a wildcard, which fails when it matches nothing and changes shape when it matches more than one file.

## How to fix it

### Print the context before you build it

1. List the directory the COPY names, in the same job, immediately before the build.
2. If it is absent, the fix is in the step that should have produced or downloaded it.
3. If it is present, the build was given a context that does not include it: the action default, the root, or .dockerignore.

```.github/workflows/ci.yml
ls -la dist || echo "dist is not here, check the build step"
docker buildx build -t app .
```

### Check out, then pass the context explicitly

A checkout step plus a `context:` input is the pair that puts the runner files in the build and keeps them there, which is the fix for the default above. Spell out the Dockerfile as well, so a move of the file or a change of working directory cannot quietly change what the builder receives.

```.github/workflows/ci.yml
- uses: actions/checkout@v7
- uses: docker/setup-buildx-action@v4
- uses: docker/build-push-action@v7
  with:
    context: ./services/api
    file: ./services/api/Dockerfile
    push: false
```

### Make .dockerignore say what it means, and check that it is read

An allowlist fails loudly, which is its point: anything you forget to admit is missing at the COPY rather than quietly present. Keep the negations beside the exclusion they undo. Then make sure the file runs at all, because the README puts ignore-file processing among the things the Git context skips: under that default neither exclusions nor negations apply, and the pair above is what restores them.

```.dockerignore
# .dockerignore
*
!package.json
!package-lock.json
!dist/
```

### Write source paths the way the runner reads them

Match the case exactly, because a Linux runner does even where your laptop does not. Then be deliberate about wildcards: the documentation says that with multiple source files, directly or by wildcard, the destination must be a directory ending in a slash, so a pattern that worked while it matched one file can fail when a second appears.

```Dockerfile
# says what it means, and fails loudly when a file is missing
COPY package.json package-lock.json ./
# a wildcard needs a directory destination once it matches more than one
COPY src/*.ts /app/src/
```

### Build the artifact in a stage instead of the context

The durable fix for a missing build output is to stop shipping it through the context. A build stage produces it inside the builder, where no skipped step, cache miss, ignore file or context default can lose it, and the final stage copies it by name.

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

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

## How to prevent it

- List the context path in the job, right before the build that needs it.
- Write .dockerignore as an allowlist so omissions fail at the COPY.
- Pin `context:` and `file:` in the build action rather than taking the defaults.
- Produce build outputs in a build stage wherever the toolchain allows it.

## Two builders, two sentences, one cause

The build context is the set of files the build can access, in the words of the Docker documentation read on 2026-09-20, and a COPY source is relative to the root of that context. The whole page is one question: why is your path not in that set.

The wording obscures it, and the BuildKit sentence sends people to look at caching: the cache key it could not compute is the checksum of the file it could not find. Read the table below against your log first.

| What your log says | Which builder | What it tells you |
| --- | --- | --- |
| "COPY failed: file not found in build context" | Legacy builder | The path, and that .dockerignore is a candidate |
| "failed to compute cache key ... not found" | BuildKit | The path with a leading slash, and nothing else |
| `WARN: CopyIgnoredFile` | BuildKit check | That .dockerignore is why, and the line number in it |
| A COPY that silently copies nothing | Either | A wildcard source that matched no files |

> The first three rows are quoted from our own run on a `latchkey-small` runner on 2026-09-20. The BuildKit check fired only on the third build, where the directory existed and was excluded.

## Read the BuildKit sentence right to left

The BuildKit wording is one sentence assembled by six programs, each wrapping the error beneath it, so it grows from the inside out and only the rightmost fragment names anything you wrote. That is also why searching the whole line returns nothing useful: it exists as a literal in no source file, because it is composed at runtime from six separate format strings.

The leading slash on the quoted path is the root of the build context rather than a filesystem root, and that has one consequence worth knowing. A source that climbs out with `../` is normalized against that root before the lookup, so it arrives here as an ordinary missing name and nothing anywhere says a path escaped the context. A local build on Docker 29.6.2, not a runner, printed `"/outside.txt": not found` for `COPY ../outside.txt`, which differs from a genuinely absent file only in the name. Drop the leading slash and you have the path to go and list.

```Dockerfile
# both of these fail with the same sentence shape
COPY nope.txt /          # nothing named nope.txt in the context
COPY ../outside.txt /    # climbs out, gets normalized to "/outside.txt"
```

| Fragment, left to right | The program that formats it |
| --- | --- |
| `ERROR:` | buildx, the progress printer that renders a failed vertex |
| `failed to build:` | buildx, the build command wrapping the client call |
| `failed to solve:` | The BuildKit Go client, in the solve call |
| `failed to compute cache key:` | The BuildKit solver, wrapping a slow cache function |
| `failed to calculate checksum of ref <id>:` | The BuildKit content hash helper, naming the ref it was hashing |
| `"/dist": not found` | BuildKit content hash, the only fragment naming your path |

> Traced in moby/buildkit at commit 99bd9de and docker/buildx on 2026-09-21, from solver/edge.go through solver/llbsolver/ops/opsutils/contenthash.go to cache/contenthash/checksum.go. The `../` line is captured locally on Docker 29.6.2 and buildx v0.35.0-desktop.2, 2026-09-21, not from the recorded run above.

## The context is not your repository

In a workflow the two drift apart constantly. A job that builds artifacts in one step and an image in the next has a `dist` directory only if the first step ran on this runner. That is the first thing to check, because one listing settles it.

The second drift is the one Actions adds, and it catches people whose files are demonstrably on the runner. With no `context:` input, `docker/build-push-action@v7` builds the Git context, which BuildKit fetches from the Git reference. Its README warns: "Be careful because any file mutation in the steps that precede the build step will be ignored, including processing of the `.dockerignore` file since the context is based on the Git reference." Checking out and naming a `context:` input switches to the path context and lifts that restriction, which is what both workflow samples on this page do.

```.github/workflows/ci.yml
- uses: actions/checkout@v7
- run: npm run build          # produces ./dist on this runner
- run: ls -R dist | head      # prove it before the build
- uses: docker/build-push-action@v7
  with:
    context: ./services/api
    file: ./services/api/Dockerfile
```

## What a multi-stage copy resolves against

A stage copy follows a different path rule, and it is the one people carry over wrongly. The documentation states that the source path of a `COPY --from` is always resolved from the filesystem root of the image or stage you name, so a path that was right in the context is usually wrong in a stage.

```Dockerfile
# the stage name has to match, and the path is absolute in that stage
FROM node:22 AS build
WORKDIR /src
RUN npm ci && npm run build

FROM nginx:1.27
COPY --from=build /src/dist /usr/share/nginx/html
```

## What the runner does about it

No repair, and none would be honest. On the recorded run the wrapper posted the failure to the sidecar and the sidecar answered, nothing was applied, and the build failed on the same missing path. A file nothing produced is not a condition a runner can fix underneath you, so what the recorded script does instead is list the context on the line before the build.

## FAQ

### What does failed to compute cache key not found mean?

It is BuildKit reporting what the older wording calls a missing file. To decide whether a COPY layer can be reused it checksums the source path, and with the path absent from the context there is nothing to checksum. Nothing about your cache is broken: the path in quotes is the whole message.

### Does .dockerignore cause COPY failed: file not found?

Yes, and in BuildKit it looks identical to a path that was never there. The documentation says matching files are removed from the context before it reaches the builder, and on our recorded run an excluded directory produced the same sentence as a missing one.

### Is COPY case sensitive in Docker?

On a Linux build it follows the filesystem, which is case sensitive, so `COPY Dist/` and `COPY dist/` are different paths. That is why a build passes on a case-insensitive laptop and fails on a hosted runner with no change to the Dockerfile.

### Why does docker build work locally and fail in GitHub Actions?

Because the context differs, and in Actions it may not be the runner at all: with no `context:` input the build action builds the Git reference, so files your steps wrote are ignored. After that, check the file was produced in this job, then case sensitivity, then the working directory.

### What is the long ref identifier in the middle of the message?

It is the internal identifier of the content reference BuildKit was hashing, generated per build. It means nothing outside BuildKit, and including it in a search is the fastest way to get zero results. Search on the quoted path, or on the phrase "failed to calculate checksum of ref", instead.

## References

- [Docker docs: the build context and .dockerignore](https://docs.docker.com/build/concepts/context/)
- [Docker docs: Dockerfile reference, COPY and COPY --from](https://docs.docker.com/reference/dockerfile/)
- [swarmpit/swarmpit#651: a pipeline copying a jar nothing had built](https://github.com/swarmpit/swarmpit/issues/651)
- [spliit-app/spliit#235: the BuildKit cache key wording for a missing file](https://github.com/spliit-app/spliit/issues/235)
- [BuildKit: the content hash helper that names the ref in the message](https://github.com/moby/buildkit/blob/master/solver/llbsolver/ops/opsutils/contenthash.go)

---

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
