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