Build Cache
Docker build cache is a critical mechanism for reducing build times. Understanding how caching works and how to optimize it can dramatically improve your development and CI/CD workflows.
How Build Cache Works
Every instruction in a Dockerfile creates a layer. Docker caches each layer and reuses it when the inputs haven't changed.
Instruction 1: FROM node:20-alpine ──▶ Cached ✅ (same base image)
Instruction 2: WORKDIR /app ──▶ Cached ✅ (no change)
Instruction 3: COPY package.json ./ ──▶ Cached ✅ (file unchanged)
Instruction 4: RUN npm ci ──▶ Cached ✅ (previous layer unchanged)
Instruction 5: COPY . . ──▶ MISS ❌ (source code changed)
Instruction 6: RUN npm run build ──▶ MISS ❌ (previous layer changed)
Instruction 7: CMD ["node", "server"] ──▶ MISS ❌ (previous layer changed)Cache Invalidation Rules
| Instruction | Cache Invalidated When |
|---|---|
FROM | Base image digest changes |
RUN | Command string changes, or previous layer invalidated |
COPY | File content (checksum) changes |
ADD | File content changes, URL content changes |
ENV | Value changes |
ARG | Value changes (only affects subsequent layers) |
WORKDIR | Path changes |
EXPOSE | Port specification changes |
LABEL | Label content changes |
The Cache Chain
Cache invalidation is cascading — when one layer's cache is invalidated, all subsequent layers are also invalidated:
Layer 1: FROM node:20-alpine ✅ Cached
Layer 2: WORKDIR /app ✅ Cached
Layer 3: COPY package.json . ✅ Cached
Layer 4: RUN npm ci ✅ Cached
Layer 5: COPY . . ❌ INVALIDATED (source changed)
Layer 6: RUN npm run build ❌ Rebuilt (cascade)
Layer 7: CMD ["node", "server"] ❌ Rebuilt (cascade)Local Build Cache
Inline Cache
The default cache is stored alongside images in the local image store:
# Build with local cache (default behavior)
docker build -t my-app:1.0 .
# Second build uses cached layers
docker build -t my-app:1.0 . # Much faster!
# Force rebuild without cache
docker build --no-cache -t my-app:1.0 .Cache Mounts
Cache mounts persist package manager data between builds even when the RUN instruction is re-executed:
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
# The npm cache directory persists between builds
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production
COPY . .
CMD ["node", "server.js"]Cache Mount Options
| Option | Description | Example |
|---|---|---|
target | Mount path inside container | target=/root/.npm |
id | Cache identifier | id=my-npm-cache |
sharing | Sharing mode (shared, private, locked) | sharing=locked |
mode | File permissions | mode=0755 |
uid / gid | Owner UID/GID | uid=1000,gid=1000 |
from | Source stage for cache | from=builder |
source | Source path for pre-populated cache | source=/cache |
# Multiple cache mounts with specific IDs
RUN --mount=type=cache,id=npm-cache,target=/root/.npm \
--mount=type=cache,id=build-cache,target=/app/.next/cache \
npm ci && npm run buildExternal Cache Backends
BuildKit supports exporting and importing cache from external sources, which is essential for CI/CD pipelines.
Registry Cache
Store build cache in a container registry:
# Build and export cache to registry
docker buildx build \
--cache-to type=registry,ref=myuser/my-app:buildcache,mode=max \
--cache-from type=registry,ref=myuser/my-app:buildcache \
-t my-app:1.0 \
--push .
# Use the cached layers in a subsequent build
docker buildx build \
--cache-from type=registry,ref=myuser/my-app:buildcache \
-t my-app:1.1 \
--push .Inline Cache
Embed cache metadata within the image itself:
# Build with inline cache metadata
docker buildx build \
--cache-to type=inline \
--cache-from type=registry,ref=myuser/my-app:latest \
-t myuser/my-app:latest \
--push .Local Directory Cache
Store cache in a local directory (useful for CI):
# Export cache to local directory
docker buildx build \
--cache-to type=local,dest=/tmp/docker-cache \
--cache-from type=local,src=/tmp/docker-cache \
-t my-app:1.0 .GitHub Actions Cache
# Use GitHub Actions cache backend
docker buildx build \
--cache-to type=gha,mode=max \
--cache-from type=gha \
-t my-app:1.0 .S3 Cache
# Use S3 as cache backend
docker buildx build \
--cache-to type=s3,region=us-east-1,bucket=my-build-cache,name=my-app \
--cache-from type=s3,region=us-east-1,bucket=my-build-cache,name=my-app \
-t my-app:1.0 .Cache Backend Comparison
| Backend | Shared Across | Setup Complexity | Speed | Best For |
|---|---|---|---|---|
| Local | Single machine | None | Fastest | Local development |
| Inline | Image pulls | Low | Fast | Simple CI/CD |
| Registry | All machines | Low | Medium | Multi-machine CI |
| Local Directory | CI runners | Low | Fast | CI/CD with persistent storage |
| GitHub Actions | GHA workflows | Low | Fast | GitHub Actions CI |
| S3 | All machines | Medium | Medium | AWS-based CI/CD |
| Azure Blob | All machines | Medium | Medium | Azure-based CI/CD |
Cache Mode
| Mode | Description | Size | Use Case |
|---|---|---|---|
mode=min | Cache only final stage layers | Smaller | Inline cache, simple images |
mode=max | Cache all intermediate layers | Larger | Multi-stage builds, CI/CD |
# Max mode: cache all layers (recommended for CI)
docker buildx build \
--cache-to type=registry,ref=cache:latest,mode=max \
--cache-from type=registry,ref=cache:latest \
-t my-app:1.0 .CI/CD Cache Strategies
GitHub Actions
name: Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myuser/my-app:latest
cache-from: type=gha
cache-to: type=gha,mode=maxGitLab CI
build:
image: docker:latest
services:
- docker:dind
variables:
DOCKER_BUILDKIT: "1"
script:
- docker buildx create --use
- docker buildx build
--cache-from type=registry,ref=$CI_REGISTRY_IMAGE:cache
--cache-to type=registry,ref=$CI_REGISTRY_IMAGE:cache,mode=max
--push
-t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
.Cache Management
# View build cache usage
docker buildx du
# View detailed cache info
docker buildx du --verbose
# Prune build cache
docker buildx prune
# Prune cache older than 7 days
docker buildx prune --filter "until=168h"
# Prune all cache
docker buildx prune --all
# Prune with size limit (keep 10GB)
docker buildx prune --keep-storage 10737418240Cache Debugging
# Show cache hits and misses during build
docker build --progress=plain -t my-app:1.0 . 2>&1 | grep -E "CACHED|DONE"
# Check if a specific layer is cached
docker build --progress=plain -t my-app:1.0 . 2>&1
# Example output:
# #5 [2/6] WORKDIR /app
# #5 CACHED
#
# #8 [5/6] COPY . .
# #8 DONE 0.3s ← Not cached, was rebuiltCache Optimization Best Practices
| Practice | Impact | Description |
|---|---|---|
| Order by change frequency | ⭐⭐⭐ | Least changed instructions first |
| Separate dependency install | ⭐⭐⭐ | Copy manifests before source code |
| Use cache mounts | ⭐⭐⭐ | Persist package manager caches |
| Use external cache in CI | ⭐⭐⭐ | Share cache across CI runs |
Use mode=max for CI | ⭐⭐ | Cache all intermediate layers |
| Pin base image versions | ⭐⭐ | Prevent unexpected cache invalidation |
Use .dockerignore | ⭐⭐ | Prevent irrelevant file changes |
Avoid --no-cache in CI | ⭐⭐ | Unless cache corruption is suspected |
Next Steps
- BuildKit Guide — Deep dive into BuildKit features
- Build Optimization — Optimize build speed and size
- Docker Build Overview — Build fundamentals
- Multi-stage Builds — Cache across multi-stage builds
- Image Building Best Practices — Comprehensive building guide