Docker 19 🐳 Docker Build Context Optimization and .dockerignore Best Practices
The build context is the set of files Docker sends to the daemon before the build begins. When you run docker build ., the . is not just a path to the Dockerfile; it is the entire directory tree that Docker packages into a tar archive and transfers to the daemon over the Unix socket . The daemon unpacks that archive into a temporary directory and then executes the Dockerfile instructions against it. Every file in the context is transferred, whether the Dockerfile references it or not. A project with node_modules, .git, and build artifacts can send hundreds of megabytes on every build, and any change to a transferred file can invalidate the build cache for COPY . . instructions .
The .dockerignore file is the mechanism that controls what enters the build context. Its syntax mirrors .gitignore, and the recommended contents are similar: exclude version control history, installed dependencies, build outputs, test artifacts, secrets, editor configuration, and OS metadata . The result is a build context that contains only the source code and configuration files the build actually needs, typically a few megabytes instead of hundreds . This chapter covers the build context, the .dockerignore file, its syntax, the interaction with the build cache, and the patterns that keep builds fast and images small.
Key point: The build context is everything Docker sends to the daemon before the build starts. .dockerignore excludes files from the context. A small context transfers faster and prevents spurious cache invalidation. Use * as the first pattern followed by ! exceptions for a default-deny approach. The file goes in the same directory as the Dockerfile.
Why build context optimization matters
The transfer problem. Every build sends the entire context to the daemon, even if the Dockerfile only copies a few files . On a local machine, the transfer is over a Unix socket and is fast. In CI, the build runs on a remote runner and the context is transferred over the network. A 500 MB context that should be 5 MB wastes time and bandwidth on every build .
The cache invalidation problem. Docker computes a cache checksum from the metadata of files referenced by COPY and ADD instructions . When you use COPY . ., every file in the context contributes to the checksum. Editing a README, switching a git branch, or running tests locally changes files that the build does not need but that still invalidate the cache for the COPY . . layer . The entire build after that layer is rebuilt.
The secret leakage problem. The build context includes everything in the directory, including .env files, credential files, and the .git directory . If the Dockerfile uses COPY . ., those files can end up in an image layer. Deleting a secret in the latest commit does not remove it from .git history, and copying .git into an image ships that history to anyone who can pull the image .
The reproducibility problem. A build that includes local artifacts, caches, and generated files can produce different images on different machines. A .dockerignore that excludes those files makes the context deterministic, so the same source produces the same image .
The recursive context problem. A Dockerfile in the context can be copied into the image by a broad COPY . ., which creates a recursive context and can include the Dockerfile itself in the build output. Excluding Dockerfile*, docker-compose*, and .dockerignore prevents this .
a. The build context
The build context is the directory tree that Docker sends to the daemon. The path you pass to docker build is the context root.
docker build .
# Sending build context to Docker daemon 847.3MB
The first line of output shows the context size. An 847 MB context from a Node.js project with node_modules is common when no .dockerignore is present . The context is transferred before the Dockerfile executes, so even a Dockerfile that only copies package.json sends the entire directory.
The context root is the working directory for the build. The COPY and ADD instructions can only access files within the context. A path like COPY ../other-dir/file . fails because .. is outside the context .
The daemon does not see files outside the context. The context is a security boundary and a performance boundary. A smaller context is faster to transfer and less likely to include files that should not be in the image.
b. The .dockerignore file
The .dockerignore file goes in the root of the build context, alongside the Dockerfile. It contains patterns that exclude files and directories from the context .
# .dockerignore
.git
node_modules
dist
.env
.env.*
*.log
The patterns are matched against paths relative to the context root. A line that starts with # is a comment. Blank lines are ignored .
The syntax is similar to .gitignore but with one important difference: .dockerignore supports ** for matching any number of directories, including zero. The pattern **/*.go excludes all Go files in all directories, including the root .
Exclusions are applied before the context is transferred. Files that match a pattern in .dockerignore are not sent to the daemon, and they cannot be copied by COPY or ADD .
c. Pattern syntax
The patterns use Go’s filepath.Match rules plus the ** wildcard .
| Pattern | Matches |
|---|---|
node_modules | The node_modules directory at the root |
**/node_modules | node_modules at any depth |
*.log | Files ending in .log at the root |
**/*.log | Files ending in .log at any depth |
temp? | Files with names like tempa, tempb at the root |
**/temp* | Files or directories starting with temp at any depth |
The ! prefix creates an exception. A file that was excluded by an earlier pattern is re-included if a later pattern matches it with !.
*.md
!README.md
This excludes all Markdown files except README.md .
The order of the ! rules matters. The last matching pattern determines whether the file is included or excluded. In the following example, README-secret.md is excluded because the last matching pattern is the plain README-secret.md line:
*.md
!README*.md
README-secret.md
The !README*.md line re-includes all README files, but the last line excludes README-secret.md again .
A default-deny pattern uses * as the first pattern to exclude everything, followed by ! exceptions for the files that should be included.
*
!Dockerfile
!src/
!src/**
!package.json
!package-lock.json
This approach is useful when the number of files to include is small and known, and the number of files to exclude is large .
d. What to exclude
The contents of .dockerignore should be similar to .gitignore . Anything that is not tracked in version control is usually not needed in the build context.
Version control. .git and .gitignore exclude the history and configuration. The .git directory can be large, and copying it into an image ships the commit history .
Dependencies. node_modules, .venv, venv, and vendor directories contain dependencies that the Dockerfile reinstalls inside the image. Excluding them prevents the local platform’s binaries from being copied into the container .
Build outputs. dist, build, target, and similar directories contain artifacts that the Dockerfile rebuilds. Excluding them keeps the context small and prevents stale artifacts from affecting the build .
Test and coverage artifacts. coverage, .nyc_output, htmlcov, .pytest_cache, and test reports are not needed at build time .
Secrets and environment files. .env, .env.*, *.pem, *.key, *.crt, and credential files must never enter the context. The .dockerignore is the first line of defense against secret leakage .
Editor and IDE files. .vscode, .idea, *.swp, and similar files are local configuration that should not be in the image .
OS metadata. .DS_Store, Thumbs.db, and Desktop.ini are platform-specific and irrelevant to the build .
CI and documentation. .github, .gitlab-ci.yml, *.md, LICENSE, and docs are not needed for the build. Excluding them prevents changes to documentation from invalidating the cache .
Docker files. Dockerfile*, docker-compose*, and .dockerignore should be excluded to avoid recursive contexts .
e. The build cache interaction
The build cache is keyed on the metadata of files referenced by COPY and ADD instructions . When the instruction is COPY . ., every file in the context contributes to the cache checksum. A change to any file in the context invalidates the cache for that layer and every layer after it .
A .dockerignore that excludes files that change frequently — git history, test reports, editor configuration — prevents those changes from invalidating the cache . The README can be edited, the git branch can be switched, and the tests can be run without affecting the build cache for the COPY . . layer.
The Dockerfile structure also matters. Copying package.json before the source code, and installing dependencies before copying the rest, keeps the dependency layer cached as long as package.json does not change . The .dockerignore complements this structure by ensuring that only the files that matter are in the context.
BuildKit, the default builder in modern Docker, automatically skips transferring files that are not referenced by any COPY or ADD instruction . This reduces the context for simple Dockerfiles. But the automatic exclusion does not apply inside a directory that is copied with a broad instruction, and it does not prevent files from entering the context when COPY . . is used. A .dockerignore is still required for those cases .
Complete Example Session
# ============================================
# PART 1: PROJECT STRUCTURE WITHOUT IGNORE
# ============================================
# .git/ (150 MB of history)
# node_modules/ (400 MB of installed deps)
# coverage/ (20 MB of test reports)
# .env (secrets)
# dist/
# src/
# Dockerfile
# ============================================
# PART 2: BUILD WITHOUT .dockerignore
# ============================================
docker build .
# Sending build context to Docker daemon 570.1MB
# The entire directory is transferred, including unused files
# ============================================
# PART 3: CREATE .dockerignore
# ============================================
# .dockerignore
.git
node_modules
dist
coverage
.env
.env.*
*.md
.vscode
.idea
# ============================================
# PART 4: BUILD WITH .dockerignore
# ============================================
docker build .
# Sending build context to Docker daemon 4.2MB
# Only the source and configuration files are transferred
# ============================================
# PART 5: NEGATION PATTERN
# ============================================
# .dockerignore
*.md
!README.md
# All markdown files excluded except README.md
# ============================================
# PART 6: DEFAULT-DENY PATTERN
# ============================================
# .dockerignore
*
!Dockerfile
!src/
!src/**
!package.json
!package-lock.json
# Everything excluded except the listed files
# ============================================
# PART 7: NODE.JS .dockerignore
# ============================================
# .dockerignore
node_modules
dist
coverage
.git
.env
.env.*
*.log
.vscode
.idea
.DS_Store
Dockerfile*
docker-compose*
# ============================================
# PART 8: PYTHON .dockerignore
# ============================================
# .dockerignore
__pycache__
*.py[cod]
.venv
venv
*.egg-info
.pytest_cache
.coverage
htmlcov
.git
.env
.env.*
# ============================================
# PART 9: GO .dockerignore
# ============================================
# .dockerignore
*.exe
*.test
*.out
vendor/
.git
.env
.env.*
**/*_test.go
**/testdata/
# ============================================
# PART 10: VERIFY THE CONTEXT
# ============================================
docker build .
# Check the first line for context size
# A small context means the ignore file is working
These ten parts cover the project structure without an ignore file, the build without one, creating the file, the build with one, negation patterns, the default-deny pattern, and .dockerignore templates for Node.js, Python, and Go.
Quick Reference
Build Context
| Aspect | Detail |
|---|---|
| What it is | The directory tree sent to the daemon |
| When transferred | Before the Dockerfile executes |
| Size shown | First line of docker build output |
| Boundary | COPY and ADD cannot access outside |
.dockerignore Syntax
| Pattern | Matches |
|---|---|
node_modules | Root-level directory |
**/node_modules | Any depth |
*.log | Root-level files |
**/*.log | Any depth |
!README.md | Exception (re-include) |
# comment | Ignored |
What to Exclude
| Category | Examples |
|---|---|
| Version control | .git, .gitignore |
| Dependencies | node_modules, .venv, vendor/ |
| Build outputs | dist, build, target |
| Test artifacts | coverage, .pytest_cache |
| Secrets | .env, *.pem, *.key |
| Editor files | .vscode, .idea |
| OS metadata | .DS_Store, Thumbs.db |
| Documentation | *.md, docs |
| Docker files | Dockerfile*, .dockerignore |
Negation Order
| Order | Result |
|---|---|
*.md then !README.md | All MD except README |
!README*.md then README-secret.md | READMEs except secret |
README-secret.md then !README*.md | All READMEs included |
Cache Interaction
| Instruction | Cache Key |
|---|---|
RUN | Command string |
COPY / ADD | File metadata checksum |
COPY . . | Every file in context |
Best Practices
✅ Do This:
# .dockerignore in the context root
.git
node_modules
dist
.env
.env.*
*.log
.vscode
.idea
.DS_Store
Dockerfile*
docker-compose*
.dockerignore
# Copy specific files before the full source
COPY package.json package-lock.json ./
RUN npm ci
COPY src/ ./src/
# Verify the context size
docker build . 2>&1 | head -1
❌ Don’t Do This:
# No .dockerignore
docker build . # ❌ sends everything
# Exclude the Dockerfile from the context
# (it is still sent to the daemon but not copied)
# Copy everything without an ignore file
COPY . . # ❌ includes node_modules, .git, .env
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Context too large | No .dockerignore | Create one |
| Secret in image | .env copied | Exclude .env |
| Cache invalidated by README | COPY . . with no ignore | Exclude *.md |
COPY fails | File excluded by ignore | Add !file exception |
** not working | Used *.go instead of **/*.go | Use **/ prefix |
.git in image | Not excluded | Add .git |
Real-World Examples
1. Basic Exclusions
.git
node_modules
dist
.env
2. Negation
*.md
!README.md
3. Default Deny
*
!src/
!src/**
!Cargo.toml
4. Node.js
node_modules
coverage
.git
.env
*.log
5. Python
__pycache__
.venv
*.egg-info
.pytest_cache
6. Go
vendor/
**/*_test.go
**/testdata/
7. Editor Files
.vscode
.idea
*.swp
8. OS Metadata
.DS_Store
Thumbs.db
9. Docker Files
Dockerfile*
docker-compose*
.dockerignore
10. Verify Context Size
docker build . 2>&1 | head -1
Visual
Build Context Flow
┌──────────────────────────────────────────────────────────────┐
│ PROJECT DIRECTORY │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ .git/ (150 MB) │ │
│ │ node_modules/ (400 MB) │ │
│ │ src/ (2 MB) │ │
│ │ Dockerfile (1 KB) │ │
│ │ .env (secrets) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ .dockerignore applied │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ .dockerignore │ │
│ │ .git │ │
│ │ node_modules │ │
│ │ .env │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ CONTEXT SENT TO DAEMON │ │
│ │ src/ (2 MB) │ │
│ │ Dockerfile (1 KB) │ │
│ │ package.json (1 KB) │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Negation Pattern Order
┌──────────────────────────────────────────────────────────────┐
│ PATTERN ORDER MATTERS │
│ │
│ *.md │
│ !README*.md │
│ README-secret.md │
│ │
│ Result: README-secret.md excluded │
│ (last matching pattern wins) │
│ │
│ ───────────────────────────────────────── │
│ │
│ *.md │
│ README-secret.md │
│ !README*.md │
│ │
│ Result: README-secret.md included │
│ (last matching pattern wins) │
└──────────────────────────────────────────────────────────────┘
Cache Invalidation
┌──────────────────────────────────────────────────────────────┐
│ WITHOUT .dockerignore: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ COPY . . ← every file in context │ │
│ │ Edit README → cache invalidated │ │
│ │ Switch branch → cache invalidated │ │
│ │ Run tests → cache invalidated │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ WITH .dockerignore: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ COPY . . ← only source files │ │
│ │ Edit README → cache preserved │ │
│ │ Switch branch → cache preserved │ │
│ │ Run tests → cache preserved │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Default-Deny Pattern
┌──────────────────────────────────────────────────────────────┐
│ * ← exclude everything │
│ !Dockerfile ← include Dockerfile │
│ !src/ ← include src directory │
│ !src/** ← include everything inside src │
│ !package.json ← include package.json │
│ !package-lock.json ← include package-lock.json │
│ │
│ The context contains only the listed files. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Build context | Everything sent to the daemon before the build |
.dockerignore | Excludes files from the context |
| Location | Context root, alongside the Dockerfile |
| Syntax | .gitignore-like, plus ** |
| Negation | ! re-includes a previously excluded file |
| Order matters | Last matching pattern wins |
| Default deny | * then ! exceptions |
| Cache key | File metadata for COPY/ADD |
| BuildKit | Skips unused files automatically |
| Best practice | Mirror .gitignore |
Key takeaways:
- The build context is everything Docker sends to the daemon. It is transferred before the Dockerfile executes, and it includes every file in the directory tree, whether the Dockerfile references it or not .
.dockerignoreexcludes files from the context. The file goes in the context root, and its patterns are matched against paths relative to that root. The syntax is similar to.gitignorewith the addition of**.- A small context is faster and safer. It transfers faster, especially in CI where the transfer is over the network, and it prevents secrets, git history, and local artifacts from entering the image .
- The ignore file prevents spurious cache invalidation. When
COPY . .is used, every file in the context contributes to the cache checksum. Excluding files that change frequently — README, test reports, editor config — keeps the cache valid . - Negation patterns have order-dependent behavior. The last matching pattern determines the outcome. Use
!after the exclusion it overrides . - The default-deny approach is for minimal contexts.
*excludes everything, and!exceptions include only the files the build needs . - BuildKit skips unused files automatically. It does not transfer files that no
COPYorADDinstruction references, but it does not help inside a broadCOPY . ..
Remember: The build context is the foundation of the build. A well-written .dockerignore is one of the cheapest and most effective optimizations available: it reduces transfer time, prevents cache invalidation, and keeps secrets out of the image. The file should mirror .gitignore and exclude version control history, dependencies, build outputs, test artifacts, secrets, editor configuration, and OS metadata. The negation syntax allows exceptions when a file inside an excluded directory is needed. The default-deny pattern gives the smallest possible context. And the build cache interaction means that excluding files that change frequently is as important for build speed as it is for security.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!