Publishing and deployment

Framework-dependent versus self-contained output, containerising a service, and the trimming and AOT trade-offs you must measure.

Choosing a publish mode

ModeCommand shapeTrade-off
Framework-dependentdotnet publish -c ReleaseSmallest output; the host must have a compatible runtime installed
Self-contained-r linux-x64 --self-contained trueShips the runtime; large (roughly 60-80 MB) but no runtime prerequisite
Single file-p:PublishSingleFile=trueOne executable to copy; extraction cost and slower first start unless combined with R2R
ReadyToRun-p:PublishReadyToRun=trueAhead-of-time compiled IL for faster startup; bigger files, more build time
Native AOT-p:PublishAot=trueNo runtime, minimal memory and fast start; no dynamic loading, limited reflection
# framework-dependent: the default production choice
dotnet publish src/Shop.Api -c Release -o out

# self-contained for a host you do not control
dotnet publish src/Shop.Api -c Release -r linux-x64 --self-contained true -o out

# container image that runs on the runtime-only base image
dotnet publish src/Shop.Api -c Release -r linux-x64 --self-contained false -o out
  • Set InvariantGlobalization=true when you do not need culture-specific collation — it drops ICU and shrinks self-contained output substantially.
  • -o pointing outside the project directory avoids the published files being picked up by the next glob-based build.
  • Publish with the same configuration that produced your tests. Rebuilding differently for deployment invalidates the testing you did.

Containers and hosts

A multi-stage Dockerfile keeps the SDK out of the runtime image. The build stage restores against the lock file, publishes, and the final stage copies only the output.

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY *.sln ./
COPY src/Shop.Api/*.csproj src/Shop.Api/
COPY src/Shop.Core/*.csproj src/Shop.Core/
RUN dotnet restore src/Shop.Api/Shop.Api.csproj
COPY . .
RUN dotnet publish src/Shop.Api/Shop.Api.csproj -c Release -o /app --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app .
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
USER $APP_UID
ENTRYPOINT ["dotnet", "Shop.Api.dll"]
  • Copy project files and restore before copying source so the package layer is cached across code changes.
  • The aspnet runtime image is smaller than the SDK image and contains no compiler.
  • Run as a non-root user; the official images define APP_UID for exactly that.
  • Read the port and connection strings from environment variables so the same image promotes from staging to production.
⚠️
Framework-dependent output and the runtime image version must stay in step. Publishing for net8.0 then running on a net7.0 runtime fails at startup with a missing-framework error that no amount of container tweaking will fix.

Trimming and native AOT

Trimming removes unreferenced code, and native AOT compiles ahead of time into a native binary. Both rely on the compiler proving what is reachable, so anything reflective breaks unless it is preserved or replaced.

<PropertyGroup>
  <PublishTrimmed>true</PublishTrimmed>
  <TrimMode>partial</TrimMode>
  <PublishAot>true</PublishAot>
  <InvariantGlobalization>true</InvariantGlobalization>
  <StackTraceSupport>false</StackTraceSupport>
</PropertyGroup>
// reflection-based serialisation is the classic AOT casualty
// prefer a source-generated context
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(List<Order>))]
internal partial class ShopJsonContext : JsonSerializerContext
{
}

// register it once
builder.Services.ConfigureHttpJsonOptions(o =>
    o.SerializerOptions.TypeInfoResolverChain.Insert(0, ShopJsonContext.Default));
  • Publish warnings starting with IL2 name the exact call site that cannot be statically analysed — fix them, do not suppress them.
  • Measure before committing to AOT: a CLI tool benefits enormously, a reflection-heavy framework integration usually does not.
  • Keep the JIT build as your default. AOT is a deployment optimisation, not an architectural requirement.

FAQ

Self-contained or framework-dependent?
Framework-dependent in containers and on hosts you manage, because the base image already carries the runtime and patching is a tag change. Self-contained when you hand someone a binary for a machine you cannot provision.
Should I publish ReadyToRun?
If cold start matters. It typically cuts startup noticeably at the cost of a larger output and a slower build; measure your own service rather than trusting a general figure.

The dotnet CLI workflow Minimal APIs

Last refreshed 2026-09-18.