Skip to content
Latchkey LogoLatchkey home

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

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.

Runner log: one missing directory refused by the legacy builder and then by BuildKit
The recorded run: the same Dockerfile and the same missing directory through both builders, then a third build where the path exists and .dockerignore hides it.
Diagram of the build context, what removes a path from it, and the two error wordings
Three things keep a path out of the context: nothing produced it here, the build took the Git reference instead of the runner, or .dockerignore excludes it.

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

Reproduced on a Latchkey runner

Run 2026-09-20·Runner latchkey-small·Exit code 1

Docker version 29.7.2, build a7dcaa6
github.com/docker/buildx v0.36.1 1d8dde89b8aba914e05e45366770736fea1fd690
docker.io/library/alpine:3
--- build context, before the builds
Dockerfile
--- legacy builder: DOCKER_BUILDKIT=0 docker build
Step 2/2 : COPY dist/ /app/dist/
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
--- now dist/ exists and .dockerignore excludes it
app.js
#1 WARN: CopyIgnoredFile: Attempting to Copy file "dist" that is excluded by .dockerignore (line 2)
ERROR: failed to build: failed to solve: failed to compute cache key: failed to calculate checksum of ref ryg7koya7jiszdax614bj27m4::njccdmougp14wckursctxjgcx: "/dist": not found
[latchkey-bash-wrapper] BEGIN sidecar POST (boot_wait=30s max_time=320s url=http://localhost/diagnose socket=/run/latchkey-self-heal/sock)
[latchkey-bash-wrapper] END sidecar POST ok (attempts=1 http=200)

The runner diagnosed the failure and did not retry it; this failure needs the fix below.

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 saysWhich builderWhat it tells you
"COPY failed: file not found in build context"Legacy builderThe path, and that .dockerignore is a candidate
"failed to compute cache key ... not found"BuildKitThe path with a leading slash, and nothing else
WARN: CopyIgnoredFileBuildKit checkThat .dockerignore is why, and the line number in it
A COPY that silently copies nothingEitherA wildcard source that matched no files

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

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 rightThe 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 foundBuildKit content hash, the only fragment naming your path

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.

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.

Frequently asked questions

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.

Related guides

References

The run after the fix should not start from zero. Latchkey keeps your layers on the runner. Start free → 30-day trial · No credit card