Your .NET Docker builds take 5-10 minutes every time? Improper Dockerfile layer ordering is killing your cache efficiency.
Bad Dockerfile (No Cache Optimization):
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# This invalidates cache on EVERY code change
COPY . .
RUN dotnet restore
RUN dotnet build -c Release
RUN dotnet publish -c Release -o /app/publish
# Result: 8-10 minute builds every time
Optimized Dockerfile (Smart Layer Caching):
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Step 1: Copy only project files (rarely change)
COPY ["MyApp/MyApp.csproj", "MyApp/"]
COPY ["MyApp.Data/MyApp.Data.csproj", "MyApp.Data/"]
COPY ["MyApp.Services/MyApp.Services.csproj", "MyApp.Services/"]
# Step 2: Restore packages (cached unless .csproj changes)
RUN dotnet restore "MyApp/MyApp.csproj"
# Step 3: Copy source code (changes frequently)
COPY . .
# Step 4: Build and publish
WORKDIR "/src/MyApp"
RUN dotnet build -c Release --no-restore
RUN dotnet publish -c Release -o /app/publish --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]
# Result: 30-60 second builds on code changes
# Result: 3-5 minute builds only when packages change
Why This Works:
Docker caches each layer. When a file changes:
• That layer rebuilds
• ALL subsequent layers rebuild
• Previous layers use cache
By copying .csproj files first (which rarely change), the expensive ‘dotnet restore’ layer stays cached even when you modify source code.
Build Time Comparison:
// Bad Dockerfile:
Code change → Copy everything (cache miss) → Restore packages (5 min) → Build (2 min) → Publish (1 min)
Total: 8 minutes EVERY time
// Optimized Dockerfile:
Code change → .csproj unchanged (cache hit) → Restore skipped → Copy code → Build (1 min) → Publish (30s)
Total: 90 seconds for code changes
Package change → .csproj changed (cache miss) → Restore (3 min) → Build (1 min) → Publish (30s)
Total: 5 minutes only when adding NuGet packages
Multi-Project Solution Pattern:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy all .csproj files with correct directory structure
COPY ["src/MyApp.Web/MyApp.Web.csproj", "src/MyApp.Web/"]
COPY ["src/MyApp.Core/MyApp.Core.csproj", "src/MyApp.Core/"]
COPY ["src/MyApp.Infrastructure/MyApp.Infrastructure.csproj", "src/MyApp.Infrastructure/"]
COPY ["tests/MyApp.Tests/MyApp.Tests.csproj", "tests/MyApp.Tests/"]
# Restore all projects
RUN dotnet restore "src/MyApp.Web/MyApp.Web.csproj"
# Now copy everything else
COPY . .
# Build only what you need (exclude tests in production image)
RUN dotnet build "src/MyApp.Web/MyApp.Web.csproj" -c Release --no-restore
RUN dotnet publish "src/MyApp.Web/MyApp.Web.csproj" -c Release -o /app/publish --no-restore
Advanced – Use BuildKit for Parallel Builds:
# Enable BuildKit (Docker 18.09+)
export DOCKER_BUILDKIT=1
# Or in docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
environment:
DOCKER_BUILDKIT: 1
# BuildKit benefits:
# - Parallel layer building
# - Improved caching (cache mounts)
# - Better secrets handling
Cache Mount for NuGet Packages:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyApp.csproj", "."]
# Mount NuGet cache (BuildKit feature)
RUN --mount=type=cache,target=/root/.nuget/packages \
dotnet restore
COPY . .
RUN --mount=type=cache,target=/root/.nuget/packages \
dotnet publish -c Release -o /app/publish --no-restore
# NuGet packages cached across builds
# Even faster restores!
Verify Cache Usage:
# Build with cache details
docker build --progress=plain -t myapp .
# Look for:
# CACHED [stage-0 2/8] COPY ["MyApp.csproj", "."]
# CACHED [stage-0 3/8] RUN dotnet restore
# If you see "RUN dotnet restore" without "CACHED", your layer ordering needs work
