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:
- What is the
transferring contextsize? More than a few MB is suspicious. - Is there a
.dockerignoreat the context root (not next to the Dockerfile)? - Are nested directories covered with
**/? - Does any
COPY . .still exist? Prefer copying exactly what the build needs.
What you can now do
- Read the context size and find what is bloating it.
- Write a
.dockerignorewith**/patterns and!exceptions. - Explain
failed to calculate checksum ... not found.