Docker Cache Key Error: Fix COPY Not Found Fast
Docker failed to compute cache key on COPY? Fix missing context files, .dockerignore, build directory, and --from stage errors.
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
- ✓Docker 23+ with BuildKit available
- ✓A Dockerfile containing at least one COPY instruction
- ✓Basic comfort with shell directories and paths
- failed to compute cache key means the COPY source isn't in the build context — the file the daemon received, not your repo
- Check your shell directory first: the final docker build argument defines the context, and running from a subfolder excludes everything above it
- Grep .dockerignore for the failing filename — broad rules silently hide files from COPY as well as caching
- COPY paths can't escape with ../ and --from must name a real AS stage — renames orphan both silently
- Standardize on BuildKit and pass context roots explicitly so laptop and CI ship identical file sets
Imagine you ask a print shop to copy a photo, but you hand them the wrong envelope — your photo sits on your desk at home. The clerk says your photo's missing, and you insist it's in your house. You're both right: the file exists, just not in the envelope you handed over. The Docker build context is that envelope. Wrong directory, ignore rules, and bad paths all mean the file never made it into the envelope, so no copy can happen.
You run docker build, and instead of layers you get failed to compute cache key for COPY app/server.js not found. The file is right there — you can cat it, your editor shows it, Git tracks it. So you rebuild, you restart the daemon, you prune the builder cache, and the error doesn't move. That's because the error was never about the file. It's about the build context: the slice of your filesystem the daemon actually received, which doesn't contain your file at all.
This failure blocks everything downstream. No image, no test run, no deploy — and the message points at the Dockerfile line instead of the real culprits: a wrong build directory, a .dockerignore rule, or a COPY path that escapes the context root. Teams lose hours editing Dockerfiles that were correct while the context quietly ships the wrong file set.
This guide fixes it in context order. You'll learn how BuildKit computes cache keys from transferred files, why missing COPY sources really mean missing context, how .dockerignore silently eats files, what the build directory and -f flag actually control, how multi-stage --from references break, and how the legacy builder and BuildKit report the same underlying causes with different wording — so search for both messages and pin the Docker version in CI for reproducible builds. By the end you'll diagnose any cache-key failure with two checks and structure contexts that stop causing them.
BuildKit computes a cache key for each COPY by hashing the source files as they exist inside the transferred context — not your repo, not your disk, but the file set the CLI actually sent the daemon. When that hash can't be computed because no matching file arrived, the build stops before creating any layer. The phrase cache key misleads everyone into cache-pruning; the real sentence is the second half: not found. Nothing to hash, nothing to build.
The transfer is the part nobody visualizes. The CLI tars up the context directory, applies .dockerignore, streams it to the daemon, and only then resolves COPY sources against that tarball. A 400 MB context with node_modules excluded down to 40 MB is normal and good — but every exclusion is also a promise that no COPY needs those files. Break the promise and the daemon reports the file missing, because from its side of the stream it genuinely is. Your local ls is irrelevant; the tarball is the truth.
Internalize this and every later section gets easier: debugging means comparing the context against the repo, never the Dockerfile against your memory. Rebuild with --progress=plain and read which files the step attempted. The gap between ls in your shell and the daemon's file list is the entire bug class, and closing it is a matter of directory, ignore rules, and paths — the next three sections in that order.
The most common context bug is also the simplest: the COPY source path doesn't exist relative to the context root. COPY app/server.js ./ means <context>/app/server.js, full stop. A typo (app/sever.js), a case mismatch that macOS forgives but Linux builders don't (App/server.js), or a move that relocated the file without updating the Dockerfile all produce the identical not-found error. The Dockerfile is a contract against a directory layout, and layouts drift.
Case sensitivity deserves special attention because it creates works-on-my-machine ghosts. macOS default filesystems treat App and app as the same directory, so the build passes on a MacBook and fails in Linux CI with the same repo and the same Dockerfile. Trailing-slash semantics are consistent: COPY app/ dest/ and COPY app dest both copy the directory's contents, so neither nests differently. The legacy builder and BuildKit share the same causes and fixes here; only the wording differs — legacy prints 'COPY failed: file not found in build context or excluded by .dockerignore: stat app/server.js: file does not exist', while BuildKit prints 'failed to compute cache key: ... not found'. Search for both messages, and pin the Docker version in CI for reproducible builds.
Verify paths mechanically, not by reading. List the exact source under the context root with ls, check case with a case-sensitive glob, and keep a repo-root convention: all COPY sources relative to one root, one Dockerfile location, one build command. When files live outside the root, stage them in with a build script instead of clever relative paths — cleverness is how ../ escapes sneak in, and those fail by design in the next section's way.
The .dockerignore file decides which files never enter the context tarball, and its rules apply to COPY with equal force. That's the intended deal — smaller contexts, faster transfers, secrets kept out — but every line is also a loaded footgun pointed at future COPY instructions. A rule like *.log added to trim junk also swallows build/manifest.log when someone later copies it. A dist exclusion from the pre-bundler era silently eats the compiled assets the new pipeline needs. The error never mentions the ignore file; it just says the file wasn't found.
Wildcard scope is where teams get bitten. Patterns like * embrace more than intended (they match dotfiles in some positions, subdirectories in others), and negation rules (!keep.me) only re-include files when no parent directory is excluded — exclude build/ and a !build/keep.me rule won't resurrect anything. Engineers add a broad rule, verify the build still passes that day, and the breakage arrives months later when a new COPY reaches for an ignored path. The person who added the rule and the person who hit it are never the same engineer.
When a COPY fails on a missing file, the cause is the same under the legacy builder and BuildKit — only the wording differs: legacy prints 'COPY failed: file not found in build context or excluded by .dockerignore: stat app/server.js: file does not exist', while BuildKit prints 'failed to compute cache key: ... not found'. Search for both messages, and pin the Docker version in CI for reproducible builds.
Treat .dockerignore like code: commented, reviewed, and tested. Every rule gets a trailing comment naming what it excludes and why. Before adding a rule, grep the Dockerfile for COPY sources it could match. When a cache-key failure strikes, grep the ignore file for the failing filename first — it's the fastest confirmation in this whole guide. And keep a canary: a CI step that asserts each COPY source survives the ignore rules catches tomorrow's broad wildcard today.
The final argument to docker build is the context, and the -f flag only locates the Dockerfile — mixing these up is the classic monorepo failure. Running docker build -f docker/web/Dockerfile docker/ sets the context to docker/, so COPY app/server.js resolves to docker/app/server.js regardless of where the Dockerfile lives. Your repo root can hold every file perfectly while the build sees only the subdirectory. Local builds from the root work; CI builds from the nested directory fail; everyone argues about the Dockerfile, which is identical in both.
The confusion is historical. Both the legacy builder and BuildKit use the final docker build argument as the context and resolve -f relative to the current directory; they do not resolve contexts differently. What differs is the message: legacy prints 'COPY failed: file not found in build context or excluded by .dockerignore: stat app/server.js: file does not exist', while BuildKit prints 'failed to compute cache key: ... not found'. The causes and fixes are identical under both, so search for both messages. BuildKit is the default since Docker Engine 23, so pin the Docker version in CI for reproducible builds.
Standardize one pattern and never deviate: context is always the repo root, -f points at the nested Dockerfile. docker build -f docker/web/Dockerfile . reads naturally once you know the last dot is the context doing the real work. Wrap it in a build script so nobody improvises directories under deadline pressure, and assert in CI that every COPY source exists under the declared context root. Directory discipline eliminates this failure class permanently.
Multi-stage builds add a second namespace where cache-key failures hide: the --from reference. COPY --from=builder /out/app ./ copies from a previous stage's filesystem, not from your context — so the failure means the stage name doesn't resolve, not that a file is missing. Renames orphan references silently: someone tidies builder into compile, three downstream COPY lines still say --from=builder, and the build fails on a name that no longer exists. Numeric indexes are worse — --from=0 shifts meaning every time a stage is inserted or removed above it.
Conditional and target-scoped builds add subtler variants. A stage behind a build-arg condition that evaluates false never materializes, so --from pointing at it fails only in the configurations that skip it — green default builds, red matrix entries. Building with --target mid-Dockerfile similarly truncates later stages; any --from naming a stage past the target fails even though the full build works. These config-dependent failures look flaky because they are: same repo, different flags, different stage graphs.
Pin stage names like API contracts. Declare every AS name in a comment block at the top of multi-stage Dockerfiles, grep --from targets against FROM lines in CI, and ban numeric indexes in review. When a --from failure hits, the diagnosis is one command: list the stages, list the references, diff them. Names that appear in references but not in stages are your answer, and the fix is a rename in one direction or the other — never a cache prune.
BuildKit and the legacy builder report context failures with different wording, so identical commands can look like different problems per machine. Both use the final docker build argument as the context and resolve -f relative to the current directory; the causes and fixes are identical under both. Legacy prints 'COPY failed: file not found in build context or excluded by .dockerignore: stat app/server.js: file does not exist', while BuildKit prints 'failed to compute cache key: ... not found'. The same repo, the same command, two messages — and the failure follows the builder's wording, not the code.
Mixed fleets make this miserable. Laptops on Docker 24 with BuildKit default, CI runners on Docker 20 with legacy, a specialty builder somewhere in between — each words edge cases its own way, so failures correlate with where the build ran instead of what changed. Engineers bisect commits while the variable is the runner image. The --progress=plain output differs too, which means even the debugging evidence looks inconsistent across machines.
End the confusion by standardizing. Enable BuildKit everywhere — DOCKER_BUILDKIT=1 or Docker 23+, where it's default — pin the Docker version in CI images for reproducible builds, and search for both messages when diagnosing context failures. Then re-test all Dockerfiles under the standard builder once, because the stricter reporting surfaces latent sins (escapes, ignored files, sloppy -f usage) that legacy tolerated. One builder, one context convention, zero machine-dependent mysteries.
Wrong Context Directory Blocked 23 PRs for 7 Hours
- Cache-key errors describe the context, not the cache — pruning builders 11 times cost a day while the context stayed wrong.
- Build pipelines must assert their own inputs: a 5-line COPY-source check would have failed the first broken commit, not the 40th.
- Monorepo restructures must update build contexts alongside code moves, because the Dockerfile keeps pointing at paths the move deleted.
| File | Command / Code | Purpose |
|---|---|---|
| cache-key-diagnosis.sh | export DOCKER_BUILDKIT=1 | bash configuration |
| Dockerfile | FROM node:20-alpine AS deps | dockerfile configuration |
| ignore-audit.sh | cat .dockerignore | bash configuration |
| context-directory-fix.sh | docker build -f docker/web/Dockerfile -t web:latest . | bash configuration |
| Dockerfile | FROM node:20-alpine AS base | dockerfile configuration |
| builder-standardize.sh | docker buildx version | bash configuration |
Key takeaways
Common mistakes to avoid
6 patternsRunning docker build from a subdirectory
Adding a broad .dockerignore rule and forgetting it
Using parent-directory paths in COPY
Referencing a renamed or deleted build stage
Assuming legacy and BuildKit resolve contexts identically, the causes and fixes are the same under both builders; only the wording differs, so search for both messages. Legacy prints 'COPY failed: file not found in build context or excluded by .dockerignore: stat app/server.js: file does not exist', while BuildKit prints 'failed to compute cache key: ... not found'. BuildKit is the default since Docker Engine 23, so pin the Docker version in CI for reproducible builds.
Treating stage renames as cosmetic cleanup
Interview Questions on This Topic
What is the Docker build context and why does COPY depend on it?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
That's Docker. Mark it forged?
7 min read · try the examples if you haven't