dotnet publish

Produce deployable output for a target OS and CPU: framework-dependent or self-contained, single file, ReadyToRun, Native AOT or a container image.

Signature
dotnet publish [<project>] [-c <cfg>] [-r <rid> | --os <os> --arch <arch>] [--self-contained [true|false]] [-o <dir>] [--no-restore] [--no-build] [-p:<Name>=<Value>] [-t:PublishContainer]
  • Added in .NET .NET Core 1.0

Parameters

ParameterTypeDescription
$-c, --configurationoptionalstring= Release (since .NET 8)Build configuration.
$-r, --runtimeoptionalRID= none: portable, framework-dependentTarget runtime identifier, e.g. linux-x64, linux-arm64, linux-musl-x64. Required for self-contained, single-file, ReadyToRun and AOT.
$--os / --archoptionalstring= the host OS and architecture when only one is givenBuild the RID from parts, e.g. --os linux --arch x64.
$--self-containedoptionalbool= falseInclude the .NET runtime in the output so the target needs nothing installed.
$-o, --outputoptionalpath= bin/Release/net10.0/[rid/]publish/Publish folder.
$-p:PublishSingleFileoptionalbool= falseBundle the app into one executable file.
$-p:PublishReadyToRunoptionalbool= falsePrecompile IL to native code to cut JIT time at start-up.
$-t:PublishContaineroptionaltargetBuild an OCI image instead of (as well as) a folder; ContainerRepository, ContainerImageTags and ContainerFamily configure it.

Return value

process exit code0 when the publish folder (or container image) was produced. Non-zero on build errors, unsupported option combinations or AOT/trimming errors.

Errors

Fails with an error when an option needs a RID and none is given, when trimming or AOT analysis finds errors, or when Native AOT is asked to target another operating system.

Changelog

  • PHP .NET 8Release became the default configuration, and specifying a RID no longer implies self-contained.

dotnet publish builds the project and copies everything needed to run it into one folder: your assemblies, NuGet dependencies, appsettings*.json, static files and a .runtimeconfig.json. Since .NET 8 it builds Release by default. Deploy that folder (or the image built from it), never bin/Debug.

The deployment shapes

ShapeCommandTarget needsTrade-off
Framework-dependent, portabledotnet publishThe ASP.NET Core 10 runtimeSmallest output; the default for aspnet:10.0 images
Framework-dependent, RID-specific-r linux-x64The runtimeAdds a native apphost and only that RID's native assets
Self-contained-r linux-x64 --self-containedNothing (runtime-deps)Bigger; you ship runtime patches yourself
Single file-p:PublishSingleFile=trueDepends on self-containedAssembly.Location is empty
ReadyToRun-p:PublishReadyToRun=trueSame as aboveFaster start-up, larger files, still JITs hot paths
Native AOT<PublishAot>true</PublishAot> in the projectNothingFast start, small memory; no runtime code generation, must build on the target OS

For an API in a container, framework-dependent on the aspnet image is the default choice: Microsoft patches the runtime in the base image and a rebuild picks it up. Self-contained moves that patching duty to you.

Runtime identifiers

A RID is os-arch: linux-x64, linux-arm64, linux-musl-x64 for Alpine, win-x64, osx-arm64. Since .NET 8 a RID alone does not make the output self-contained; say --self-contained when you want that. Publishing on an Apple silicon laptop without a RID and copying the apphost to an x64 server fails, while the portable DLL run with dotnet Orders.Api.dll works anywhere.

Container images

dotnet publish -t:PublishContainer builds an OCI image with the SDK, no Dockerfile. ContainerRepository, ContainerImageTags and ContainerFamily (for example noble-chiseled) shape it; ContainerRegistry pushes it. Without a RID the image targets the build machine's architecture, so an arm64 laptop produces an arm64 image that crashes with an exec format error on an amd64 cluster. Pass --os linux --arch x64 for images you push.

Failure modes

  • appsettings.Development.json is published too. It is ignored unless the environment is Development, but it must not hold anything secret.
  • Trimming (PublishTrimmed) removes code reached only through reflection; heed the trim warnings or the app fails at run time on a path the build never exercised.
  • --no-build with a different -c or RID than the build publishes nothing useful or fails; build and publish with identical arguments.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Publish the orders API as an amd64 chiseled container image from an arm64 CI runner with --os linux --arch x64
  • Ship a self-contained single-file worker to a VM that has no .NET runtime installed
  • Cut cold-start time of a scale-to-zero API with ReadyToRun or Native AOT publishing

