OnCallReady

Lesson 10.21 · Images & Builds · 14 min read

The build context and .dockerignore

In plain words

Imagine asking a friend in another town to bake your cake, and you must mail them everything they might need first. If you put your whole kitchen in the box, including the dirty dishes, your diary and the spare house key, it takes ages to send and your friend now has your key. A packing list saying 'do not send these' fixes both problems.

The build context is that box: the directory you pass to docker build (usually .), sent in full to the builder before anything runs. COPY can only reach what is in it. .dockerignore is the packing list. The gotcha: its patterns are anchored at the context root, so node_modules only matches the top-level one; **/node_modules matches every depth.

Why this matters

Two incidents start here. The slow one: every build first spends seconds uploading hundreds of MB it never uses. The bad one: COPY . . bakes a developer's password file into an image that the whole company can download. One small file, .dockerignore, prevents both.

What you need to know already: docker build and the context line in its output ("The Dockerfile, and reading a build"), the cache (previous lesson), globs like *.log (bash, Ch 6), du -sh (Ch 4).

What gets sent

docker build -t app . - the . is the context: a directory that is sent to the builder in full before anything runs. COPY can only reach files inside it (COPY ../secrets . does not work, by design), and the transfer shows up in the build output:

 => [internal] load build context                                     3.1s
 => => transferring context: 412.35MB                                 3.1s

400MB for a service whose source is a few hundred kB. Where does it come from?

# in that project (a Java service with a Node.js frontend and no .dockerignore)
du -sh .git target node_modules .idea 2>/dev/null
88M	.git
25M	target
312M	node_modules
204K	.idea

.git is the git history, target is Maven's build output, node_modules is where Node.js keeps downloaded packages, .idea is an editor's settings.

It gets worse than slow. COPY . . puts all of it into a layer: your git history, your local build output, the editor config - and .env, the file of local settings and credentials, now baked into an image that gets pushed to a shared registry. Anyone who can pull the image can read it. "Someone pushed an image with the database password in it" starts exactly like this.

.dockerignore

A file named .dockerignore in the root of the context lists what not to send, one pattern per line:

# version control and IDEs
.git
.idea
.vscode

# build output and dependencies - rebuilt inside the image
target
node_modules
dist
__pycache__
*.pyc
.venv

# secrets and local config
.env
*.pem
*.key

# the build definition itself
Dockerfile*
.dockerignore

