.dockerignore & the Build Context

Docker & DevOps · Lesson 11 of 26 7 min read Updated

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:

Output
=> [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 local node_modules over the one that npm ci just built inside the image. Packages with compiled parts, such as bcrypt, were built for Windows or macOS and crash on Linux.
  • Leaked secrets. A .env file, 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 .git or logs changes the COPY . . 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:

Text
# 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
.dockerignore

The rules are short:

PatternMeaning
node_modulesThe file or folder named node_modules at the root of the context
*.logAny file ending in .log at the root
**/*.logAny .log file in any folder, at any depth
logs/ or logsThe logs folder and everything inside it
!.env.exampleAn exception: include this file even though an earlier line excluded it
# textA 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:

Output
=> [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:

Terminal
docker build -t ferry-api:1.2 .
docker run --rm ferry-api:1.2 ls -a /app
Output
.
..
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.

Text
# Python project
__pycache__
**/*.pyc
.venv
.pytest_cache
.env

# PHP / Laravel project
vendor
storage/logs
storage/framework/cache
.env

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

Text
*
!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.

Terminal
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 .dockerignore next to the Dockerfile when the context is a different folder. It must be in the root of the build context, the folder you pass to docker build.
  • Expecting .gitignore rules to apply. Docker does not read .gitignore. Some patterns behave differently, such as *.log matching 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 .dockerignore excludes it.

Key points

  • The build context is the folder you pass to docker build; COPY can only use files inside it.
  • Without .dockerignore, COPY . . copies local dependencies, Git history, logs and secrets into the image.
  • .dockerignore patterns match from the context root; use **/ for any depth and ! for exceptions.
  • Leave out dependencies the image installs itself, such as node_modules, .venv and vendor.
  • 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
Text
.git
.env
.env.*
!.env.example
vendor
node_modules
storage/logs
**/*.log
tests
Dockerfile*
.dockerignore

vendor 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?

2. What does the pattern **/*.log exclude?

3. Why should node_modules be in .dockerignore for a Node.js image?

4. Your .dockerignore has .env.* on line 3 and !.env.example on line 2. Is .env.example copied into the context?

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