How does a `.dockerignore` file work — where must it live, what pattern syntax does it use, and in what ways does its matching differ from `.gitignore`?
answer
- lives at the context root, not next to the Dockerfile
- Go filepath.Match: * stops at /, ** crosses
- last matching pattern wins; ! re-includes
- node_modules ≠ **/node_modules (unlike git)
- allowlist idiom: * then !src !package.json
basics
~20 sIt sits at the build-context root and lists patterns excluded before the context is sent. Patterns are path-matched with *, ? and **, ! re-includes, and the last matching line wins. Unlike .gitignore, a bare name like node_modules matches only at the top level — use **/node_modules.
solid answer
~50 s`.dockerignore` lives at the **root of the build context** (not next to the Dockerfile, unless that is the same place) and filters files client-side before they are transferred. Syntax: one pattern per line, `#` for comments, Go path matching with `*` (does not cross `/`), `?`, and `**` for any number of directories. Leading `/` is optional — patterns are always relative to the context root. A line starting with `!` re-includes, and **the last matching pattern decides**. The differences from `.gitignore` that catch people: - `node_modules` matches only `./node_modules`, not nested copies; git would match it at any depth. Write `**/node_modules`. - Re-inclusion works even when the parent was excluded, so the allowlist idiom `*` then `!src` then `!package.json` is valid — in git it is not. Excluded files are neither uploaded nor available to COPY. `Dockerfile` and `.dockerignore` may be listed, which prevents COPY-ing them without breaking the build. With BuildKit, a sibling file named `<dockerfile>.dockerignore` takes precedence over the root one.
code
bash · 22 lines# version control and CI
.git
.github
# dependencies and build output, at any depth
**/node_modules
**/target
**/dist
**/.venv
# local config and secrets
.env*
*.pem
# editor / OS noise
.idea
.vscode
.DS_Store
# keep this one file despite the *.md rule below
**/*.md
!README.mdgo deeper
Know that the file sits at the context root, lists things to exclude, and keeps builds fast and secrets out.
Explain the matching rules precisely — * versus **, last-match-wins, ! exceptions — and the recursion difference from .gitignore.
Discuss cache stability and secret hygiene, the allowlist trade-off, BuildKit's per-Dockerfile ignore file, and how to verify the effective context empirically.
Treat ignore files as governed repository policy: shared baselines, per-service overrides in a monorepo, and review or lint rules that keep them from drifting.
## Placement The file must be at the **root of the build context** — the directory you pass to `docker build`. If you build from the repository root with `-f services/api/Dockerfile .`, the ignore file belongs at the repository root, not beside the Dockerfile. Putting it next to the Dockerfile in that layout is a common and silent failure: the build works, it is just slow, because nothing is being filtered. BuildKit adds one refinement: if a file named `<path-to-dockerfile>.dockerignore` exists — for example `services/api/Dockerfile.dockerignore` — it is used **instead of** the context-root file. That is what makes per-service ignore rules practical in a monorepo where every service shares one context. ## Pattern syntax One pattern per line. Blank lines and lines beginning with `#` are ignored. Patterns are matched against each file's path **relative to the context root**, using Go's `filepath.Match` semantics plus a Docker extension: - `*` matches any sequence of characters **except** the path separator. - `?` matches a single non-separator character. - `**` matches any number of directories, including zero. - A leading `/` is allowed and means the same as omitting it — everything is anchored at the context root. - `!` at the start of a line re-includes anything that matches it. - **Last match wins**: ordering is significant, and Docker evaluates every pattern against every path rather than short-circuiting on the first hit. Examples: `*.md` excludes markdown at the top level only; `**/*.md` excludes it everywhere; `logs/*` excludes the contents of `logs` but keeps the directory itself; `temp?` excludes `temp1` and `tempa` but not `temp10`. ## The three differences from `.gitignore` worth memorising **1. No implicit recursion.** In git, a pattern without a slash matches at any depth, so `node_modules` covers nested copies. In `.dockerignore`, `node_modules` matches only the top-level directory because `*`-free patterns are compared against the full relative path. In a workspace repository with `packages/*/node_modules`, that single missing `**/` is often the whole reason a context is a gigabyte. **2. Re-inclusion under an excluded parent works.** Git refuses to re-include a file if its parent directory was excluded, because it never descends into it. Docker walks the whole tree and evaluates patterns per path, so the allowlist idiom is valid and widely used: ``` * !src !package.json !package-lock.json ``` This works because `*` does not cross `/`, so it excludes top-level entries only, and `!src` re-includes the directory whose children were never matched by `*` in the first place. Allowlists give the smallest, most predictable context; the cost is that a genuinely new required file is silently omitted until someone updates the list, which surfaces as a confusing COPY failure or a missing file at runtime. **3. No trailing-slash directory semantics.** Git treats `build/` as 'directory only'. Docker's matcher has no such distinction; `build` matches the path whether it is a file or a directory. ## What exclusion actually does Excluded paths are not walked into the transfer, so they are not sent to the builder and are **not available to COPY or ADD**. This makes `.dockerignore` do double duty: - **Performance**: less to walk, hash and transfer, and — importantly — a stable set of inputs for the `COPY . .` layer, so the cache is not invalidated by a rebuilt `dist/` or a fresh `.git` object. - **Safety**: `.env`, private keys, `.git` history and cloud credential files never enter the payload, so a broad `COPY . .` cannot bake them into a layer that ships to a registry. A subtlety: listing `Dockerfile` and `.dockerignore` themselves is allowed and does not break the build, because the client reads them separately; it only prevents them from being COPY-able into the image. A second subtlety: exclusion is client-side filtering, not redaction. If a secret already exists in an earlier image layer, adding it to `.dockerignore` later changes nothing about that published image. ## Verifying it works Do not trust the file by inspection. Compare the builder's reported context size before and after (`docker build --progress=plain` shows `transferring context: …`). For a precise answer, build a throwaway image that copies everything and list it: ``` printf 'FROM busybox\nCOPY . /ctx\nCMD ["true"]\n' > /tmp/ctx.Dockerfile docker build -f /tmp/ctx.Dockerfile -t ctxcheck . docker run --rm ctxcheck find /ctx -maxdepth 2 ``` That shows exactly what survived the filter — far more reliable than reasoning about pattern precedence in your head. ## Practical baseline Most repositories want, at minimum: version-control metadata (`.git`), dependency directories (`**/node_modules`, `**/target`, `**/.venv`), build outputs (`dist`, `build`, `out`), local environment and secret files (`.env*`, `*.pem`), editor and OS noise (`.idea`, `.vscode`, `.DS_Store`), and test/CI artifacts. Then measure, and tighten toward an allowlist if the context is still large.
- Your `.dockerignore` contains `node_modules` but the context is still huge in a workspace repo. Why?A pattern with no wildcard is matched against the full relative path, so `node_modules` excludes only the top-level directory. Nested copies such as `packages/web/node_modules` are untouched. Use `**/node_modules` to exclude them at any depth — this is the single biggest behavioural difference from `.gitignore`.
- When is an allowlist (`*` plus `!` exceptions) better than a denylist, and what does it cost?An allowlist gives the smallest and most predictable context, which matters in large monorepos and for cache stability, and it fails safe on secrets since nothing new is included by default. The cost is maintenance: a newly added required file is silently excluded until someone updates the list, typically surfacing as a COPY failure or a file missing at runtime.
- Does adding a file to `.dockerignore` remove it from images you already published?No. It only filters what future builds transfer. A secret already committed into a published layer stays in that image and must be handled as a real exposure — rotate the credential and rebuild and republish affected tags.
saying these in an interview costs you the question
- Placing `.dockerignore` next to the Dockerfile when the context root is elsewhere, so nothing is filtered.
- Assuming a bare `node_modules` pattern recurses the way `.gitignore` does.
- Believing `!` exceptions cannot re-include content under an excluded parent — in Docker they can.
- Thinking `.dockerignore` prevents COPY of a file that a prior published layer already contains.
- Assuming the first matching pattern wins rather than the last.