.dockerignore Best Practices That Actually Matter
A bloated build context slows every build, wrecks your cache, and quietly leaks secrets into images. Here's how to keep it lean.
Key takeaways
- A bloated build context slows every build, wrecks your cache, and quietly leaks secrets into images.
- Here's how to keep it lean.
On this page
.dockerignore Best Practices That Actually Matter#
Most people write a .dockerignore once, copy it from an old project, and never look at it again. Then they wonder why their builds take 40 seconds to even start, why the cache never hits, and why docker history shows a 900MB image for a Go binary that should weigh 15MB.
The .dockerignore file is not a nice-to-have. It controls what gets sent to the Docker daemon before a single instruction runs. Get it wrong and you pay for it on every build, in every CI run, on every developer's laptop.
The build context is the whole point#
When you run docker build ., that trailing . is the build context. The CLI tars up everything in that directory and ships it to the daemon. COPY and ADD can only reference files inside that tarball. So the size of the context sets a floor on how slow your build starts.
Run a build in a repo with a fat node_modules and a .git history and you'll see it: Sending build context to daemon 512MB. That transfer happens before Dockerfile line one. On a laptop it's annoying. In CI, with a remote daemon or a slow disk, it's the difference between a 30-second pipeline and a 4-minute one.
The .dockerignore file lives at the root of the context and tells the CLI what to leave out of that tarball. Fewer files sent means a faster start and, just as important, a cache that actually works.
Syntax, and why it is not .gitignore#
The patterns look like .gitignore, and that resemblance trips people up. It uses Go's filepath.Match rules, not the git wildmatch engine. A few things worth knowing:
# comments start with a hash
node_modules
**/*.log
.git
!keep-this.log
node_modulesmatches that directory anywhere it appears in the tree.**matches any number of path segments.**/*.logkills logs at every depth.- A leading
!negates a pattern, re-including something an earlier line excluded. - Trailing slashes and directory-only semantics behave differently from git. Don't assume a
.gitignorecopied verbatim does the same thing.
The negation order matters more than people expect. Rules are evaluated top to bottom, last match wins. If you exclude * and then try to re-include !src/, but a later broad rule re-excludes it, the file drops out again. When a negation isn't working, the fix is almost always moving it below the rule it's fighting.
What to always exclude#
Some entries earn their place in nearly every project:
.gitand.gitignore— history and metadata the image never needs..gitalone is often the biggest single thing in the context.node_modules,vendor,venv,__pycache__— dependency and build artifacts you rebuild inside the image anyway.dist,build,target,*.o,*.pyc— local compiler output that would clash with what the build produces fresh..env,*.pem,*.key,secrets/,credentials.json— the ones that keep you up at night. More on this below..vscode,.idea,.DS_Store,*.swp— editor and OS junk.README.md,docs/,*.md, test fixtures, large sample data — anything the runtime doesn't touch.Dockerfile,docker-compose.yml,.github/— CI and build config the image itself has no use for.
The security angle is the one that bites#
Here's the failure mode. You write COPY . . in your Dockerfile because it's easy. Meanwhile there's a .env with a database password sitting in the repo root because that's how local dev works. Now that password is baked into an image layer, and it stays there even if a later layer deletes the file. Anyone who pulls the image runs docker history or unpacks the layers and reads it straight out.
A .dockerignore that lists .env, *.pem, and secrets/ is a real control here, not hygiene theater. It means a careless COPY . . can't scoop up a credential that never should have left the developer's machine. Pair it with build secrets (--mount=type=secret) for anything the build genuinely needs, and never let a plaintext secret ride along in the context.
Treat this as belt and suspenders. The ignore file stops the accident; scoped COPY and build secrets handle the intentional cases. If you're already fighting build failures, a lean context also makes them easier to reason about — our Docker troubleshooting guide walks through the common ones.
Cache and multi-stage builds#
Every COPY invalidates cache when the copied files change. If your context includes churny junk — logs, local build output, an editor's scratch files — you get cache misses on COPY . . for changes that have nothing to do with your actual code. Excluding that noise means the cache holds until real source changes.
Multi-stage builds don't get you out of this. The .dockerignore applies to the whole build, every stage. A builder stage that does COPY . . to compile still pulls the full unfiltered context if you skipped the ignore file. The clean pattern is to copy narrowly first for dependency resolution, then copy source:
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
That first COPY only busts cache when dependencies change, so npm ci stays cached across ordinary code edits. But it only works if node_modules is ignored — otherwise the local one clobbers what npm ci installed.
The mistake that's worse than a fat context#
Excluding something the build actually needs. Someone adds *.md to trim the context, and a Go build that embeds a README.md via //go:embed breaks with a file-not-found that makes no sense until you check the ignore file. Or a broad config/ exclusion drops a config the app reads at startup.
The negation fix pattern is worth memorizing. Exclude a directory, then re-include the one file you need:
config/
!config/production.yaml
Order matters. Put the ! line after the exclusion or it does nothing.
A starter .dockerignore#
Solid default for a Node, Python, or Go service. Trim what doesn't apply:
# VCS
.git
.gitignore
.gitattributes
# Dependencies / build output
node_modules
vendor
venv
.venv
__pycache__
*.pyc
dist
build
target
*.o
# Secrets and env
.env
.env.*
*.pem
*.key
secrets/
credentials.json
# Editor / OS
.vscode
.idea
.DS_Store
*.swp
# Docs, tests, CI
*.md
docs/
.github/
Dockerfile*
docker-compose*.yml
.dockerignore
# Logs and local data
*.log
tmp/
testdata/
The call we'd make#
Write the .dockerignore first, before the Dockerfile, and treat .git, dependencies, and secrets as non-negotiable exclusions. Then verify: run a build, watch the context size the daemon reports, and unpack a layer or two to confirm nothing sensitive rode along. Two minutes of checking beats finding a leaked key in a published image. A lean context isn't a micro-optimization — it's the cheapest speed and safety win in the whole Docker workflow.
Get the DevOps Troubleshooting Cheat Sheet
Subscribe and get our free one-page reference for the errors that eat an afternoon — CrashLoopBackOff, OOMKilled, Terraform state locks, and more — plus new guides as we publish them.
GitLab CI/CD Best Practices in 2026: Pipelines You Can Trust
A production-focused GitLab CI/CD guide: include/extends templates, rules and workflow, needs DAGs, cache vs artifacts, protected environments, masked/protected variables and Vault, built-in security scanning, and review apps — with copy-paste examples.
Ansible vs Terraform — Configuration vs Provisioning
Terraform provisions infrastructure and Ansible configures machines, so pitting them against each other is the wrong question to ask.
More from DevOps
Explore more articles in this category
Best Kubernetes IDE and GUI Tools in 2026
kubectl is fine until you're juggling five namespaces across three clusters. These are the tools that make that manageable, compared.
Chef vs Puppet vs Ansible: Configuration Management in 2026
One is agentless and Python-based, the other two run a persistent agent and a domain-specific language. The architecture difference matters more than the syntax.
PagerDuty vs Opsgenie: Choosing an Incident Alerting Tool
Both page the right person at 3am and both integrate with everything. The real differences show up in pricing structure, workflow depth, and who already owns the ecosystem around you.
You might have missed
Evergreen posts worth revisiting.