.dockerignore is the single most underrated file in a Dockerfile setup. Add a reasonable one and your image gets smaller, your builds get faster, your node_modules from the host stops poisoning the container, and secrets in .env don't accidentally end up in a published image. Skip it and COPY . . ships everything in the project directory — typically hundreds of MB of stuff you don't need.
What it does
.dockerignore lives next to your Dockerfile (in the build context root). Before docker build sends the context to the daemon, the daemon applies the ignore rules. Anything matched is excluded from the build context entirely — invisible to COPY and ADD, not transferred over the wire, never in the image.
Same syntax as .gitignore (with small differences noted below).
A reasonable baseline
For a typical web project, this is the starting point:
# Version control
.git
.gitignore
.gitattributes
# Editor / IDE / OS
.vscode
.idea
.DS_Store
Thumbs.db
*.swp
*.swo
# Build artifacts (regenerated in the image)
dist
build
.next
out
.nuxt
*.log
# Dependency directories (regenerated in the image)
node_modules
vendor
.venv
venv
__pycache__
*.pyc
*.pyo
# Test and coverage
coverage
.nyc_output
.pytest_cache
.mypy_cache
.ruff_cache
# Local env (NEVER ship)
.env
.env.local
.env.*.local
# Docker artifacts
Dockerfile
.dockerignore
docker-compose*.yml
# Docs (usually not needed in runtime images)
README.md
LICENSE
CHANGELOG.md
Drop that into .dockerignore at your project root and you've already done 90% of the work.
Per-language additions
Node.js — the standard baseline above plus:
.npmrc
.yarnrc
.pnp.*
Python:
.tox
.eggs
*.egg-info
poetry.lock.bak
.python-version
Go:
*.exe
*.test
*.out
go.sum.bak
PHP / Laravel:
vendor
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
public/storage
Rust:
target
**/*.rs.bk
Why each entry matters
node_modules — biggest single win. Without this, COPY . . copies your host's node_modules (potentially native modules compiled for the wrong OS/architecture) on top of whatever was installed inside the container. Images explode in size and break in subtle ways.
.git — on a project with any history, the .git directory is often 50-500 MB. Useless inside the image; ship without it.
.env — secret database passwords, API keys, OAuth tokens. Should never end up in an image (which can be pushed to a registry and pulled by anyone with access). Pass env vars at runtime via docker run -e or Compose environment: / env_file:.
Build artifacts (dist, build, .next, out) — your image's build stage runs npm run build (or equivalent) and produces these. Whatever is in the host's dist/ from an old build is wrong and confusing. Always rebuild inside the image.
Dockerfile and .dockerignore — the files used to build the image aren't useful inside the image itself. Tiny but ideologically correct to ignore.
Pattern syntax
Same as .gitignore, with the same wildcards:
*.log # any .log file at any directory level
**/*.log # explicit — same as *.log
build/ # a directory called build at the root
**/build/ # build directory at any level
!important.log # negation — re-include something matched earlier
! negation is useful for "ignore everything in this directory except…":
storage/*
!storage/.gitkeep
Order matters. The first matching rule wins.
Inspecting what gets sent
docker build --no-cache --progress=plain -t test . 2>&1 | head -20The first lines of build output show transferring context: with the size. A "transferring context: 1.5GB" message is a strong sign you need a .dockerignore.
For more detail, BuildKit's plain progress reports each step's input size. You can also use docker buildx bake --progress=plain for the same output.
Common pitfalls
- Forgetting to add
node_modules(orvendor,venv,__pycache__). The biggest single contributor to oversize images and slow builds. Even with multi-stage, the build context still copies in slow if it's huge. - Not excluding
.env. Ends up in image history, pushed to a registry, readable by anyone with image-pull access. - Negation patterns getting ignored. Pattern order matters. If you
*.logthen!important.log,important.logis re-included. If you reverse the order,*.logmatches it and the negation is too late. - Trailing slashes.
build/matches only directories namedbuild.buildmatches both files and directories. - Relative paths confusion.
.dockerignoreuses paths relative to the build context, not relative to where you randocker buildfrom. Usually the same, but if youdocker build -f path/to/Dockerfile path/to/contextand don't notice, the ignore rules apply to the context path, not your working directory. - Leading slashes. Don't use them.
/buildandbuildmean different things in.gitignorebut in.dockerignoreonlybuild(no leading slash) matches at the root. Just writebuild/and**/build/if you want both.
Compose-specific
Compose passes a build context to Docker the same way docker build does. The .dockerignore in the build-context root applies. If you have multiple services with different build: contexts, each gets its own .dockerignore next to its Dockerfile.
What to do next
- How to Write a Dockerfile — fundamentals, including the layer-cache patterns.
- Docker Image Size Optimization —
.dockerignoreis the biggest single lever for image size. - Docker Environment Variables — the right way to handle the secrets that should never be in
.dockerignore-excluded files.
FAQ
Sources
Authoritative references this article was fact-checked against.
- .dockerignore — Docker docsdocs.docker.com

