| |

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 .

PatternMatches
node_modulesThe node_modules directory at the root
**/node_modulesnode_modules at any depth
*.logFiles ending in .log at the root
**/*.logFiles 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

AspectDetail
What it isThe directory tree sent to the daemon
When transferredBefore the Dockerfile executes
Size shownFirst line of docker build output
BoundaryCOPY and ADD cannot access outside

.dockerignore Syntax

PatternMatches
node_modulesRoot-level directory
**/node_modulesAny depth
*.logRoot-level files
**/*.logAny depth
!README.mdException (re-include)
# commentIgnored

What to Exclude

CategoryExamples
Version control.git, .gitignore
Dependenciesnode_modules, .venv, vendor/
Build outputsdist, build, target
Test artifactscoverage, .pytest_cache
Secrets.env, *.pem, *.key
Editor files.vscode, .idea
OS metadata.DS_Store, Thumbs.db
Documentation*.md, docs
Docker filesDockerfile*, .dockerignore

Negation Order

OrderResult
*.md then !README.mdAll MD except README
!README*.md then README-secret.mdREADMEs except secret
README-secret.md then !README*.mdAll READMEs included

Cache Interaction

InstructionCache Key
RUNCommand string
COPY / ADDFile 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

PitfallWhy It HappensFix
Context too largeNo .dockerignoreCreate one
Secret in image.env copiedExclude .env
Cache invalidated by READMECOPY . . with no ignoreExclude *.md
COPY failsFile excluded by ignoreAdd !file exception
** not workingUsed *.go instead of **/*.goUse **/ prefix
.git in imageNot excludedAdd .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

ItemValue
Build contextEverything sent to the daemon before the build
.dockerignoreExcludes files from the context
LocationContext root, alongside the Dockerfile
Syntax.gitignore-like, plus **
Negation! re-includes a previously excluded file
Order mattersLast matching pattern wins
Default deny* then ! exceptions
Cache keyFile metadata for COPY/ADD
BuildKitSkips unused files automatically
Best practiceMirror .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 .
  • .dockerignore excludes 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 .gitignore with 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 COPY or ADD instruction references, but it does not help inside a broad COPY . . .

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!