Docker Layer Caching Strategies to Speed Up Builds
Master Docker layer caching: instruction ordering, cache mounts, BuildKit inline cache, and CI registry caching for faster builds.
What you'll learn
- ✓How Docker layer caching works and why instruction order matters
- ✓BuildKit cache mounts for package managers
- ✓Registry-based caching for CI/CD pipelines
Prerequisites
- •Basic Dockerfile knowledge — see Dockerfile Best Practices
- •Familiarity with Docker build commands
Every instruction in a Dockerfile creates a layer. Docker caches each layer and reuses it if nothing has changed. When a layer’s cache is invalidated, every subsequent layer is rebuilt from scratch. Understanding this mechanism is the key to fast Docker builds.
How Layer Caching Works
Docker checks each instruction against its cache in order. For COPY and ADD, it compares file checksums. For RUN, it checks if the command string has changed. The moment any layer’s cache is invalid, all layers after it are rebuilt.
Consider this Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]
Every time you change any source file, COPY . . invalidates the cache, and npm ci runs again even though your dependencies have not changed. A full npm ci on a large project can take minutes.
Strategy 1: Order Instructions by Change Frequency
Put things that change rarely at the top and things that change often at the bottom:
FROM node:20-alpine
WORKDIR /app
# Step 1: Copy only dependency files (change rarely)
COPY package.json package-lock.json ./
# Step 2: Install dependencies (cached unless package files change)
RUN npm ci
# Step 3: Copy source code (changes frequently)
COPY . .
# Step 4: Build (runs only when source changes)
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]
Now when you edit source code, only steps 3 and 4 run. The npm ci step is cached because package.json and package-lock.json did not change. On a project with 500+ dependencies, this saves 30-90 seconds per build.
The same pattern works for any language:
# Go
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o server .
# Python
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .
# Rust
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main(){}" > src/main.rs
RUN cargo build --release
COPY src/ src/
RUN cargo build --release
Strategy 2: BuildKit Cache Mounts
Cache mounts persist package manager caches between builds without including them in the final image:
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN npm run build
The npm cache directory is stored outside the image layers. On the next build, npm finds packages in the cache and skips downloading them. The cache mount is not included in any layer, so it does not increase image size.
Common cache mount targets:
# npm
RUN --mount=type=cache,target=/root/.npm npm ci
# pip
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
# Go modules
RUN --mount=type=cache,target=/go/pkg/mod go mod download
# Go build cache
RUN --mount=type=cache,target=/root/.cache/go-build go build -o server .
# apt
RUN --mount=type=cache,target=/var/cache/apt \
apt-get update && apt-get install -y curl
Cache mounts require BuildKit. Enable it with DOCKER_BUILDKIT=1 or configure it as the default in /etc/docker/daemon.json.
Strategy 3: Use .dockerignore Effectively
A large build context slows down every build because Docker sends the entire context to the daemon before building. It also causes unnecessary cache invalidation when COPY . . picks up files that do not affect the build.
Create a thorough .dockerignore:
.git
.gitignore
node_modules
dist
build
coverage
test
tests
__tests__
*.test.js
*.spec.js
*.md
.env*
.vscode
.idea
docker-compose*.yml
Dockerfile*
.dockerignore
This reduces context transfer time and prevents test file changes from invalidating your source copy layer.
Strategy 4: Registry-Based Caching for CI
In CI environments, local layer cache is empty on every run. You can use registry-based caching to persist layers across builds.
Inline cache — embed cache metadata in the image:
# Build and push with cache metadata
docker buildx build \
--cache-to type=inline \
--push \
-t registry.example.com/myapp:latest .
# Next build uses the pushed image as cache
docker buildx build \
--cache-from type=registry,ref=registry.example.com/myapp:latest \
-t registry.example.com/myapp:$(git rev-parse --short HEAD) .
Registry cache — store cache layers separately from the image:
docker buildx build \
--cache-to type=registry,ref=registry.example.com/myapp:buildcache,mode=max \
--cache-from type=registry,ref=registry.example.com/myapp:buildcache \
--push \
-t registry.example.com/myapp:latest .
The mode=max flag caches all layers, including intermediate stages in multi-stage builds. Without it, only the final stage layers are cached.
GitHub Actions example:
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/myorg/myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
The type=gha cache backend uses GitHub Actions cache storage, which persists across workflow runs.
Strategy 5: Selective COPY for Monorepos
In a monorepo, avoid copying the entire repo when only one service needs to build:
FROM node:20-alpine AS builder
WORKDIR /app
# Copy workspace root dependency files
COPY package.json package-lock.json ./
COPY packages/shared/package.json ./packages/shared/
COPY packages/api/package.json ./packages/api/
# Install all workspace dependencies
RUN npm ci
# Copy only the packages this service needs
COPY packages/shared/ ./packages/shared/
COPY packages/api/ ./packages/api/
# Build
WORKDIR /app/packages/api
RUN npm run build
Changes to packages/frontend/ do not invalidate the cache for the API build.
Strategy 6: Combine RUN Instructions Wisely
Each RUN creates a layer. Combining commands reduces layers, but too much combining hurts caching:
# Bad: one huge layer, any change rebuilds everything
RUN apt-get update && \
apt-get install -y curl git build-essential && \
npm ci && \
npm run build
# Good: group by change frequency
RUN apt-get update && \
apt-get install -y --no-install-recommends curl git && \
rm -rf /var/lib/apt/lists/*
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
Group system dependencies together since they rarely change. Keep application dependency installation and build steps separate since they change at different rates.
Measuring Cache Effectiveness
Use --progress=plain to see which steps are cached:
docker build --progress=plain -t myapp . 2>&1 | grep -E "CACHED|RUN"
Cached steps show CACHED in the output. If a step you expected to be cached is running, check what changed above it in the Dockerfile.
Check build times:
time docker build -t myapp .
Summary
Docker layer caching is the most effective way to speed up builds. The strategies in priority order: reorder instructions so dependencies are copied before source code, use cache mounts for package managers, maintain a thorough .dockerignore, and set up registry-based caching for CI pipelines. A well-cached build that takes 10 minutes uncached can drop to under 30 seconds on subsequent runs.
Related articles
- Docker Docker BuildKit Advanced Features You Should Be Using
Master BuildKit's cache mounts, secret mounts, SSH forwarding, and parallel builds to speed up Docker image creation and keep secrets out of layers.
- Docker Docker Compose Patterns for Production
Production-ready Docker Compose patterns: override files, healthchecks, resource limits, logging, and secrets management.
- Docker Docker Multi-Stage Builds: Advanced Optimization Techniques
Go beyond basic multi-stage builds. Learn cache mounts, parallel stages, and distroless bases to shrink Docker images by 90%.
- Docker Dockerfile Best Practices
Write production-ready Dockerfiles: layer caching, multi-stage builds, .dockerignore, security hardening, slim base images, and patterns that keep builds fast and images small.