.dockerignore & the Build Context
When you run docker build ., that final dot does more than you might think. Docker packs up the folder and hands it to the builder as the build context. Anything in that folder can end up in your image through COPY . .: your local node_modules, the whole Git history, log files, and even the .env file with your database password.
A small text file called .dockerignore controls what goes into the context. In this lesson you will see the problems it prevents and learn to write one for any project.
What the build context is
The builder (BuildKit) may run in a different place from your files: inside the Docker Desktop VM, on a remote server or in a cloud build service. So it cannot simply read your disk. Instead, the client sends the files it needs from the context folder. COPY and ADD can only see files inside the context; a path like COPY ../secrets.txt . fails, because the parent folder is not part of it.
BuildKit is smart: it only transfers files that COPY and ADD actually use, and it caches them between builds. But COPY . . uses everything. Look at the build output line transferring context. In Isha's Node.js project without a .dockerignore, it says:
=> [internal] load build context 6.8s => => transferring context: 286.43MB 6.7s
Her code is only a few kilobytes. The rest is node_modules (installed on Windows), the .git folder and an old logs folder. The upload alone takes almost seven seconds, every time the files change.
Problems a missing .dockerignore causes
- Slow builds. Hundreds of megabytes are sent and copied for no reason.
- Broken apps.
COPY . .copies your localnode_modulesover the one thatnpm cijust built inside the image. Packages with compiled parts, such asbcrypt, were built for Windows or macOS and crash on Linux. - Leaked secrets. A
.envfile, an SSH key or a cloud credentials file copied into an image can be read by anyone who pulls the image, even if a later step deletes it. - Broken cache. Any change in
.gitorlogschanges theCOPY . .layer, so it re-runs even when your code did not change.
Writing a .dockerignore file
Create .dockerignore in the root of the build context (normally next to the Dockerfile). Each line is a pattern for files or folders to leave out:
# Dependencies are installed inside the image
node_modules
npm-debug.log*
# Version control and editor files
.git
.gitignore
.vscode
.idea
# Secrets and local settings
.env
.env.*
!.env.example
*.pem
# Build output, logs and test reports
dist
coverage
logs
*.log
# Docker files themselves
Dockerfile*
compose*.yaml
.dockerignoreThe rules are short:
| Pattern | Meaning |
|---|---|
node_modules | The file or folder named node_modules at the root of the context |
*.log | Any file ending in .log at the root |
**/*.log | Any .log file in any folder, at any depth |
logs/ or logs | The logs folder and everything inside it |
!.env.example | An exception: include this file even though an earlier line excluded it |
# text | A comment |
Patterns are matched from the root of the context. That surprises people who know .gitignore, where *.log matches in every folder. In .dockerignore, write **/*.log for that. Order matters for exceptions: a ! line must come after the line it overrides.
Excluding Dockerfile and .dockerignore is safe. Docker still reads them to run the build; it just does not copy them into the image.
Checking what is inside the image
After adding the file, the context shrinks:
=> [internal] load build context 0.0s => => transferring context: 14.92kB 0.0s
You can check what COPY . . really put into the image by listing the app folder in a throwaway container:
docker build -t ferry-api:1.2 .
docker run --rm ferry-api:1.2 ls -a /app. .. node_modules package-lock.json package.json server.js
No .git, no .env, no logs. The node_modules you see is the one npm ci created inside the image, built for Linux.
Patterns for other languages
The idea is always the same: leave out dependencies the image installs itself, version control, secrets, caches and build output.
# Python project
__pycache__
**/*.pyc
.venv
.pytest_cache
.env
# PHP / Laravel project
vendor
storage/logs
storage/framework/cache
.envA Python virtual environment (.venv) or a PHP vendor folder from your laptop has the same problem as node_modules: it belongs to your computer, not to the image.
Advanced: allow-list style and per-Dockerfile ignore files
For projects with many folders, some teams flip the logic: ignore everything, then allow only what the image needs. Nothing new can sneak in by accident.
*
!package.json
!package-lock.json
!server.js
!src/If one project has several Dockerfiles, for example Dockerfile and Dockerfile.dev, BuildKit looks for an ignore file named after the Dockerfile first: Dockerfile.dev.dockerignore is used when you build with -f Dockerfile.dev. If it does not exist, the normal .dockerignore is used.
docker build -f Dockerfile.dev -t ferry-api:dev .
docker build --progress=plain --no-cache -t ferry-api:check . 2>&1 | grep "transferring context"The first command builds with the development Dockerfile (and its own ignore file, if there is one). The second prints BuildKit's plain-text log instead of the live display and filters it, a quick way to check the context size in a script or CI job.
Common mistakes
- Putting
.dockerignorenext to the Dockerfile when the context is a different folder. It must be in the root of the build context, the folder you pass todocker build. - Expecting
.gitignorerules to apply. Docker does not read.gitignore. Some patterns behave differently, such as*.logmatching only at the root. - Putting
!exceptions before the exclusion. Later lines win, so the exception must come after. - Ignoring a file the Dockerfile needs. If
COPY package-lock.json .fails with "not found", check whether.dockerignoreexcludes it.
Key points
- The build context is the folder you pass to
docker build;COPYcan only use files inside it. - Without
.dockerignore,COPY . .copies local dependencies, Git history, logs and secrets into the image. .dockerignorepatterns match from the context root; use**/for any depth and!for exceptions.- Leave out dependencies the image installs itself, such as
node_modules,.venvandvendor. - Check the result with
docker run --rm IMAGE ls -a /app, and use build secrets for anything sensitive.
Exercise
Ananya's Laravel project folder contains app, public, routes, vendor, node_modules, storage/logs, .git, .env, .env.example, composer.json, composer.lock, artisan and a tests folder. Her Dockerfile runs composer install and copies the code. Write a .dockerignore that keeps secrets, local dependencies, logs and Git history out of the image, but keeps .env.example.
Show solution
.git
.env
.env.*
!.env.example
vendor
node_modules
storage/logs
**/*.log
tests
Dockerfile*
.dockerignorevendor is rebuilt by composer install inside the image, so the local copy is not needed. The !.env.example exception comes after .env.*, so the example file is still copied. Leaving out tests is optional; keep them if you run tests inside the image.
Quick Quiz
1. Where must the .dockerignore file be placed?
Docker reads it from the root of the context you pass to docker build.
2. What does the pattern **/*.log exclude?
** matches any number of folders.
3. Why should node_modules be in .dockerignore for a Node.js image?
Local packages may be built for Windows or macOS and slow down the build.
4. Your .dockerignore has .env.* on line 3 and !.env.example on line 2. Is .env.example copied into the context?
Later lines win, so an exception must come after the pattern it overrides.
Learn Docker & DevOps with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
