Docker: failed to compute cache key — "/file": not found

Quick answer

Despite the name, this is almost never about caching. A COPY named a path that isn't in the build context:

  • COPY resolves from the context root (the . you passed to docker build) — not from the Dockerfile's folder.
  • Check .dockerignore — an excluded file is invisible to the build even though it's on disk.
  • .. can't escape the context — widen the context instead.
  • COPY --from resolves inside that stage, not the context.

The exact error string

$ docker build -t myapp .
 => ERROR [4/8] COPY package.json ./
------
 > [4/8] COPY package.json ./:
------
failed to solve: failed to compute cache key: failed to calculate checksum
of ref abc123::xyz: "/package.json": not found

// The legacy (non-BuildKit) builder says the same thing far more plainly —
// and names the second-most-common cause outright:
//
// COPY failed: file not found in build context or excluded by .dockerignore:
// stat package.json: file does not exist

If Docker reports failed to compute cache key: failed to calculate checksum of ref … "/file": not found, the problem is almost always that the file isn't in the build context — not that anything is wrong with your cache. The wording is unhelpful in a specific way: it describes the step that failed (hashing a file to decide whether the layer can be reused) rather than the reason (the file isn't there to hash). Read past "cache key" — the useful part is the quoted path at the end and the COPY line above it. The legacy builder's message is the better mental model, because it names both causes at once: not in the build context, or excluded by .dockerignore.

The one concept that explains most of these

COPY source paths are resolved from the build context root — the path you pass as the final argument to docker build — and never from the directory the Dockerfile happens to live in. Only files inside that context are uploaded to the builder; everything else may as well not exist.

Everything on this page applies equally to ADD, which resolves local source paths the same way and fails with the same message.

COPY resolves from the build context — not the Dockerfile's folder docker build -f docker/Dockerfile . context root = "." uploaded to the builder myrepo/ package.json src/ docker/ Dockerfile ../shared/ outside the context — never uploaded, never reachable by COPY COPY package.json ./ → <context>/package.json  ✓ COPY ../shared/lib ./ → escapes the context  ✗ Moving the Dockerfile changes nothing. Only the context argument moves the root.

The Dockerfile's location is irrelevant to COPY resolution — -f only says which file to read. The final argument to docker build is what sets the root, and nothing outside it is uploaded.

Cause 1: the Dockerfile is in a subdirectory

The classic. Your Dockerfile sits in docker/ and its COPY lines are written as if paths were relative to itself:

# myrepo/docker/Dockerfile, built with:  docker build -f docker/Dockerfile .

# ❌ package.json sits beside the Dockerfile, in docker/
# COPY package.json ./

# ✅ spell it from the context root
COPY docker/package.json ./

The mirror-image mistake is just as common: the file is at the repo root, but you build with docker build docker/, which shrinks the context so the root file now sits outside it. Pick where the context root should be first, then write every path relative to that.

The same trap in Docker Compose

Compose splits the two settings across separate keys, which makes it easy to assume dockerfile: is what matters. It isn't — context: is:

services:
  app:
    build:
      context: .                      # ← this sets the COPY root
      dockerfile: docker/Dockerfile   # ← this only says which file to read

With that config, COPY package.json ./ looks for <project-root>/package.json, not <project-root>/docker/package.json — exactly as if you'd run docker build -f docker/Dockerfile . by hand. Note also that context: is relative to the Compose file's own location, so moving compose.yaml into a subdirectory silently moves the root along with it.

Cause 2: .dockerignore is excluding it

The most confusing version, because the file is plainly on disk and the path is plainly correct. Excluded files are never uploaded, so the build genuinely cannot see them:

# .dockerignore
*                    # exclude everything...
!src/                # ...then re-include only these
!package.json

# A Dockerfile line like `COPY tsconfig.json ./` now fails: tsconfig.json
# was excluded by `*` and never re-included.

Allowlist patterns (* followed by ! re-inclusions) cause this far more often than plain denylists, because adding a new file to the repo silently leaves it excluded — the build breaks on a file you never touched.

Three cheap checks, in order, before anything clever:

  1. grep your .dockerignore for anything that could match the path — and read the whole file, since a later ! line may or may not re-include what an earlier rule excluded.
  2. Confirm the context root you're actually building with (the final argument to docker build, or context: in Compose).
  3. Re-read the COPY source character by character against the real path on disk.

Advanced: see exactly what the builder received. If those three don't settle it, build a throwaway image that copies the whole context and lists it — this reflects .dockerignore filtering precisely, with no guessing about pattern semantics:

# Print the context as the builder sees it, honouring .dockerignore
$ docker build --no-cache --progress=plain -f - . <<'EOF'
FROM busybox
COPY . /ctx
RUN find /ctx -maxdepth 2 -not -path '*/.git/*'
EOF

If the missing file doesn't appear in that listing, .dockerignore is your answer — no further guessing needed.

Cause 3: the path escapes the context with ..

Sharing a library between two services and reaching "up and over" into it is the natural instinct, and it can never work:

# ❌ from a context of ./service-a, this points outside — always fails
# COPY ../shared/lib ./lib

# ✅ move the context up so both are inside it, and keep -f pointing at the file
$ docker build -f service-a/Dockerfile -t service-a .
# then, in the Dockerfile:
COPY shared/lib ./lib
COPY service-a/src ./src