Frequently asked questions

Do I still need -c Release on dotnet publish?

Not since .NET 8, which made Release the default for publish. Passing it explicitly is harmless and keeps scripts readable when build and test steps also need -c Release.

Does -r linux-x64 make my app self-contained?

No, not since .NET 8. It produces RID-specific framework-dependent output. Add --self-contained to bundle the runtime.

Why does my published single-file app fail to find its files?

Assembly.Location is empty in single-file and Native AOT apps. Build paths from AppContext.BaseDirectory instead.

More examples

Container publish settings in the project

With these properties, <code>dotnet publish --os linux --arch x64 -t:PublishContainer</code> builds a chiseled image based on <code>aspnet:10.0-noble-chiseled</code>. The base chiseled family has neither ICU nor tzdata, so invariant globalization is set; choose <code>noble-chiseled-extra</code> as the <code>ContainerFamily</code> when the API needs culture-specific formatting or time zone conversion, because <code>TimeZoneInfo.FindSystemTimeZoneById</code> throws <code>TimeZoneNotFoundException</code> without tzdata.

Example · xml
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>

    <ContainerRepository>shop/orders-api</ContainerRepository>
    <ContainerFamily>noble-chiseled</ContainerFamily>
    <ContainerImageTags>1.4.2;latest</ContainerImageTags>
    <InvariantGlobalization>true</InvariantGlobalization>
  </PropertyGroup>
</Project>

Publishing in a multi-stage Dockerfile

Restore runs on the project files, lock files and <code>nuget.config</code> alone so Docker caches it. Every referenced project's <code>.csproj</code> must be there, <code>nuget.config</code> keeps the <code>packageSourceMapping</code> that limits <code>Contoso.*</code> to the internal feed, and <code>--locked-mode</code> restores exactly the committed versions. The feed token arrives as a BuildKit secret (<code>docker build --secret id=contoso_feed_pat,env=CONTOSO_FEED_PAT .</code>), never an <code>ARG</code> or <code>ENV</code> that stays in the image history. Publish then needs <code>--no-restore</code> and no <code>-c</code>, since Release is the default. <code>UseAppHost=false</code> skips the native launcher because the entrypoint calls <code>dotnet</code> directly.

Example · dockerfile
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY nuget.config Directory.Build.props Directory.Packages.props global.json ./
COPY src/Orders.Api/Orders.Api.csproj src/Orders.Api/packages.lock.json src/Orders.Api/
COPY src/Orders.Data/Orders.Data.csproj src/Orders.Data/packages.lock.json src/Orders.Data/
COPY src/Orders.Domain/Orders.Domain.csproj src/Orders.Domain/packages.lock.json src/Orders.Domain/
RUN --mount=type=secret,id=contoso_feed_pat \
    NuGetPackageSourceCredentials_contoso="Username=build;Password=$(cat /run/secrets/contoso_feed_pat)" \
    dotnet restore src/Orders.Api/Orders.Api.csproj --locked-mode
COPY src/ src/
RUN dotnet publish src/Orders.Api/Orders.Api.csproj --no-restore -o /app -p:UseAppHost=false

FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
USER $APP_UID
ENTRYPOINT ["dotnet", "Orders.Api.dll"]

Self-contained single file for a VM

Self-contained output carries its own runtime, so a .NET security patch reaches the VM only through your next build. Put a monthly rebuild in the pipeline or prefer framework-dependent deployment where the host can install runtime patches.

Example · bash
# A worker deployed to a plain Linux VM with no .NET installed.
dotnet publish src/Orders.Worker \
  -r linux-x64 --self-contained \
  -p:PublishSingleFile=true \
  -p:PublishReadyToRun=true \
  -o artifacts/orders-worker

ls -lh artifacts/orders-worker
# Orders.Worker  appsettings.json  appsettings.Production.json  Orders.Worker.pdb

# Runtime security patches now ship only when you republish and redeploy.

Discussion

  • Be the first to comment on this lesson.