docker

Docker Build Context and .dockerignore: Faster Builds, Smaller Images

Learn what the Docker build context is, how .dockerignore patterns work, and how to cut build time, protect secrets and shrink images, with ready-to-use templates.

By Shubhankar Tripathi • • 5 min read

Docker Build Context and .dockerignore: Reduce Build Time and Image Size



Every docker build command ends with an argument that many developers type without thinking: usually a single dot. That dot is the build context, and it decides which files the builder can see. Point it at a directory full of node_modules, Git history, logs and local environment files, and your builds become slow, your cache keeps breaking, your images grow, and secrets can end up inside an image you push to a registry.

The fix is usually a few lines in a .dockerignore file. This guide explains what the build context is, how BuildKit transfers it, exactly how .dockerignore patterns are matched, and how to use them to make builds faster, images smaller and secrets safer. It ends with ready-to-use templates for Node.js, Python, Java and Go projects. Examples are written for Docker Engine 29 with BuildKit, as of October 2026.

Quick Answer

  • Build context: the set of files the builder can access during a build. In docker build -t app ., the final . means "the current directory and everything under it".

  • Only files in the context can be copied: COPY and ADD can read only from the build context (or from other build stages and named contexts), never from anywhere else on your machine.

  • .dockerignore: a file in the root of the context listing paths to exclude. Excluded files are never sent to the builder and can never be copied into the image.

  • Why it matters: a good .dockerignore makes builds faster, keeps the COPY cache stable, keeps secrets out of images and prevents bloated images.

What Is the Build Context?

The docker build command has this form:

docker build [OPTIONS] PATH | URL | -

The PATH, URL or - argument is the build context. The Dockerfile tells the builder what to do; the context provides the files it can use while doing it.

docker build -t my-app .                       # context = current directory

docker build -t my-app ./services/api          # context = a subdirectory

docker build -t my-app -f docker/Dockerfile .   # Dockerfile elsewhere, context = current directory

Important: The Dockerfile location and the context are independent. -f chooses the Dockerfile, while the positional argument chooses the context. All COPY and ADD source paths are relative to the context, not to the Dockerfile.

Because sources must come from the context, this does not work:

COPY ../shared/config.yaml /app/   # fails: the path is outside the build context

To include files from a parent directory, make the parent directory the context and point -f at the Dockerfile, or use a named context (covered later in this guide).

Figure 1: How .dockerignore filters the build context before it reaches BuildKit

Figure 1: How .dockerignore filters the build context before it reaches BuildKit

How BuildKit Transfers the Context

Docker uses a client-server architecture: the docker CLI and the builder (BuildKit, inside the Docker daemon or a remote builder) are separate programs, and they can even run on different machines. The builder therefore cannot simply read your disk; the CLI has to send it the files it needs. You can see this step in every build:

=> [internal] load build definition from Dockerfile        0.0s

=> [internal] load .dockerignore                           0.0s

=> [internal] load build context                           4.8s

=> => transferring context: 734.21MB                       4.7s

BuildKit is smarter than the legacy builder it replaced. It loads files from the context only when they are needed, and on repeated builds it reuses files it already received and sends only what changed. However, an instruction such as COPY . . needs the whole context, so a large context is still transferred, still affects caching and still ends up in your image. The size reported in the "transferring context" line is the first number to check when a build feels slow.

Remote builders: Context size matters even more when the builder is remote, for example a Docker context pointing to another server, a cloud builder, or Docker Desktop's virtual machine. Every megabyte has to cross the network or the virtualisation boundary.

Types of Build Context

Context type

Example

Notes

Local directory

docker build .

The most common type; all subdirectories are included

Git repository

docker build https://github.com/user/repo.git#main:docker

Shallow clone of the given branch, tag or full commit hash; :docker selects a subdirectory

Remote tarball

docker build https://example.com/context.tar.gz

The builder downloads and extracts it

Local tarball via stdin

docker build - < context.tar.gz

Useful when generating the context in a script

No context (Dockerfile via stdin)

docker build - < Dockerfile

Builds without any files; COPY from the context is not possible

Dockerfile via stdin with a local context

docker build -f- . < Dockerfile

Generated Dockerfile, real files