Widening the context has a cost worth knowing: everything inside it is uploaded to the builder on every build, so a repo-root context with large directories makes builds slow. Pair it with a .dockerignore that excludes node_modules, .git, build output, and anything else the image doesn't need — which is the same file that causes Cause 2, so keep the two in mind together.

Cause 4: the file isn't committed (CI-only failures)

Builds fine locally, fails on the runner. The usual explanation is that the file exists in your working tree but not in a fresh checkout:

# Is the path actually tracked by git?
$ git ls-files --error-unmatch dist/bundle.js
error: pathspec 'dist/bundle.js' did not match any file(s) known to git

Typical culprits are build output covered by .gitignore, a local .env, or generated code. The second flavour is ordering: a step that produces the file runs before docker build on your machine but after it in the pipeline. Either commit the file, generate it earlier in the pipeline, or produce it inside the Dockerfile so the build no longer depends on the host having run something first.

The third flavour is case. macOS and Windows filesystems are case-insensitive by default, so COPY Config.json ./ happily finds a file named config.json on your machine and fails on a case-sensitive Linux CI runner:

# Local (macOS/Windows): resolves — the filesystem ignores case
# CI (Linux):            "/Config.json": not found

$ git ls-files config.json
config.json          # ← tracked and present; the NAME is what's wrong

This one is worth calling out separately because it slips past the git ls-files check above: the file is committed and present, so that test passes while the build still fails. Compare the exact casing in the COPY line against git ls-files output, which reports the name as recorded in the repository rather than as your local filesystem chooses to display it.

Cause 5: COPY --from in a multi-stage build

Here the rules change: with --from, the source path resolves inside the named stage's filesystem, not the build context. So "not found" means that path doesn't exist in that stage:

FROM node:22-slim AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build      # produces /app/dist

FROM node:22-slim
WORKDIR /app
# ❌ wrong path inside the builder stage — build output is at /app/dist
# COPY --from=builder /dist ./dist

# ✅
COPY --from=builder /app/dist ./dist

Three things to check when --from fails: the stage name is spelled the same as its AS label; the stage is typically defined earlier in the file (BuildKit does resolve forward references in some cases, but ordering the stages conventionally removes the question); and the build in that stage actually produced the directory you're naming. For the third, stop at the earlier stage and look:

# Build only the builder stage, then inspect what it really contains
$ docker build --target builder -t myapp-builder .
$ docker run --rm myapp-builder ls -la /app

That --target trick is the fastest way to settle a multi-stage disagreement, and it works for any stage in the file. If the stage itself fails to start rather than the copy, see executable file not found in $PATH.

Quick triage

SymptomMost likely causeFirst check
Dockerfile is in a subfolderPath written relative to the DockerfileRe-spell the path from the context root
File is visibly on disk, path looks right.dockerignore exclusionGrep .dockerignore; list the real context
Source path contains ..Escapes the contextWiden the context, use -f
Only fails in CIFile not committed, or generated latergit ls-files on the path
Only fails in CI, but the file is committedFilename case — case-insensitive locally, not on LinuxCompare casing against git ls-files output
Using Docker Composecontext:, not dockerfile:, sets the rootRead build.context in the Compose file
Line uses --from=Path wrong inside that stage--target that stage and ls

Debugging checklist

Frequently Asked Questions

What does 'failed to compute cache key' mean?

BuildKit hashes the files a COPY or ADD instruction references so it can decide whether that layer can be reused from cache. To hash a file it must first find it in the build context. When the path is not there, hashing fails and the build stops with this message. Despite the wording it is almost never a caching problem — the quoted path simply is not in the context.

Why does COPY not find a file that is clearly there?

COPY source paths resolve from the build context root — the path you passed to docker build, usually the final dot — not from the directory containing the Dockerfile. If your Dockerfile lives in docker/ and you build with a context of ., then COPY package.json means the package.json at the repository root, not the one beside the Dockerfile. The file exists; it is just not where the context says to look.

Can COPY reach files outside the build context?

No. Only files inside the context are uploaded to the builder, so a source path containing .. that escapes the context root can never resolve. This is a deliberate boundary, not a bug. The fix is to widen the context — build from a parent directory and pass the Dockerfile with -f — or to restructure so everything the build needs lives inside one context.

How does .dockerignore cause this error?

Files matched by .dockerignore are never sent to the builder, so as far as the build is concerned they do not exist — even though you can see them on disk. This is the most confusing version of the error because everything looks correct locally. Check .dockerignore for a pattern matching the missing path, including broad rules such as an asterisk followed by re-inclusions, which exclude far more than people expect.

Why does it fail in CI but work on my machine?

Usually the file exists locally but was never committed, so it is absent from a fresh CI checkout — build output, a .env file, or anything covered by .gitignore. The other common cause is a build step that generates the file running after the Docker build in CI but before it locally. Confirm by checking whether git ls-files lists the path.

Why does COPY --from fail with the same message?

With COPY --from the source path resolves inside the named stage or image, not in the build context, so the question becomes whether that path actually exists in that stage's filesystem. Common causes are a stage name typo, referencing a stage defined later in the file, or assuming an output directory that the build in that stage never produced. Run a shell in the earlier stage and list the directory to confirm what is really there.

References

More Docker errors & tools

Browse the Docker error cluster, or analyse a Dockerfile for size, caching, and security issues.

All Error References Docker Image Optimizer Docker: pull access denied
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.