Docker tells you that the build cache broke. whycache tells you why, which file did it, and what it cost you.
That is a real run against psf/black. Every commit
costs a five-minute rebuild, because COPY . /src/ pulls in .git, which
changes on every commit, and the expensive apt install + pip install +
hatch build steps all sit below it.
Nobody on that project is doing anything wrong. It is simply invisible — Docker prints which steps re-ran, never why.
pip install whycacheNo daemon, no account, no config, no network calls. One dependency-free CLI.
whycache # build the current directory
whycache path/to/project # or somewhere else
whycache . -- --build-arg V=2 # anything after -- goes to docker buildRun it once to record a baseline, then run it again after you change something.
A file changed. Which one, and only the ones that instruction actually copies:
X Cache broke at build step 10: COPY src ./src
Reason - 1 file(s) changed in the build context:
src/sqlfluff/api/info.py
Cost - 7.5s of this 7.5s build.A build arg changed. Docker substitutes args into the command before caching it, so the change is visible if you know where to look:
X Cache broke at step 2: RUN echo "building 3.0.0" > /version
Reason - the instruction itself changed since the last build:
was: RUN echo "building 2.0.0" > /version
now: RUN echo "building 3.0.0" > /versionJunk is polluting your build context. With a concrete fix:
Fix - add to .dockerignore:
.git/ (changes on every commit, 2 file(s))
node_modules/ (reinstallable from lockfile, 41 file(s))
Saves ~4m12s per build....unless that fix would break your build. black derives its version from
git via hatch-vcs, so ignoring .git/ does not make the build slower — it
makes it fail:
.git/ is breaking the cache (1 file(s)), but do NOT ignore it:
pyproject.toml reads git metadata at build time, so excluding
.git/ makes the build fail rather than just slow.This one is not hypothetical. An earlier version of this tool gave that advice,
and broke black's build with it.
Same tool, three real projects, four measurements:
| Project | What changed | Cache broke at | Cost |
|---|---|---|---|
| psf/black | one commit | COPY . /src/ (step 3 of 6) |
5m14s |
| sqlfluff | one source file | COPY src ./src (step 10 of 12) |
7.5s |
| traefik/whoami | go.mod |
COPY go.mod . (step 4) |
30.2s |
| traefik/whoami | app.go |
COPY . . (step 7) |
29.6s |
A miss high in the Dockerfile is expensive because everything below it re-runs. A miss near the bottom is cheap. That gap is the entire skill of writing a Dockerfile, and this is what makes it visible.
I read the Dockerfile of 500 popular projects.
One in five rebuilds from scratch on every commit — and for a third of those,
adding .git to .dockerignore does not slow the build down, it breaks it.
- Runs your build with
--progress=rawjsonand reads the structured output. - Finds the first step that was not
CACHED. - Fingerprints your build context, honouring
.dockerignore, and diffs it against the last run. - Sums the time of every step that re-ran.
State lives in ~/.whycache/, keyed by project path. Nothing is written into
your build context — doing that would change the context and break the cache,
which is the exact problem this tool exists to report.
- It will not edit your Dockerfile. It tells you; you decide.
- It will not guess. If the cache was cold, or the cause is outside the build context, it says so and names no file. A confidently wrong answer is worse than no answer, so it declines rather than blames.
- No AI. This is a deterministic diff of two file lists. An LLM would make it slower, costlier, and less trustworthy.
| Tool | Answers |
|---|---|
dive |
How much space is wasted in the image |
hadolint |
Does the Dockerfile follow style rules |
whycache |
Why the cache missed on this build, and what it cost |
Different questions. dive and hadolint are both excellent; neither answers this one.
- Docker with BuildKit (the default since Docker 23)
- Python 3.10+
Works on Linux, macOS, and Windows. It is developed on Windows, so the console output degrades to ASCII rather than crashing on cp1252 — a courtesy most Linux-first tooling forgets.
Fingerprinting a 5,955-file / 25 MB context takes ~730 ms on a warm run, because unchanged files are never reopened. First run on a project costs ~6 s.
Bug reports with a Dockerfile that reproduces are the most useful thing you can
send. The test suite runs without Docker except for test_e2e.py:
python tests/test_dockerignore.py # .dockerignore matching
python tests/test_build.py # step parsing, miss detection
python tests/test_blame.py # attribution and the git-versioning guard
python tests/test_manifest.py # context fingerprinting
python tests/test_e2e.py # full run against a real build (needs Docker)Or all at once with pytest tests/. Everything but test_e2e.py runs without
Docker, and all of them run from a fresh clone with nothing installed.
whycache/
build.py run docker build, read BuildKit's progress stream
blame.py decide what caused the miss, and what is safe to advise
report.py turn a diagnosis into something actionable
manifest.py fingerprint the build context
dockerignore.py Docker's ignore semantics
cli.py wire it together, own no logic
tests/
research/ the survey behind the write-up; not part of the package
MIT