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.
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.
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 . ..
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.
node_modules
**/*.log
.git
.env
.env.*
!.env.example
dist
coverageExcluding .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.
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.
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 themPut 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:
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.
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:
# 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 curlCache mounts live in the builder's storage: docker builder prune clears them and docker system df shows how much space the build cache occupies.
.dockerignore, so COPY . . copies the host's node_modules, which may contain binaries for the wrong platform.RUN re-executes when its inputs change; only the command text is compared.Why does `RUN apt-get update` not re-run on the next build even though repositories have changed?
COPY can see; keep it small with a .dockerignore that excludes .git, dependencies, build output and .env files.RUN compares text, COPY compares file checksums.--no-cache and --pull force rebuilds; --progress=plain and docker history show what happened.Next lesson: Docker Hub and Private Registries: Pushing and Pulling Images — share your images through public and private registries.