Skip to main content

Layer Cache and Build Context

Docker builds images one instruction at a time. Each instruction creates a layer, and Docker caches these layers so it does not have to rebuild everything from scratch on every build. Understanding how this cache works is the single biggest lever you have for making builds faster.

How Layer Caching Works

When Docker executes a build, it checks each instruction against its cache:

flowchart TD
A["Instruction: FROM node:20-alpine"] -->|"Unchanged?"| B{"Cache hit?"}
B -->|Yes| C["Reuse cached layer"]
B -->|No| D["Rebuild this layer"]
D --> E["All following layers also rebuild"]
C --> F["Move to next instruction"]
F --> G["Instruction: COPY package.json ./"]
G -->|"File changed?"| H{"Cache hit?"}
H -->|Yes| I["Reuse cached layer"]
H -->|No| J["Rebuild + all layers after this"]

style C fill:#e8f5e9,stroke:#2e7d32
style I fill:#e8f5e9,stroke:#2e7d32
style D fill:#ffebee,stroke:#c62828
style J fill:#ffebee,stroke:#c62828

The critical rule: when one layer changes, every layer after it must rebuild too. This is why instruction order matters so much.

What Invalidates the Cache

Docker decides whether to reuse a cached layer by checking:

Instruction TypeCache Check
FROMHas the base image changed?
COPY / ADDHave any of the copied files changed? (content hash)
RUNHas the command string changed?
ARGHas the build argument value changed?
Any instructionHas any previous layer been rebuilt?

If any check fails, that layer and all subsequent layers are rebuilt.

Bad vs Good Instruction Order

Bad: Source Code Before Dependencies

FROM node:20-alpine
WORKDIR /app
COPY . . # Copies everything, including source code
RUN npm ci --omit=dev # Installs dependencies
CMD ["node", "server.js"]

Problem: Every time you change a single line of source code, Docker must re-install all dependencies because the COPY . . layer changed, which invalidates the RUN npm ci layer after it.

Good: Dependencies Before Source Code

FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./ # Only dependency manifests
RUN npm ci --omit=dev # Cached unless manifests change
COPY . . # Source code changes only rebuild from here
CMD ["node", "server.js"]

Result: When only source code changes, Docker reuses the cached dependency layer. Dependency installation (often the slowest step) is skipped entirely.

Cache Behavior by Change Type

This table shows what rebuilds based on what you changed:

What ChangedWhat Rebuilds
Source code onlyCOPY . . and CMD layers (fast)
package.json / lock fileCOPY manifests, RUN install, COPY source, CMD (medium)
Base image tagEverything (full rebuild)
Dockerfile instruction textThat instruction and everything after it

The Build Context

When you run docker build ., the . tells Docker to send the current directory as the build context. The daemon receives all these files before executing any instructions.

A large build context causes:

  • Slow build starts (transferring hundreds of MB before any instruction runs)
  • Security risk (sensitive files like .env or SSH keys are sent to the daemon)
  • Bloated images if you accidentally COPY unwanted files

Check Your Context Size

Watch the first line of build output:

Sending build context to Docker daemon  156.7MB

If that number is larger than your actual source code, you need a .dockerignore file.

The .dockerignore File

Place a .dockerignore file in your project root to exclude files from the build context:

# Version control
.git
.gitignore

# Dependencies (will be installed inside the image)
node_modules

# Build outputs
dist
build
coverage

# Local environment
.env
.env.local

# OS files
.DS_Store
*.log
tmp

Impact Example

ProjectWithout .dockerignoreWith .dockerignore
Node.js app with node_modules~250 MB context~5 MB context
Python app with .git history~180 MB context~3 MB context

Language-Specific Cache Patterns

Node.js

COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .

Python

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

Go

COPY go.mod go.sum ./
RUN go mod download
COPY . .

The pattern is always the same: copy the dependency file, install, then copy the rest.

BuildKit Cache Mounts

BuildKit (enabled by default in modern Docker) supports cache mounts that persist package manager caches between builds. This avoids re-downloading packages even when the dependency layer is invalidated:

# syntax=docker/dockerfile:1
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]

The pip cache directory persists across builds, so packages already downloaded are reused even if requirements.txt changes.

Debugging Cache Misses

If a layer is rebuilding when you expect it to be cached:

  1. Check file changes: Did any file in the COPY scope change? Even a timestamp change on a file will invalidate the cache.
  2. Check instruction text: Did you modify the RUN command, even whitespace?
  3. Check build args: Did an ARG value change? ARG declarations before a RUN instruction will invalidate it.
  4. Check preceding layers: If any earlier layer rebuilt, all following layers rebuild too.

Use docker build --progress=plain . to see detailed build output showing which steps used cache and which rebuilt.

Key Takeaways

  • Docker caches each layer and reuses it if the inputs have not changed.
  • One changed layer forces all subsequent layers to rebuild -- so put rarely-changing instructions (dependency install) before frequently-changing ones (source code copy).
  • Always create a .dockerignore file to keep the build context small and secure.
  • Copy dependency manifests first, install, then copy source code. This is the most impactful optimization for build speed.
  • Use --progress=plain to debug cache misses.

What's Next

  • Continue to Multi-Stage Builds to learn how to separate build tools from runtime artifacts.