Stage 4 · Provision
Image Internals & Advanced Builds
Dockerfile Cache Strategy
Order COPY and RUN instructions around dependency locks, package caches, and cache-busting build arguments.
Cache Mechanics
Docker caches each layer after building it. On subsequent builds, Docker checks if the instruction and its inputs have changed. If nothing changed, Docker reuses the cached layer. If anything changed, Docker invalidates that layer and all layers after it.
FROM node:20-alpine # Cached if base image unchanged
WORKDIR /app # Cached if not changed
COPY package.json ./ # Cached if package.json unchanged
RUN npm install # Cached if package.json unchanged
COPY . . # Cached if no source files changed
RUN npm run build # Cached if no source files changedCache invalidation is cascading. If COPY . . changes (any source file), every layer after it rebuilds, even if those layers have not changed.
Dependency Ordering
The most important cache optimization is separating dependency installation from source code copying. Dependencies change less frequently than source code. Install dependencies first, then copy source code.
# Bad — source code change invalidates npm install
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci
# Good — dependency installation cached separately
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .In the good pattern, changing source code only invalidates COPY . . and layers after it. The npm ci layer is cached because package.json and package-lock.json did not change.
Cache Busting
# Bust cache with a build argument
ARG CACHE_BUST=1
RUN npm ci
# Force rebuild of specific layers
# docker build --build-arg CACHE_BUST=$(date +%s) .
# Pin dependency versions to control cache
FROM node:20.11-alpine AS deps
COPY package.json package-lock.json ./
RUN npm ci
# Different cache for different Node versions
FROM node:20.12-alpine AS deps-new
COPY package.json package-lock.json ./
RUN npm ciThe CACHE_BUST ARG lets you force a rebuild of specific layers without modifying source files. Use it when you need to refresh caches that would otherwise be stale.
Lock Files
# Node.js — use package-lock.json
COPY package.json package-lock.json ./
RUN npm ci # npm ci uses the lock file exactly
# Go — use go.sum
COPY go.mod go.sum ./
RUN go mod download
# Python — use requirements.txt with pinned versions
COPY requirements.txt ./
RUN pip install -r requirements.txt
# Rust — use Cargo.lock
COPY Cargo.toml Cargo.lock ./
RUN cargo build --releaseLock files guarantee reproducible builds. Always copy lock files before source code. The cache key changes only when dependencies change, not when source code changes.
COPY Order
# Order from least-changing to most-changing
FROM node:20-alpine
# 1. System dependencies (rarely change)
RUN apk add --no-cache tini
# 2. Application dependencies (change with package.json)
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# 3. Configuration (change occasionally)
COPY tsconfig.json ./
COPY next.config.js ./
# 4. Source code (change frequently)
COPY src/ ./src/Each COPY instruction creates a cache key based on the file content and path. By ordering from least-changing to most-changing, you maximize cache hits during development.
Advanced Patterns
# Copy only what's needed
COPY package.json package-lock.json ./
RUN npm ci
# Copy only specific directories
COPY src/ ./src/
COPY config/ ./config/
# Use .dockerignore to exclude files
# This prevents unnecessary cache invalidation
# Example .dockerignore:
# node_modules
# .git
# *.test.js
# coverage/Selective COPY instructions create smaller cache keys. Copying only package.json and package-lock.json means the cache key changes only when dependencies change, not when any file changes.
A well-structured Dockerfile with good cache strategy can reduce build times from minutes to seconds. Invest time in understanding cache behavior — it pays dividends on every build.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.