Build Context, .dockerignore and Layer Caching

Intermediate
12 min

Build Context, .dockerignore and Layer Caching

A build that takes five minutes when it could take five seconds usually has two causes: it sends too much to the daemon, or its Dockerfile invalidates the cache on every change. This lesson explains what the build context is, how .dockerignore shrinks it, exactly how BuildKit decides whether a layer can be reused, and how to order instructions and use cache mounts so rebuilds are fast.

What the Build Context Is

The last argument of docker build is the context: a directory (or Git URL) whose contents are sent to the builder. COPY and ADD can only see files inside it.

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

BuildKit transfers only the files a COPY actually references, but it still has to scan the whole directory, and legacy tooling sends everything. A context containing node_modules or .git slows every build and risks copying secrets into the image with COPY . ..

.dockerignore

A .dockerignore file in the context root excludes paths before they reach the builder. The syntax matches .gitignore with a few differences: patterns are matched against the context root, ** matches any number of directories, and ! re-includes a path.

text
node_modules **/*.log .git .env .env.* !.env.example dist coverage

Excluding .git, dependency folders, build output and local environment files is the minimum for any project. When the Dockerfile lives in a different directory, name the ignore file <Dockerfile-name>.dockerignore next to it and BuildKit will prefer it.

How the Cache Works

Each instruction produces a layer, and BuildKit reuses a layer when the instruction and everything before it are unchanged. The comparison differs by instruction:

| Instruction | Cache hit when | |-------------|----------------| | RUN | The command string is identical (its output is not inspected) | | COPY / ADD | The checksums of the copied files are identical (mtime is ignored) | | FROM | The base image digest is unchanged locally (--pull forces a check) | | ENV, WORKDIR, ARG | The instruction text is identical |

The consequence is that the first changed instruction invalidates it and every instruction after it. RUN apt-get update will therefore never re-run on its own; pair it with --no-cache or change the line when you need fresh package lists.

bash
docker build --no-cache -t my-app . # ignore the cache entirely docker build --pull -t my-app . # refresh the base image first docker build --progress=plain -t my-app . # show CACHED markers per step docker history my-app # layer sizes and the commands that made them

Ordering Instructions for Cache Hits

Put what changes least at the top and what changes most at the bottom. For a Node.js project, dependency manifests change rarely while source changes constantly:

dockerfile
FROM node:22-alpine WORKDIR /app COPY package.json package-lock.json ./ # only these two files RUN npm ci --omit=dev # cached until the lockfile changes COPY . . # source changes only invalidate from here CMD ["node", "server.js"]

With this order, editing server.js reuses the npm ci layer and the rebuild takes a second. With COPY . . first, every edit reinstalls all dependencies. The same principle applies to pip install -r requirements.txt, go mod download and Maven's dependency:go-offline.

BuildKit Cache Mounts

Even when a dependency layer must be rebuilt, the package manager's download cache does not have to start empty. A cache mount persists a directory between builds without adding it to the image:

dockerfile
# syntax=docker/dockerfile:1 RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ apt-get update && apt-get install -y curl

Cache mounts live in the builder's storage: docker builder prune clears them and docker system df shows how much space the build cache occupies.

Common Mistakes

  • Forgetting .dockerignore, so COPY . . copies the host's node_modules, which may contain binaries for the wrong platform.
  • Copying the entire source before installing dependencies, defeating the cache on every edit.
  • Assuming RUN re-executes when its inputs change; only the command text is compared.
Quick Quiz
Question 1 of 3

Why does `RUN apt-get update` not re-run on the next build even though repositories have changed?

Key Takeaways

  • The build context is what COPY can see; keep it small with a .dockerignore that excludes .git, dependencies, build output and .env files.
  • A layer is reused only if its instruction and every previous layer are unchanged; RUN compares text, COPY compares file checksums.
  • Order Dockerfiles from least to most frequently changing; copy manifests and install dependencies before copying source.
  • --no-cache and --pull force rebuilds; --progress=plain and docker history show what happened.
  • BuildKit cache mounts keep package-manager caches between builds without bloating the image.

Next lesson: Docker Hub and Private Registries: Pushing and Pulling Images — share your images through public and private registries.

Build Context, .dockerignore and Layer Caching - Docker | CodeYourCraft | CodeYourCraft