Skip to content

Bits of .NET

Daily micro-tips for C#, SQL, performance, and scalable backend engineering.

  • Asp.Net Core
  • C#
  • SQL
  • JavaScript
  • CSS
  • About
  • ErcanOPAK.com
  • No Access
  • Privacy Policy
Asp.Net Core

.NET Core: Reduce Docker Image Build Time with Layer Caching Optimization

- 01.02.26 | 01.02.26 - ErcanOPAK

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

Related posts:

Use ProblemDetails for API Errors

.NET Core: Use Top-Level Statements to Skip Program Class Boilerplate

.NET Core: Preventing 'Captive Dependency' Memory Leaks

Post Views: 3

Post navigation

Speed Up Git Operations 10x by Cleaning Up Bloated Repository History
.NET Core: Fix Memory Leaks by Understanding IDisposable and Using Patterns

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

April 2026
M T W T F S S
 12345
6789101112
13141516171819
20212223242526
27282930  
« Mar    

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (950)
  • How to add default value for Entity Framework migrations for DateTime and Bool (858)
  • Get the First and Last Word from a String or Sentence in SQL (836)
  • How to select distinct rows in a datatable in C# (805)
  • How to make theater mode the default for Youtube (753)
  • Add Constraint to SQL Table to ensure email contains @ (578)
  • How to enable, disable and check if Service Broker is enabled on a database in SQL Server (564)
  • Average of all values in a column that are not zero in SQL (531)
  • How to use Map Mode for Vertical Scroll Mode in Visual Studio (489)
  • Find numbers with more than two decimal places in SQL (447)

Recent Posts

  • C#: Use Init-Only Setters for Immutable Objects After Construction
  • C#: Use Expression-Bodied Members for Concise Single-Line Methods
  • C#: Enable Nullable Reference Types to Eliminate Null Reference Exceptions
  • C#: Use Record Types for Immutable Data Objects
  • SQL: Use CTEs for Readable Complex Queries
  • SQL: Use Window Functions for Advanced Analytical Queries
  • .NET Core: Use Background Services for Long-Running Tasks
  • .NET Core: Use Minimal APIs for Lightweight HTTP Services
  • Git: Use Cherry-Pick to Apply Specific Commits Across Branches
  • Git: Use Interactive Rebase to Clean Up Commit History Before Merge

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (950)
  • How to add default value for Entity Framework migrations for DateTime and Bool (858)
  • Get the First and Last Word from a String or Sentence in SQL (836)
  • How to select distinct rows in a datatable in C# (805)
  • How to make theater mode the default for Youtube (753)

Recent Posts

  • C#: Use Init-Only Setters for Immutable Objects After Construction
  • C#: Use Expression-Bodied Members for Concise Single-Line Methods
  • C#: Enable Nullable Reference Types to Eliminate Null Reference Exceptions
  • C#: Use Record Types for Immutable Data Objects
  • SQL: Use CTEs for Readable Complex Queries

Social

  • ErcanOPAK.com
  • GoodReads
  • LetterBoxD
  • Linkedin
  • The Blog
  • Twitter
© 2026 Bits of .NET | Built with Xblog Plus free WordPress theme by wpthemespace.com