Publishing Modes — Framework-Dependent, Self-Contained and Single File

When you run dotnet publish, .NET packages your application for deployment. There are three publishing modes, each with different trade-offs between deployment size, runtime requirements, and startup performance. Understanding these modes is the first step in designing your deployment pipeline — whether you are deploying to a managed cloud service that already has .NET installed, building Docker images, or distributing standalone executables.

The Three Publishing Modes

// ── 1. Framework-Dependent (default) ─────────────────────────────────────
// Output: application DLLs only (~500KB-5MB)
// Requires: .NET runtime pre-installed on target machine
// Best for: Docker (aspnet base image has runtime), cloud PaaS (Azure App Service)
dotnet publish -c Release -o ./publish

// ── 2. Self-Contained ─────────────────────────────────────────────────────
// Output: application + entire .NET runtime (~60-100MB)
// Requires: nothing — runs on any machine with the target OS/arch
// Best for: Windows Services, on-premises servers without .NET installed
dotnet publish -c Release --runtime linux-x64 --self-contained -o ./publish

// ── 3. Single File ────────────────────────────────────────────────────────
// Output: one compressed executable file
// Can be combined with self-contained for a truly standalone single binary
dotnet publish -c Release --runtime linux-x64 --self-contained \
    /p:PublishSingleFile=true \
    /p:PublishTrimmed=true \    // remove unused BCL code (reduces size ~40%)
    -o ./publish

// ── ReadyToRun — pre-compile IL to native for faster startup ─────────────
dotnet publish -c Release --runtime linux-x64 \
    /p:PublishReadyToRun=true \
    -o ./publish
// R2R pre-compiles to native at publish time, reducing JIT startup cost
// Recommended for containerised APIs where cold-start latency matters
Note: ReadyToRun (R2R) and Native AOT are different. R2R produces assemblies that contain both IL and pre-compiled native code — the JIT can still compile methods it missed, making R2R a safe optimisation with no compatibility restrictions. Native AOT compiles everything to native at publish time with no JIT at runtime, producing a much smaller, faster-starting binary, but with restrictions: no runtime code generation, limited reflection, and not all NuGet packages are compatible. For most ASP.NET Core Web APIs, R2R is the right choice; Native AOT suits microservices and serverless functions.
Tip: Always publish with -c Release (Release configuration). Debug builds include debug symbols, disable most optimisations, and produce significantly larger binaries. A common mistake in CI/CD pipelines is publishing in Debug mode — the application runs slower and uses more memory than it should. Add the configuration check to your Dockerfile: dotnet publish -c Release and verify it in your pipeline definition.
Warning: PublishTrimmed=true uses static analysis to remove unused code from the BCL and third-party libraries. Some libraries use reflection to load types dynamically — trimming can remove types that are used only through reflection, causing TypeLoadException or MissingMethodException at runtime. Test thoroughly after enabling trimming. EF Core, Serilog, and AutoMapper all have trimming issues — check each library’s trimming compatibility before enabling it in production.

Build and Publish in .csproj

// ── .csproj publish settings ───────────────────────────────────────────────
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>

    <!-- Production publish settings -->
    <PublishReadyToRun>true</PublishReadyToRun>   <!-- faster cold start -->
    <RuntimeIdentifier>linux-x64</RuntimeIdentifier>  <!-- for Docker -->

    <!-- Optional: strip debug symbols in release -->
    <DebugSymbols>false</DebugSymbols>
    <DebugType>none</DebugType>
  </PropertyGroup>
</Project>

Common Mistakes

Mistake 1 — Publishing in Debug configuration (slow, large binaries in production)

❌ Wrong — accidentally publishing without -c Release:

dotnet publish -o ./publish   // defaults to Debug configuration!

✅ Correct — always specify -c Release in production pipelines.

Mistake 2 — Enabling PublishTrimmed without testing (missing reflection-loaded types)

❌ Wrong — enabling trimming causes TypeLoadException in production when a trimmed type is needed at runtime.

✅ Correct — test trimmed builds with a full integration test suite before enabling in production.

🧠 Test Yourself

Your ASP.NET Core API deploys to Docker using the official mcr.microsoft.com/dotnet/aspnet:8.0 base image. Should you use framework-dependent or self-contained publishing? Why?