A bloated build context slows every build, wrecks your cache, and quietly leaks secrets into images. Here's how to keep it lean.
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.
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.
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_modules matches that directory anywhere it appears in the tree.** matches any number of path segments. **/*.log kills logs at every depth.! negates a pattern, re-including something an earlier line excluded..gitignore copied 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.
Some entries earn their place in nearly every project:
.git and .gitignore — history and metadata the image never needs. .git alone 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.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.
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.
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.
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/
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 latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
One bundles your whole toolchain, the other lets you build anything from parts. The right pick depends on how much glue you want to own.
GitHub Actions OIDC gets all the attention, but GitLab, Buildkite, and CircleCI issue the same signed tokens. Here's how to trust them without opening a hole.
Explore more articles in this category
The IaC landscape fractured after the Terraform license change. This is the map to what each tool is actually best at, and how to choose without regret.
Terragrunt keeps large Terraform setups DRY and orchestrated, but small teams often pay its learning curve for little return.
A practical comparison of Kubernetes-native continuous reconciliation against the classic CLI-driven, state-file IaC model for platform teams.
Evergreen posts worth revisiting.