How does Docker layer caching work and how do you optimise it?
Docker images are layered. Each Dockerfile instruction creates a layer. Build cache reuses a layer when:
- The instruction is identical to a previous build
- All input files referenced have unchanged content hashes
When ANY layer changes, all subsequent layers rebuild. Layer order is therefore everything.
Bad — every code change re-runs dotnet restore:
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY . . # touches everything
RUN dotnet restore # invalidated by ANY src change
RUN dotnet publish -c Release -o /app/publish
A 1-character edit in Program.cs invalidates the COPY layer and the restore layer. Restore alone takes 30-60 seconds.
Good — copy csproj first, restore, then copy source:
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
# Layer 1: just the project files — rarely change
COPY *.sln ./
COPY src/MyApi/*.csproj src/MyApi/
COPY src/MyApi.Domain/*.csproj src/MyApi.Domain/
RUN dotnet restore src/MyApi/MyApi.csproj # cached unless csproj changes
# Layer 2: source code — changes often
COPY src/ src/
RUN dotnet publish src/MyApi/MyApi.csproj -c Release -o /app/publish --no-restore
Now editing a .cs file invalidates only the source layer; restore (the slow one) stays cached. CI builds drop from 90s to 15s.
Other layer-caching tips:
- Combine RUN steps to avoid leftover intermediate files:
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
-
.dockerignoreis critical. Without it,COPY . .pulls inbin/,obj/,.git/,node_modules/, secrets — bloating the build context and invalidating layers on irrelevant file changes. -
--mount=type=cachefor package-manager caches (BuildKit):
RUN --mount=type=cache,target=/root/.nuget/packages \
dotnet restore
NuGet packages cache across builds without bloating the image.
Test it: rebuild your image twice in a row. If the second build doesn't take milliseconds, you have a caching problem.