How Docker decides, in order
The rules are shorter than Git's, and that is exactly why a .gitignore habit gives the wrong
answer here.
- Every pattern is anchored at the context root.
node_modulesmeans the one directory at the top of the context, not “any directory with that name”. Use**/node_moduleswhen you mean every one of them. - Each path is judged on its own, and the last rule with a say wins. A rule has a say when it matches the path itself or any directory above it.
- A
!line puts a path back — even one inside an excluded directory. Git refuses to do this; Docker does it happily, which is what makes*followed by!dista working allowlist. - Leading and trailing slashes are dropped before matching, and
.and..segments are resolved, so/src/andsrcare the same rule. - A pattern Docker cannot parse stops the build. It is not skipped:
docker buildexits withsyntax error in patternbefore anything is sent.
The same file, read as .dockerignore and as .gitignore
Both columns are produced by running this page's engine and our gitignore tester's engine over the same text, and both are checked by a test — so this table cannot drift from either behaviour.
| File | Path | Docker | Git | Why they differ |
|---|---|---|---|---|
| node_modules | packages/ui/node_modules/react/index.js | In the context — no rule matched | Ignored — line 1 | A pattern with no slash is anchored at the context root for Docker and matches at any depth for Git. |
| docs !docs/keep.md | docs/keep.md | In the context — line 2 | Ignored — line 1 | Git stops at the first excluded parent directory and never looks inside, so line 2 is not even read. Docker has no such rule. |
| * !src | src/main.ts | In the context — line 2 | Ignored — line 1 | The common “allowlist” idiom works as written for Docker. For Git you need /* and !/src, because * there matches at every level. |
| build/ | build | Excluded — line 1 | Not ignored — no rule matched | Here build is a file, not a directory. Git honours the trailing slash as “directories only”; Docker drops it, so the file goes too. |
Pattern syntax
Every row is checked by a test against the same engine the tool above uses, so this table cannot drift from the behaviour either.
| Pattern | Meaning | Excludes | Leaves in the context |
|---|---|---|---|
node_modules | A bare name is anchored at the context root — this one directory and everything under it. Nothing deeper is touched, which is the difference from .gitignore that costs people the most time. | node_modules, node_modules/react/index.js | packages/ui/node_modules/react/index.js, my_node_modules |
**/node_modules | ** stands for any number of directories, including none. This is the pattern that reaches every node_modules in a monorepo. | node_modules/react/index.js, packages/ui/node_modules/react/index.js | my_node_modules |
*.log | * matches any run of characters except a slash, so this is only the logs sitting at the context root. | app.log | logs/app.log |
**/*.log | The same pattern at any depth, root included. | app.log, logs/a/b.log | app.log.txt |
*/temp* | A slash in the middle counts directories exactly: this is one level down, no more and no less. | somedir/temporary.txt | temp.txt, a/b/temp.txt |
temp? | ? is exactly one character, and never a slash. | tempa, tempb | temp, sub/tempa |
/src | A leading slash is simply dropped — every pattern is anchored at the context root already, so /src and src are the same rule. | src, src/main.ts | a/src/main.ts |
build/ | A trailing slash is dropped too. Docker has no directory-only patterns, so this also excludes a plain file called build — where .gitignore would leave it alone. | build, build/out.o | buildfile |
[abc].log | A bracket expression matches one character from the set; ranges like [a-c] work, and [^a] negates. | a.log, c.log | d.log, ab.log, sub/a.log |
foo/../bar | Patterns are cleaned before use: . and .. segments and repeated slashes are resolved, so this is just bar. | bar | foo/bar |
Results that surprise people
Each of these is the verdict this tool gives, verified by a test. Paste any of them above to see the deciding line.
| .dockerignore | Path | Result | Why |
|---|---|---|---|
| node_modules | packages/ui/node_modules/react/index.js | Sent to the builder — no rule matched | This is the answer to “why is node_modules still in my build context?” in a monorepo. Write **/node_modules to reach every one of them. |
| docs !docs/keep.md | docs/keep.md | Sent to the builder — line 2: docs/keep.md | Docker evaluates each path on its own and takes the last rule with a say, so line 2 wins. The identical .gitignore leaves the file ignored, because Git never descends into an ignored directory. |
| * !dist !package.json | dist/index.js | Sent to the builder — line 2: dist | !dist puts back the directory, and that is enough — everything under it comes with it. This idiom does not work in a .gitignore. |
| *.md !README*.md README-secret.md | README-secret.md | Excluded — line 3: README-secret.md | Last match wins, so a later plain rule overrides an earlier !. Order is the whole grammar here. |
| *.log | logs/app.log | Sent to the builder — no rule matched | Write **/*.log if you meant every log file below the context root. |
| a[.txt | a.txt | No answer — the build stops at line 1 | Docker validates every pattern before it starts, and refuses the build with “syntax error in pattern” rather than skipping the line. Here the character class opened by [ is never closed. |
Three things the patterns do not decide
- The Dockerfile and the ignore file reach the builder anyway. If your patterns exclude
.dockerignoreor the Dockerfile you are building, the CLI appends!.dockerignoreand!<dockerfile>to the exclusions, because the API needs them; the daemon then leaves them out of the final context. This tool answers the pattern question, so it will report them excluded. (TrimBuildFilesFromExcludesindocker/cliv29.8.1.) - A Dockerfile can have its own ignore file. With BuildKit, if
<dockerfile-name>.dockerignoreexists —Dockerfile.dev.dockerignorenext toDockerfile.dev— it is used instead of.dockerignore, not in addition to it. That catches people who edit the wrong file. (frontend/dockeruiinmoby/buildkitv0.33.0.) - Excluding a path does not make a
COPYof it fail loudly. The file simply is not there. BuildKit's linter has a rule for exactly this case,CopyIgnoredFile: “Attempting to Copy file that is excluded by .dockerignore”.
There is no docker check-ignore
Git has git check-ignore -v, which names the deciding line. Docker has no equivalent, which is
why this page exists. The usual workarounds all need a running daemon and none of them names the rule: build
a throwaway image that copies the whole context and list it, or read the “transferring context” size from
docker build --progress=plain. The rsync --exclude-from trick that circulates is
worse than it looks — rsync's filter dialect is not Docker's, and it disagrees on precisely the anchoring
cases above.
About this tool
The matching is a TypeScript port of github.com/moby/patternmatcher v0.6.1 and its
ignorefile parser — the packages the Docker CLI and BuildKit use to filter a build context, so
their answer is Docker's answer. It is pinned by 1,315 recorded path answers over 417
.dockerignore files, generated by running those upstream packages directly, and the
port is checked against them on every build.
Two limits worth stating: Go compares characters, JavaScript compares UTF-16 code units, so a character
class you write yourself ([…]) counts a non-BMP character such as an emoji as two where Docker
counts one; and Go's \s is narrower than JavaScript's, which can only matter inside a pattern
containing a literal \s. Ordinary paths are unaffected. Everything runs in your browser — the
files and paths you paste are never uploaded.
Includes a derivative work of moby/patternmatcher,
Copyright 2012–2017 Docker, Inc., licensed under the Apache License, Version 2.0. The upstream licence and
NOTICE are kept in third-party/moby-patternmatcher/ in this project's repository.