A team reports that `docker build` pauses for over a minute before the first instruction runs, and that any one-line source change rebuilds nearly everything. How would you diagnose and fix this?
answer
- pause before step 1 = context transfer
- --progress=plain → transferring context: NNN MB
- ignore file at context root; ** to recurse
- COPY . . cache key = every context file
- unfiltered context → .git/.env already in published layers
basics
~20 sThat pause is the build context being walked and transferred. Measure it with --progress=plain, find the bulk with du, and exclude version-control, dependency and build-output directories via .dockerignore at the context root — which also stops unrelated files from invalidating the COPY layer's cache.
solid answer
~60 sThe pause before step one is context transfer, so start there. 1. **Measure.** `docker build --progress=plain .` and read `transferring context: NNNMB`. Then `du -sh .` and `du -ah . | sort -rh | head -20` to see what is big. 2. **Confirm the filter is even active.** Check that `.dockerignore` sits at the **context root** — not next to a `-f` Dockerfile — and that patterns recurse (`**/node_modules`, not `node_modules`). 3. **Prove it.** Build a throwaway image that copies everything and `find` inside it; that shows exactly what survived. 4. **Fix.** Exclude `.git`, dependency directories, build output, logs, editor state and `.env*`. Tighten to an allowlist if the tree is large. That also explains the cache behaviour: with `COPY . .`, the layer hash covers every file in the context, so a rebuilt `dist/` or a fresh `.git` object busts it even when no source changed. Shrinking and stabilising the context restores cache hits. Check for leaks while you are there — an unfiltered context often means `.env` or `.git` history is already inside published images.
code
bash · 8 linesdu -sh .
du -ah . | sort -rh | head -20
docker build --progress=plain -t app . 2>&1 | grep -i 'transferring context'
# see exactly what survived the filter
printf 'FROM busybox\nCOPY . /ctx\n' > /tmp/ctx.Dockerfile
docker build -f /tmp/ctx.Dockerfile -t ctxcheck .
docker run --rm ctxcheck du -sh /ctxgo deeper
Recognise that the pre-build pause is the context being sent and that .dockerignore is the fix; know the usual entries to exclude.
Measure with --progress=plain and du, get the recursion rules right, and explain why context contents drive COPY . . cache invalidation.
Run the full loop — measure, verify empirically, fix, re-measure — and treat an unfiltered context as a potential secret exposure requiring rotation, not just a slow build.
Prevent recurrence structurally: baseline ignore files in project templates, a CI guard on context size, narrow COPY by construction, and re-measurement after repository restructures.
## Reading the symptom correctly 'Nothing happens for a minute, then the build starts' is almost always context transfer. The client walks the context directory, applies `.dockerignore`, and ships the result to the builder before the first instruction runs. Legacy output said `Sending build context to Docker daemon 1.9GB`; BuildKit shows `[internal] load build context` with a `transferring context:` byte count, visible with `--progress=plain`. That number is the diagnosis. It gets worse when the builder is remote — a Docker Desktop VM, a CI runner, a shared BuildKit instance — because the payload crosses a network or a virtualised filesystem boundary. Teams often notice this first as 'builds are fine on my Linux box and terrible on laptops'. ## The investigation, in order **1. Quantify the context.** ``` du -sh . du -ah . | sort -rh | head -20 docker build --progress=plain -t app . 2>&1 | head -30 ``` The usual offenders: `.git` (the whole history, often larger than the working tree), `node_modules` / `vendor` / `target` / `.venv` / `.gradle`, build output (`dist`, `build`, `out`), test fixtures and sample data, container-local volume directories that a previous `docker run` created, editor caches, and stray archives or core dumps. **2. Verify the ignore file is actually in play.** Two silent failures dominate. The file is next to the Dockerfile while the context root is elsewhere (`-f services/api/Dockerfile .` means the repository root is the context, so the ignore file belongs there). Or the patterns do not recurse: `node_modules` matches only the top-level directory, because `.dockerignore` compares patterns against the full relative path rather than applying `.gitignore`'s implicit any-depth rule. In a workspace repo, `**/node_modules` is the difference between 40 MB and 1.4 GB. Under BuildKit, also check whether a `<dockerfile>.dockerignore` exists — it *replaces* the root file, so a stale one can quietly disable everything. **3. Prove what survived.** Reasoning about pattern precedence is error-prone; observe instead: ``` printf 'FROM busybox\nCOPY . /ctx\n' > /tmp/ctx.Dockerfile docker build -f /tmp/ctx.Dockerfile -t ctxcheck . docker run --rm ctxcheck du -sh /ctx docker run --rm ctxcheck find /ctx -maxdepth 2 ``` **4. Fix and re-measure.** Add or correct `.dockerignore`, rerun with `--progress=plain`, and confirm the transferred size dropped. In a large or messy tree, invert to an allowlist (`*` then `!src`, `!package.json`, …) so future junk cannot leak in by default. ## Why the cache misbehaves too The second symptom — a one-line change rebuilding everything — is the same root cause. A `COPY . .` layer's cache key is derived from the contents of everything being copied. If the context includes directories that change on their own — `.git` gains objects on every fetch, `dist/` is rewritten by every local build, log files grow, `.DS_Store` appears — then the COPY layer's key changes even when no source file did, and every subsequent layer is rebuilt. Excluding those directories makes the key depend only on real source, and cache hits come back. (Ordering instructions so that dependency manifests are copied and installed before application source is the complementary technique, and belongs to the discussion of COPY and layer caching rather than to context hygiene.) ## The security finding you get for free An unfiltered context has usually been shipping more than time. With a broad `COPY . .`, anything present becomes an image layer: `.env` with production credentials, `.git` containing history and possibly secrets that were 'removed' in a later commit, `*.pem` keys, cloud credential files, a `.npmrc` with a registry token. When you find an unfiltered context, check published images: ``` docker history --no-trunc myimage:tag docker run --rm myimage ls -la /app ``` If a secret is in a published layer, `.dockerignore` does not retract it — treat it as an exposure, rotate the credential, and rebuild and republish. Layer content is permanent for anyone who pulled the tag. ## Preventing recurrence - Commit a sensible baseline `.dockerignore` in the project template, including `.git` and `.env*`. - Fail CI when the transferred context exceeds a threshold; the number is easy to parse from plain progress output and is a good early-warning signal. - Prefer explicit COPY of the paths you need over a blanket `COPY . .` for small images — it makes cache keys narrow by construction. - Re-measure after major repository restructures, since a moved Dockerfile or a new workspace package quietly changes what the ignore file covers.
- A `.dockerignore` exists at the repository root but the context is still 1.4 GB. What do you check first?Whether the patterns recurse. `node_modules` or `target` without `**/` matches only the top-level path, so nested copies in a workspace repository are still transferred. Then check that the file really is at the context root for this invocation, and whether a `<dockerfile>.dockerignore` exists, since BuildKit uses that instead of the root file.
- Why does excluding `.git` and `dist` improve cache hits and not just transfer time?A `COPY . .` layer is keyed on the contents of everything copied. Directories that mutate independently of your source — new git objects after a fetch, rewritten build output — change that key on every build, invalidating that layer and everything after it. Removing them from the context makes the key depend only on real source files.
- You discover a published image contains `.env` with live credentials. What now?Treat it as a disclosed secret, not a build bug. Rotate the credentials immediately, since anyone who pulled the tag has them permanently. Then fix `.dockerignore`, rebuild and republish the affected tags, and remove the exposed tags from the registry. Adding the ignore rule alone changes nothing about images already distributed.
saying these in an interview costs you the question
- Blaming slow builds on the network or the base image without measuring the transferred context size.
- Adding `.dockerignore` next to the Dockerfile when the context root is elsewhere.
- Using non-recursive patterns and concluding that ignore files 'do not work'.
- Believing that adding an ignore rule removes a secret from images already published.
- Reaching for `--no-cache` to 'fix' cache instability, which hides the context problem and makes builds slower still.