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.
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
| Parameter | Type | Description |
|---|---|---|
$-c, --configurationoptional | string= Release (since .NET 8) | Build configuration. |
$-r, --runtimeoptional | RID= none: portable, framework-dependent | Target runtime identifier, e.g. linux-x64, linux-arm64, linux-musl-x64. Required for self-contained, single-file, ReadyToRun and AOT. |
$--os / --archoptional | string= the host OS and architecture when only one is given | Build the RID from parts, e.g. --os linux --arch x64. |
$--self-containedoptional | bool= false | Include the .NET runtime in the output so the target needs nothing installed. |
$-o, --outputoptional | path= bin/Release/net10.0/[rid/]publish/ | Publish folder. |
$-p:PublishSingleFileoptional | bool= false | Bundle the app into one executable file. |
$-p:PublishReadyToRunoptional | bool= false | Precompile IL to native code to cut JIT time at start-up. |
$-t:PublishContaineroptional | target | Build 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
| Shape | Command | Target needs | Trade-off |
|---|---|---|---|
| Framework-dependent, portable | dotnet publish | The ASP.NET Core 10 runtime | Smallest output; the default for aspnet:10.0 images |
| Framework-dependent, RID-specific | -r linux-x64 | The runtime | Adds a native apphost and only that RID's native assets |
| Self-contained | -r linux-x64 --self-contained | Nothing (runtime-deps) | Bigger; you ship runtime patches yourself |
| Single file | -p:PublishSingleFile=true | Depends on self-contained | Assembly.Location is empty |
| ReadyToRun | -p:PublishReadyToRun=true | Same as above | Faster start-up, larger files, still JITs hot paths |
| Native AOT | <PublishAot>true</PublishAot> in the project | Nothing | Fast 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.jsonis 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-buildwith a different-cor RID than the build publishes nothing useful or fails; build and publish with identical arguments.
Example
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.
<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.
# 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.
# 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