For Git contexts, newer Buildx versions also accept a query syntax such as 'https://github.com/user/repo.git?branch=main&subdir=docker', which Docker's documentation now recommends; it requires recent Buildx and Dockerfile versions. The .git directory is not kept in a Git context by default. If your build needs it, for example to embed the commit hash, pass --build-arg BUILDKIT_CONTEXT_KEEP_GIT_DIR=1.

Build With No Context When You Need No Files

If the Dockerfile only installs packages and does not copy any local files, build it without a context. Nothing is transferred and nothing can leak:

docker build -t curl-tool - <<EOF

FROM alpine:3.22

RUN apk add --no-cache curl

EOF

Why a Large or Messy Context Hurts

Problem

What happens

Typical culprit

Slow builds

Hundreds of megabytes are transferred before any instruction runs

node_modules, .git, virtual environments, datasets

Broken cache

COPY . . is invalidated whenever any file in the context changes, forcing every later step to rebuild

Log files, editor swap files, test reports, .git

Bloated images

Unneeded files are copied into the final image

Build output, test fixtures, documentation, local dependencies

Leaked secrets

Credentials are copied into an image layer and pushed to a registry

.env, .aws/, *.pem, id_rsa, .npmrc with tokens

Broken containers

Host-built dependencies overwrite ones built for the container platform

node_modules from macOS or Windows copied into a Linux image


Security: A secret that reaches any image layer stays in the image forever, even if a later instruction deletes it. Anyone who can pull the image can extract it. Excluding secret files in .dockerignore is the first line of defence; pass secrets the build genuinely needs with RUN --mount=type=secret instead.

The .dockerignore File

A .dockerignore file is a plain text file in the root of the build context. Before the context is sent, Docker removes every file and directory that matches one of its patterns. Excluded files are invisible to the build: they are not transferred, and COPY and ADD cannot use them.

Pattern Syntax

Pattern

Meaning

Example matches

name

A file or directory called name in the root of the context

node_modules, but not src/node_modules

dir/file

A specific path relative to the root

config/local.yaml

*

Any sequence of characters except /

*.log matches app.log in the root only

?

Any single character except /

file?.txt matches file1.txt

**

Any number of directories, including none

**/*.log matches log files at any depth

!pattern

An exception: re-includes paths excluded by an earlier pattern

!README.md

# text

A comment line

# Build output


How Docker Processes the File

  • Patterns are relative to the context root. Leading and trailing slashes are removed and paths are cleaned, so /node_modules, node_modules/ and node_modules all mean the same thing.

  • Matching is anchored at the root, unlike .gitignore. *.log matches only log files in the root; use **/*.log to match them everywhere.

  • A matching directory excludes everything inside it. node_modules removes the whole directory tree.

  • The last matching line wins. Exceptions with ! must come after the pattern they override.

  • Blank lines are ignored. Lines are matched exactly as written; a path that does not exist is simply ignored.

Common mistake: .dockerignore is not .gitignore. A pattern like *.pyc in .gitignore matches at every depth, but in .dockerignore it matches only in the context root. Write **/*.pyc instead.

Exceptions With !

Exceptions let you exclude a whole category and then allow specific files back in:

# Exclude all Markdown files...

**/*.md

 

# ...except the README in the root

!README.md

Order matters. Reversing the two lines would exclude README.md again, because the last matching pattern wins.

The Allowlist Approach

For maximum control, exclude everything and then allow only what the build needs. Any new file added to the project later stays out of the build until you decide otherwise:

# Exclude everything by default

*

 

# Allow only what the image needs

!src/

!package.json

!package-lock.json

!tsconfig.json

The allowlist approach is excellent for security-sensitive projects, but it needs maintenance: if the Dockerfile starts copying a new file, you must add an exception for it, or the build fails with a "not found" error.

Excluding the Dockerfile and .dockerignore

You can list Dockerfile and .dockerignore in .dockerignore. The builder still receives them, because it needs them to run the build, but COPY . . will not copy them into the image.

Dockerfile-Specific Ignore Files

Projects with several Dockerfiles often need different exclusions for each. Place an ignore file named after the Dockerfile, with .dockerignore appended, in the same directory as that Dockerfile:

project/

├── .dockerignore                 # used by default

├── docker/

│   ├── api.Dockerfile