(__pycache__, *.pyc and .venv are Python's leftovers; *.pem and *.key are certificate and key files, Ch 9.) The next build sends a few kB:

 => [internal] load .dockerignore                                     0.0s
 => => transferring context: 186B                                     0.0s
 => [internal] load build context                                     0.0s
 => => transferring context: 14.2kB                                   0.0s

Patterns are anchored at the root - it is NOT .gitignore

This is the gotcha. In .gitignore, node_modules matches at any depth. In .dockerignore, a pattern is matched against paths relative to the context root (it is anchored there):

node_modules        only ./node_modules
*.log               only logs in the root
**/node_modules     node_modules at any depth
**/*.log            logs at any depth
docs/*.md           markdown directly under docs/

**/ means "any number of directories". A repository holding several services (a monorepo) with services/api/node_modules sends all of it past a node_modules line. Use **/ for anything that can appear below the root.

Exceptions start with ! and are read in order - the last matching line wins:

*.md
!README.md          README.md is sent after all

Ignoring something you COPY is an error

If .dockerignore excludes a path that a COPY asks for, the file is simply not in the context:

$ cat .dockerignore
target
$ docker build -t app .
 => ERROR [3/3] COPY target/orders-2.14.1.jar app.jar                 0.0s
------
 > [3/3] COPY target/orders-2.14.1.jar app.jar:
------
ERROR: failed to solve: failed to compute cache key: failed to calculate checksum of ref 6b3c1f0a...::xk2f...: "/target/orders-2.14.1.jar": not found

"failed to calculate checksum ... not found" means the builder could not see the file: a wrong path, a path outside the context, or an ignore rule. Either un-ignore it with an exception line (!target/*.jar), or - better - build the JAR inside the image with a multi-stage build (a few lessons from now), so the host's target/ never matters.

Choosing the context

The context does not have to be .:

docker build -t api -f services/api/Dockerfile services/api    # small context
docker build -t api -f services/api/Dockerfile .               # whole repo

-f names the Dockerfile; the last argument is the context. A Dockerfile can live outside its context, but every COPY path is relative to the context, not to the Dockerfile. Give each service the smallest context that contains what it needs.

The four questions

When a build is slow before the first step even runs, or an image contains things it should not:

  1. What is the transferring context size? More than a few MB is suspicious.
  2. Is there a .dockerignore at the context root (not next to the Dockerfile)?
  3. Are nested directories covered with **/?
  4. Does any COPY . . still exist? Prefer copying exactly what the build needs.

What you can now do

Why it helps

This lesson prevents two real incidents. The slow one: a build spends several seconds on transferring context: 412MB before the first step, because .git, target/ and node_modules are being shipped every time. The bad one: COPY . . bakes a developer's .env with production credentials into an image that goes to a shared registry, where anyone who can pull it can read it. In a PR review, the two questions 'is there a .dockerignore at the context root?' and 'does it use **/ for nested directories?' catch both. You will also recognise failed to calculate checksum ... not found immediately: the file is outside the context or excluded by an ignore rule, not missing from disk.

Commands in this lesson

cat docker

FAQ

Does .dockerignore work like .gitignore?

Not quite, and that is the classic trap. In .gitignore, node_modules matches at any depth. In .dockerignore, patterns are matched against paths relative to the context root, so node_modules only excludes ./node_modules. Use **/node_modules or **/*.log for anything that can appear deeper, which matters in monorepos. Exceptions with ! work, and the last matching line wins.

Where does .dockerignore have to be?

In the root of the build context, not necessarily next to the Dockerfile. If you run docker build -f services/api/Dockerfile ., the context is the repo root and the .dockerignore must be there. BuildKit also supports a per-Dockerfile ignore file named <Dockerfile>.dockerignore next to the Dockerfile, which takes precedence.

Why can't I COPY ../shared-lib .?

COPY can only reach files inside the context; paths outside it are not sent to the builder, by design, so a Dockerfile cannot read arbitrary files from your machine. Either make the context a common parent directory with -f pointing at the Dockerfile, or use a named build context with --build-context, which BuildKit supports.

What does failed to calculate checksum ... not found mean?

The builder could not see the file a COPY asked for. Three usual causes: the path is wrong (it is relative to the context, not to the Dockerfile), the path is outside the context, or .dockerignore excludes it. Check the ignore file first. The better long-term fix is often to build the artifact inside a build stage instead of copying it from the host.

Should I exclude the Dockerfile itself?

You can. The builder still receives the Dockerfile through a separate channel, so excluding it only keeps it out of COPY . .. It also means editing a comment in the Dockerfile does not bust the COPY cache. The same goes for .dockerignore. It is a small tidy-up, not a requirement.

In an interview Junior

What is the Docker build context, and why does .dockerignore matter?

The last argument of docker build (the .) is the build context: a directory sent to the builder in full before any step runs. COPY can only see files inside it. The output shows its size in transferring context:.

Without a .dockerignore, that is .git, target/, node_modules/, editor settings - and .env with credentials. Slow every build, and COPY . . bakes all of it into a layer that anyone who can pull the image can read.

.dockerignore sits at the context root and lists what not to send. The gotcha: patterns are anchored at the root, unlike .gitignore - node_modules only matches ./node_modules; use **/node_modules for nested ones. ! lines are exceptions, the last match wins.

If a COPY fails with failed to calculate checksum ... not found, the file is outside the context or ignored.

Also asked: A COPY works on your machine but fails in CI with "not found". What do you check? · How is .dockerignore different from .gitignore? · Why is COPY . . risky?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.