Skip to content

.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.

Project stacks

Pick more than one for a repo that holds, say, a Node.js frontend and a Python API.

File style
What to exclude

Lines go in as written, comments included. Each line is checked against Docker's rules as you type.

.dockerignore · 84 lines
# .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

Which test paths the .dockerignore excludes
PathResultDecided by
node_modules/express/package.jsonExcludedline 27: **/node_modules (via folder node_modules)
packages/web/node_modules/esbuild/bin/esbuildExcludedline 27: **/node_modules (via folder packages/web/node_modules)
packages/api/.envExcludedline 7: **/.env
packages/api/.env.exampleSentline 9: !**/.env.example
packages/api/logs/app.logExcludedline 60: **/*.log
packages/web/dist/index.jsSentno line matches
src/__pycache__/main.cpython-312.pycSentno line matches
src/index.tsSentno 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.

What common .dockerignore patterns match
PatternMatchesDoes not match
node_modulesnode_modules/express/index.jspackages/web/node_modules/…
**/node_modulespackages/web/node_modules/…packages/my_node_modules/…
*.lognpm-debug.logapi/logs/app.log
**/*.logapi/logs/app.logapp.log.gz
*/temp*somedir/temporary.txtsomedir/subdir/temporary.txt
temp?tempatemp, tempab
foo/**foo/x, foo/x/yfoo itself
**/*.py[cod]a/b/x.pycx.py
[^a]bcxbcabc
[!a]bc!bc, abcxbc

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.

Differences between .gitignore and .dockerignore matching
Behaviour.gitignore.dockerignore
Name with no slash, e.g. node_modulesMatches in every folderMatches at the context root only
Trailing slash, e.g. build/Folders onlyRemoved; matches files and folders
Pattern with a slash in the middleRelative to that .gitignoreRelative to the context root
Re-include a file inside an excluded folderNot possibleWorks: last matching line wins
[!a] in a character classNot a! or a (use [^a])
Files per projectOne per folder, all applied togetherOne 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):

Monorepo paths against the copied file and the fixed file
PathCopied from .gitignoreWith **/ rules
node_modules/express/package.jsonExcludedExcluded
packages/web/node_modules/esbuild/bin/esbuildSentExcluded
packages/api/.envSentExcluded
packages/api/logs/app.logSentExcluded
packages/web/dist/index.jsSentSent (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.

Which ignore file each build command reads
CommandContext rootIgnore 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 - < DockerfilenoneNo 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.

Default exclusions per stack and the ones to check
StackAlways excludedBuild output option: check first
Node.jsnode_modules in every folder, .turbo, .eslintcachedist/, build/, .next/ if you copy a build made on the host or in CI
Python__pycache__ and .pyc anywhere, .venv, test and lint cachesdist/ if you COPY dist/*.whl and pip install it
Java.gradle, .kotlintarget/, build/ if you COPY target/*.jar
Gotest binaries, coverage.outbin/, 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
PHPvendor, PHPUnit and PHP CS Fixer cachespublic/build if Vite assets are built outside Docker
.NET*.user, *.suo, TestResults, packagesbin/ and obj/ in every project; turn off if you COPY a dotnet publish folder from the host
Rust*.rs.bk backupstarget/ 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