.dockerignore Generator
Build a .dockerignore for Node.js, Python, Java, Go, Ruby, PHP, .NET or Rust (or several at once), then test real paths against it with the same matching rules Docker uses.
Pick more than one for a repo that holds, say, a Node.js frontend and a Python API.
Lines go in as written, comments included. Each line is checked against Docker's rules as you type.
# .dockerignore generated by Workshelve # Paths are relative to the build context root. A name with no slash # (node_modules) only matches at the root; **/ matches in any folder. # The last line that matches a path decides whether it is sent. # Secrets and credentials **/.env **/.env.* !**/.env.example !**/.env.sample **/*.pem **/*.key **/*.p12 **/*.pfx **/id_rsa* **/id_ed25519* **/.netrc **/secrets.json **/secrets.yml **/secrets.yaml **/*.tfstate **/*.tfstate.* .aws .ssh # Node.js **/node_modules .npm .pnpm-store **/.turbo .eslintcache .parcel-cache # Build output (rebuilt inside the image) dist build out .next .nuxt .output .svelte-kit # Version control .git .gitignore .gitattributes .gitmodules .svn .hg # OS files **/.DS_Store **/._* **/Thumbs.db **/desktop.ini .Spotlight-V100 .Trashes # Logs **/*.log **/npm-debug.log* **/yarn-debug.log* **/yarn-error.log* **/pnpm-debug.log* logs # IDE & editor files .vscode .idea .vs **/*.iml **/*.swp **/*.swo **/*~ .project .classpath .settings # Caches & coverage coverage .coverage htmlcov **/.nyc_output **/.cache
4 of 8 paths excluded, 4 sent to the builder
| Path | Result | Decided by |
|---|---|---|
| node_modules/express/package.json | Excluded | line 27: **/node_modules (via folder node_modules) |
| packages/web/node_modules/esbuild/bin/esbuild | Excluded | line 27: **/node_modules (via folder packages/web/node_modules) |
| packages/api/.env | Excluded | line 7: **/.env |
| packages/api/.env.example | Sent | line 9: !**/.env.example |
| packages/api/logs/app.log | Excluded | line 60: **/*.log |
| packages/web/dist/index.js | Sent | no line matches |
| src/__pycache__/main.cpython-312.pyc | Sent | no line matches |
| src/index.ts | Sent | no line matches |
Matching follows Docker's own pattern matcher (moby/patternmatcher): root-relative paths, parent-folder matches, **, and last-match-wins for !. Paste a find listing or BuildKit log lines; the #8 0.312 prefixes are stripped.
How Docker matches .dockerignore patterns
When you run docker build, the client reads .dockerignore from the root of the build context and drops every matching file before anything is sent to the builder. Each line is cleaned first: surrounding spaces are trimmed, . and .. are resolved, and leading and trailing slashes are removed. So /foo/bar/, foo/bar/ and foo/bar are the same rule.
A pattern is matched against the whole path from the context root, using Go's filepath.Match wildcards plus ** for any number of folders, including none. If the pattern matches a parent folder, everything inside that folder matches too. When several lines match one path, the last one wins. A # starts a comment only in column 1.
| Pattern | Matches | Does not match |
|---|---|---|
| node_modules | node_modules/express/index.js | packages/web/node_modules/… |
| **/node_modules | packages/web/node_modules/… | packages/my_node_modules/… |
| *.log | npm-debug.log | api/logs/app.log |
| **/*.log | api/logs/app.log | app.log.gz |
| */temp* | somedir/temporary.txt | somedir/subdir/temporary.txt |
| temp? | tempa | temp, tempab |
| foo/** | foo/x, foo/x/y | foo itself |
| **/*.py[cod] | a/b/x.pyc | x.py |
| [^a]bc | xbc | abc |
| [!a]bc | !bc, abc | xbc |
The last two rows show a common surprise. Go reads [^…] as "not these characters", so [!a] is just a set containing ! and a. The tester above uses the same rules, so you can paste a path and see which line decided it.
.dockerignore vs .gitignore
The two files look alike and are often copied from one to the other, but they don't read patterns the same way. Lines that work in Git can quietly do less in Docker.
| Behaviour | .gitignore | .dockerignore |
|---|---|---|
| Name with no slash, e.g. node_modules | Matches in every folder | Matches at the context root only |
| Trailing slash, e.g. build/ | Folders only | Removed; matches files and folders |
| Pattern with a slash in the middle | Relative to that .gitignore | Relative to the context root |
| Re-include a file inside an excluded folder | Not possible | Works: last matching line wins |
| [!a] in a character class | Not a | ! or a (use [^a]) |
| Files per project | One per folder, all applied together | One per build (root or per-Dockerfile) |
The first row causes most of the trouble. Prefix anything that can appear in a subfolder with **/ and the rule behaves the way it did in Git.
Example: a monorepo file copied from .gitignore
A workspace repo has a web app and an API under packages/. Someone copied four lines from .gitignore into .dockerignore. The Dockerfile installs dependencies and then runs COPY . . to add the source:
node_modules/ dist/ *.log .env
Running the repo's paths through Docker's rules (press Load example above to see this in the tester):
| Path | Copied from .gitignore | With **/ rules |
|---|---|---|
| node_modules/express/package.json | Excluded | Excluded |
| packages/web/node_modules/esbuild/bin/esbuild | Sent | Excluded |
| packages/api/.env | Sent | Excluded |
| packages/api/logs/app.log | Sent | Excluded |
| packages/web/dist/index.js | Sent | Sent (add **/dist) |
Two things go wrong. The API's .env is copied into the image, where anyone who can pull it can read it. And the web package's node_modules, built on a Mac, is copied over the Linux install. Packages with native binaries, such as esbuild or sharp, then fail with errors saying they were installed for a different platform.
The fix is to make the rules match at any depth: **/node_modules, **/*.log, **/.env, plus **/dist if each package keeps its own build output. The generator writes those **/ forms for you.
Allowlist: exclude everything, then add back
A denylist only catches what you thought of. The other approach starts with *, which excludes every top-level entry, and adds back only what the Dockerfile copies. New files then stay out of the build until you list them.
* !package.json !pnpm-lock.yaml !src **/.env **/.env.*
Order matters. The secret rules come after !src, so src/.env is still excluded. If they came first, the !src line would win and send it. The price of an allowlist is maintenance: when the Dockerfile starts copying a new file, the build fails with failed to compute cache key and a "not found" message until you add that file back. Choose "Exclude everything, add back" above to start from your stack's usual files.
Where Docker looks for the file
Docker reads .dockerignore from the root of the build context, which is the path at the end of the command. That is not necessarily the Dockerfile's folder. With several Dockerfiles, you can also put an ignore file next to each one, named after it. That file takes precedence over the root .dockerignore.
| Command | Context root | Ignore file used |
|---|---|---|
| docker build . | . | ./.dockerignore |
| docker build -f docker/api.Dockerfile . | . | docker/api.Dockerfile.dockerignore if present, else ./.dockerignore |
| docker build ./api | ./api | ./api/.dockerignore |
| Compose: build: { context: ./api } | ./api | ./api/.dockerignore (same rules as docker build) |
| docker build - < Dockerfile | none | No context is sent, so nothing to ignore |
Patterns in a per-Dockerfile ignore file are still written relative to the context root, not to the Dockerfile's folder.
What each stack excludes, and what to check
Most stack rules are safe in any project. The build output rules are the exception. They assume the image compiles the code itself, and many Dockerfiles copy a build made earlier instead.
| Stack | Always excluded | Build output option: check first |
|---|---|---|
| Node.js | node_modules in every folder, .turbo, .eslintcache | dist/, build/, .next/ if you copy a build made on the host or in CI |
| Python | __pycache__ and .pyc anywhere, .venv, test and lint caches | dist/ if you COPY dist/*.whl and pip install it |
| Java | .gradle, .kotlin | target/, build/ if you COPY target/*.jar |
| Go | test binaries, coverage.out | bin/, dist/ if you copy a host-built binary. vendor/ is a separate option, off by default, because Go 1.14+ builds from it |
| Ruby | .bundle, vendor/bundle, log/*, tmp/*, storage/* (keeping .keep files) | public/assets if assets are precompiled outside Docker |
| PHP | vendor, PHPUnit and PHP CS Fixer caches | public/build if Vite assets are built outside Docker |
| .NET | *.user, *.suo, TestResults, packages | bin/ and obj/ in every project; turn off if you COPY a dotnet publish folder from the host |
| Rust | *.rs.bk backups | target/ if you COPY a binary built on the host |
The Ruby rules use log/* followed by !log/.keep rather than plain log. The folder still reaches the image with its placeholder file and without the logs, which is the same approach Rails' own generated .dockerignore takes.
Seeing what is in your build context
Every build reports the size of the context it sent, in a line like transferring context: 13.16MB. If that number is in the hundreds of megabytes for a small app, something large is getting through. To list the files themselves, build a throwaway image from the same context:
docker build --no-cache --progress=plain -f- . <<'EOF' FROM busybox COPY . /ctx RUN find /ctx -type f | sed 's|^/ctx/||' | sort EOF
The file list appears in the build log. Paste it into the tester above, which strips the #8 0.312 prefixes, and each path shows which line, if any, would have excluded it. Because the Dockerfile comes from stdin, this check uses the root .dockerignore, not a per-Dockerfile one.
Secrets, COPY . . and image layers
COPY . . copies whatever the context contains, so a missing ignore rule ends up as a file in the image. Deleting it in a later RUN rm doesn't help, because the earlier layer still holds it and anyone with the image can extract it. The fix has to be in .dockerignore, before the file is ever sent.
If the build needs a credential, such as an .npmrc token for a private registry, don't copy it. Mount it for a single step instead:
# Dockerfile RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci # build command docker build --secret id=npmrc,src=$HOME/.npmrc .
The secret is available while that RUN step runs and is never written to a layer. Keep **/.npmrc in .dockerignore so the copy on disk can't slip in through COPY . ..
Frequently asked questions
Why is node_modules still in my build context?
A line like node_modules or node_modules/ only matches the folder at the context root. Nested copies such as packages/web/node_modules need **/node_modules. Also check that you edited the file at the context root, and that no <Dockerfile>.dockerignore next to your Dockerfile is overriding it.
Does the trailing slash in build/ do anything?
No. Docker removes leading and trailing slashes before matching, so build/ also matches a file called build. In .gitignore the slash limits the rule to folders; in .dockerignore it has no effect.
Can I keep one file from a folder I excluded?
Yes, unlike Git. With docs on one line and !docs/openapi.yaml after it, Docker sends that one file, because the last matching line decides each path.
Should I exclude the Dockerfile and .dockerignore?
It is optional. The builder still receives both because it needs them, but excluding them stops COPY . . from putting them in the image.
Does .dockerignore affect docker run -v or volumes?
No. It only filters the build context. A bind mount at run time shows the host folder exactly as it is, node_modules and .env included.
Why does my build fail with "syntax error in pattern"?
Docker rejects lines it cannot compile, usually a [ with no closing ], a range like [a-], or a lone ! with nothing after it. The checker under "Your patterns" flags these lines.
Sources
- Docker Docs: Build context, .dockerignore files: file location, Dockerfile-specific ignore files, matching rules and ! exceptions
- moby/patternmatcher: the matcher Docker uses; this tool ports its pattern compiler and parent-folder matching
- Go path/filepath: Match and Clean: wildcard syntax, [^…] classes and pattern cleaning
- Git documentation: gitignore: the .gitignore rules compared above
- Docker Docs: Build secrets: RUN --mount=type=secret and docker build --secret
- Go Modules Reference: Vendoring: when the go command builds from vendor/ by default