Docker Fundamentals · Module 05 of 12

Build cache and image quality

Make builds faster and images safer by controlling build context, cache invalidation, dependency ordering and final image contents.

Learning objectives

By the end of this module, you should be able to:

  • Explain why Docker can reuse cached build results.
  • Use .dockerignore to reduce build context and accidental inclusion.
  • Describe why multi-stage builds can reduce final image size and attack surface.

Cache depends on instruction inputs

Docker can reuse previous build results when an instruction and the inputs it depends on have not changed. Ordering stable dependency-installation steps before frequently changing application source can improve cache reuse.

A cache hit is a performance optimization, not proof that dependencies are current or secure. Rebuild policy still matters.

.dockerignore protects the build context

A .dockerignore file excludes files and directories from the context sent to the builder. Typical exclusions include .git, local dependency folders, editor files, secrets and build outputs that do not belong in the image.

Multi-stage builds separate build tools from runtime

A Dockerfile can use multiple FROM stages. A build stage contains compilers or tooling, while a later runtime stage copies only the produced artifact. Docker documentation recommends multi-stage patterns broadly because they can keep runtime images smaller and reduce unnecessary components.

Worked examples

See the idea in practice.

A useful .dockerignore

.git
node_modules
*.log
.env
.env.*
coverage
dist
  • Do not send local credentials or unnecessary dependency trees into the build context.
  • Adjust exclusions to your actual build requirements.

Conceptual multi-stage pattern

FROM golang:1.24-alpine AS build
WORKDIR /src
COPY . .
RUN go build -o /out/app ./cmd/app

FROM alpine:3.20
COPY --from=build /out/app /usr/local/bin/app
CMD ["/usr/local/bin/app"]
  • The compiler exists only in the build stage.
  • The final stage receives the built binary, not the whole toolchain.
Hands-on lab

Lab — Observe cache behavior and build context

Goal: Build a small image twice, then change one input and observe which instructions must be rebuilt.

STEP 1

Create files

mkdir -p ~/docker-fundamentals/cache && cd ~/docker-fundamentals/cache
printf 'one\n' > stable.txt
printf 'first\n' > changing.txt
printf 'secret-do-not-copy\n' > local-secret.txt
STEP 2

Create ignore rules and Dockerfile

cat > .dockerignore <<'EOF'
local-secret.txt
.git
EOF
cat > Dockerfile <<'EOF'
FROM alpine:3.20
WORKDIR /app
COPY stable.txt .
RUN sha256sum stable.txt > stable.sha256
COPY changing.txt .
CMD ["cat","/app/changing.txt"]
EOF
STEP 3

Build twice

docker build -t gnu-cache:1 .
docker build -t gnu-cache:1 .
STEP 4

Change the later input and rebuild

printf 'second\n' > changing.txt
docker build -t gnu-cache:2 .
STEP 5

Prove ignored secret is not in context/image

docker run --rm gnu-cache:2
docker run --rm gnu-cache:2 sh -c 'test ! -e /app/local-secret.txt && echo ignored'

Verify

  • The second unchanged build reuses cache extensively.
  • After changing changing.txt, earlier stable steps remain reusable.
  • local-secret.txt is not present in the image.

Expected outcome

  • gnu-cache:2 prints second and ignored.

If it fails

  • Build output differs between classic and BuildKit renderings; focus on whether steps are reused rather than exact text.
  • Never rely on .dockerignore as the only secret-management control—do not keep production secrets in build directories unnecessarily.
Real-world connection

Large CI builds are often optimized by separating dependency metadata from rapidly changing application code and using multi-stage runtime images.

Avoid these traps

Common mistakes

  • Copying the whole repository before dependency installation when that destroys useful cache reuse.
  • Assuming deleting a secret in a later layer guarantees it was never present in image history.
  • Using very large general-purpose images when the runtime needs only a small subset.
Knowledge check

Can you explain it without looking back?

Open each answer only after you have tried to answer the question yourself.

1Why use .dockerignore?

To keep unnecessary or sensitive local files out of the build context.

2What is a major benefit of multi-stage builds?

Build tools can remain in an intermediate stage while the final runtime image contains only needed artifacts.

3Does a cached build mean dependencies are up to date?

No. Cache reuse is about unchanged build inputs; update/rebuild policy is separate.