│   ├── api.Dockerfile.dockerignore   # used for api.Dockerfile

│   ├── worker.Dockerfile

│   └── worker.Dockerfile.dockerignore

└── src/

docker build -f docker/api.Dockerfile -t api .

When a Dockerfile-specific ignore file exists, it is used instead of the .dockerignore file in the context root. Patterns inside it are still relative to the root of the context.

Reduce Build Time and Image Size: Step by Step

Step 1: Measure the Current Context

Build once and note the "transferring context" size, or estimate it from the shell:

du -sh .

du -sh ./* ./.[!.]* 2>/dev/null | sort -h | tail -10

The second command lists the ten largest items in the project root, which are usually the first candidates for .dockerignore.

Step 2: List Exactly What the Builder Receives

This small trick builds a throwaway image that copies the whole context and prints its contents, so you see exactly what passes your .dockerignore:

docker build --no-cache --progress=plain -f- . <<EOF

FROM busybox

COPY . /context

RUN du -sh /context && find /context -maxdepth 2 | sort | head -100

EOF

Remove anything in the output that the image does not need. Repeat until the list contains only real build inputs.

Step 3: Add the Exclusions and Rebuild

Add patterns, rebuild, and compare the "transferring context" size and the build time. For a typical Node.js project, excluding node_modules, .git and build output often reduces the context from hundreds of megabytes to a few megabytes.

Step 4: Copy Only What You Need

.dockerignore and the Dockerfile work together. Even with a clean context, prefer specific COPY instructions over COPY . . where practical, and copy dependency manifests before source code so the dependency installation step stays cached:

FROM node:24-alpine

WORKDIR /app

COPY package.json package-lock.json ./

RUN npm ci --omit=dev

COPY src ./src

USER node

CMD ["node", "src/server.js"]

For more on why this order matters, see our tutorial Docker Image Layers, Union Filesystems, and Image Caching Explained.

Step 5: Check the Result

docker image ls my-app

docker image history my-app

docker run --rm my-app ls -la /app

Confirm that no secrets, test files, Git metadata or host dependencies made it into the image.

Ready-to-Use .dockerignore Templates

Start from the template for your stack and adjust it to your project. Each one assumes the image installs dependencies and builds the application inside the Docker build.

Node.js

# Dependencies and build output (rebuilt inside the image)

node_modules

**/node_modules

dist

build

.next

coverage

 

# Logs and caches

**/*.log

npm-debug.log*

.npm

.eslintcache

 

# Version control and editor files

.git

.gitignore

.vscode

.idea

**/.DS_Store

 

# Secrets and local configuration

.env

.env.*

!.env.example

**/*.pem

 

# Docker files

Dockerfile*

.dockerignore

compose*.yaml

docker-compose*.yml

Python

# Virtual environments and caches

.venv

venv

**/__pycache__

**/*.py[cod]

.pytest_cache

.mypy_cache

.ruff_cache

.tox

 

# Build artefacts and test output

build

dist

**/*.egg-info

htmlcov

.coverage

 

# Notebooks and data

.ipynb_checkpoints

data

 

# Version control, editors, secrets

.git

.vscode

.idea

.env

.env.*

**/*.pem

Data engineering projects deserve special attention: a local data/ or output/ directory with sample Parquet or CSV files can easily add gigabytes to a context. Mount data at run time with volumes instead of copying it into the image.

Java (Maven or Gradle)

target

build

.gradle

**/*.class

**/*.log

.git

.idea

*.iml

.vscode

.env

**/*.pem

Exclude target or build when the Dockerfile compiles the application itself. If your pipeline compiles outside Docker and the Dockerfile only copies the finished JAR, use an exception such as !target/app.jar after the target line.

Go

bin

vendor

coverage.out

**/*_test.go

.git

.idea

.vscode

.env

**/*.pem

Only exclude vendor if the build downloads modules with go mod download; keep it if your project builds from vendored dependencies. Excluding test files is optional and only appropriate if tests do not run inside the Docker build.

Beyond .dockerignore: Advanced Techniques

Exclude Files in a Single COPY Instruction

Sometimes a file is needed by one instruction but not another. The --exclude flag for COPY and ADD filters files for that instruction only. It was introduced in Dockerfile syntax 1.7 through the labs channel; declare the syntax at the top of the Dockerfile if your builder does not recognise the flag:

# syntax=docker/dockerfile:1-labs

FROM nginx:1.29

COPY --exclude=*.map --exclude=*.test.js dist/ /usr/share/nginx/html/

Named Build Contexts

A named context gives the build access to an additional directory, Git repository or image, without making your context bigger. Define it on the command line and use it with COPY --from:

docker buildx build \

  --build-context shared=../shared-config \

  -t api .

COPY --from=shared app-config.yaml /etc/app/

This is the clean solution for monorepos and for files that live outside the project directory. A named context can also replace a base image, for example --build-context alpine=docker-image://alpine:3.22, which is useful for testing a build against a patched base image.

Keep Secrets Out of the Context Entirely

If the build needs a credential, such as a private package registry token, do not place it in the context at all. Pass it as a build secret:

docker build --secret id=npmrc,src=$HOME/.npmrc -t api .

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci

The secret is available only while that single RUN instruction executes and is never stored in an image layer or the build cache.

Troubleshooting

Symptom

Likely cause

Fix

COPY fails with "not found" or "failed to compute cache key"

The file is outside the context, or excluded by .dockerignore

Check the context path and .dockerignore; add a ! exception if needed

A file is still copied despite a pattern

The pattern is root-anchored, e.g. *.log instead of **/*.log

Use **/ to match at any depth

An exception does not work

The ! line comes before the pattern it should override

Move the exception after the exclusion

.dockerignore seems to be ignored

The file is not in the context root, or a Dockerfile-specific ignore file takes precedence

Place it in the context root, or edit <Dockerfile>.dockerignore

"transferring context" takes a long time

Large directories in the context

Measure with du, then exclude large unnecessary directories

COPY . . step never uses the cache

Frequently changing files (logs, .git) in the context

Exclude them, or copy specific paths

Container crashes with native module errors

Host node_modules copied into a Linux image

Exclude node_modules and install dependencies in the Dockerfile

COPY ../file fails

Paths outside the context are not allowed

Widen the context with -f, or use a named context


Frequently Asked Questions

Where should the .dockerignore file be placed?

In the root of the build context, which is the directory you pass to docker build, not necessarily next to the Dockerfile. The exception is a Dockerfile-specific ignore file such as api.Dockerfile.dockerignore, which sits next to its Dockerfile.

Does .dockerignore work like .gitignore?

Mostly, but not completely. The biggest difference is that .dockerignore patterns are anchored at the context root, so *.log matches only root-level log files. Use **/*.log for all levels. Exceptions with ! and comments with # work in both.

Does .dockerignore reduce image size?

Indirectly, yes. Files excluded from the context cannot be copied into the image, so instructions such as COPY . . produce smaller layers. The size of the base image and installed packages still matter most, so combine .dockerignore with multi-stage builds and slim base images.

Should I exclude node_modules?

Yes, in almost all cases. Install dependencies inside the image with npm ci, so they are built for the container's operating system and CPU architecture, and so the context stays small.

Does Docker Compose use .dockerignore?

Yes. Compose passes the context defined in each service's build section to the builder, and the .dockerignore in that context (or the Dockerfile-specific ignore file) applies exactly as with docker build.

Can I see which files are in my build context?

Yes. Build a throwaway image that copies the context and lists it, as shown in Step 2 of this guide, or check the "transferring context" size in the build output.

Conclusion and Next Steps

The build context is the boundary of what your build can touch, and .dockerignore is how you control it. Keep the context small and purposeful: exclude dependencies, build output, Git history, logs and every kind of secret; anchor patterns correctly with **/; put exceptions last; and consider an allowlist for sensitive projects. Combine this with specific COPY instructions, cache-friendly ordering, named contexts and build secrets, and your builds will start faster, cache better, produce smaller images and never leak credentials.

Continue the Docker course with these tutorials on BitCodeMatrix:

  • Docker Image Layers, Union Filesystems, and Image Caching Explained — why a stable context keeps the build cache working

  • Dockerfile Best Practices — every instruction explained with production patterns

  • Complete Guide to Multi-Stage Docker Builds — keep build tools out of the final image

  • Build Smaller, Faster and More Secure Docker Images — further size reduction techniques