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
-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.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.