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 Type | Cache Check |
|---|---|
FROM | Has the base image changed? |
COPY / ADD | Have any of the copied files changed? (content hash) |
RUN | Has the command string changed? |
ARG | Has the build argument value changed? |
| Any instruction | Has 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 Changed | What Rebuilds |
|---|---|
| Source code only | COPY . . and CMD layers (fast) |
package.json / lock file | COPY manifests, RUN install, COPY source, CMD (medium) |
| Base image tag | Everything (full rebuild) |
| Dockerfile instruction text | That 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
.envor SSH keys are sent to the daemon) - Bloated images if you accidentally
COPYunwanted 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
| Project | Without .dockerignore | With .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 \
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:
- Check file changes: Did any file in the
COPYscope change? Even a timestamp change on a file will invalidate the cache. - Check instruction text: Did you modify the
RUNcommand, even whitespace? - Check build args: Did an
ARGvalue change?ARGdeclarations before aRUNinstruction will invalidate it. - 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
.dockerignorefile 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=plainto debug cache misses.
What's Next
- Continue to Multi-Stage Builds to learn how to separate build tools from runtime artifacts.