Runtime Identifiers and Self-Contained Builds
A runtime identifier names the platform you publish for, and decides whether the .NET runtime travels with your app.
dotnet publish -c Release -r linux-x64 --self-contained falseA runtime identifier, or RID, is a short string naming a platform: linux-x64, win-x64, osx-arm64, or linux-musl-x64 for Alpine and other musl distributions. The pattern is operating system, dash, processor architecture. You pass one as -r linux-x64, whose long form is --runtime linux-x64, or set <RuntimeIdentifier>linux-x64</RuntimeIdentifier> in the project file. Those are the same setting.
Two ways to ship
Framework-dependent is the default. The folder holds your code and its libraries, and the machine running it must already have a matching .NET runtime installed. Self-contained copies the runtime into the folder as well, so the machine needs no .NET at all. Ask for it with --self-contained true, short form --sc.
A RID alone is not self-contained
Up to .NET 7, naming a RID quietly made the app self-contained. For apps targeting net8.0 or later it does not; a RID on its own still produces framework-dependent output. .NET 6 added warning NETSDK1179 to push you into saying which you meant. Pass --self-contained or --no-self-contained every time you pass -r.
Size against patching
A framework-dependent app is a few hundred kilobytes and rolls forward to the newest patch installed on the machine. A self-contained app is tens of megabytes and is frozen on the runtime you built with. Since .NET 8 use portable RIDs only, such as linux-x64, not ubuntu.22.04-x64.
Example
using System.Runtime.InteropServices;
// RuntimeIdentifier is the RID the running .NET build was made for.
// Added in .NET 5. Since .NET 8 it reports the portable RID.
Console.WriteLine($"rid: {RuntimeInformation.RuntimeIdentifier}");
Console.WriteLine($"architecture: {RuntimeInformation.ProcessArchitecture}");
Console.WriteLine($"linux: {RuntimeInformation.IsOSPlatform(OSPlatform.Linux)}");
// Ask which platform you are on. Never split the RID string apart
// to find out; the shape of a RID is not a public contract.
string archive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? "shipping-win.zip"
: "shipping-unix.tar.gz";
Console.WriteLine($"archive: {archive}");
// Output (run on 64-bit Linux on the official linux-x64 runtime):
// rid: linux-x64
// architecture: X64
// linux: True
// archive: shipping-unix.tar.gzWhen to use it
- Publish a build for a Linux server from a Windows or macOS laptop.
- Ship a tool to machines where you cannot ask anyone to install .NET.
- Explain why a container built on an Apple laptop will not start on an x64 node.
More examples
Declaring the targets in the project file
RuntimeIdentifiers (plural) lists what restore must prepare packages for; -r on the command line picks one of them for a given publish. Putting SelfContained in the project file means a teammate cannot produce a different kind of build by forgetting a flag, which is the usual cause of "it works on my machine".
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<!-- Every platform this app is published for. Plural property,
semicolon separated. -->
<RuntimeIdentifiers>linux-x64;linux-arm64;win-x64</RuntimeIdentifiers>
<!-- Say it once here instead of on every command line. -->
<SelfContained>false</SelfContained>
</PropertyGroup>
</Project>The same app, three deployments
Note the output path: once a RID is involved it gains a RID folder, which trips up build scripts that hardcoded bin/Release/net10.0/publish. The single-file form bundles everything into one executable; it starts slightly slower because files are extracted at run time, and it is still OS and architecture specific.
# Smallest. Needs .NET 10 already installed on the target.
dotnet publish -c Release -r linux-x64 --no-self-contained
# -> bin/Release/net10.0/linux-x64/publish/
# Carries the runtime. Needs nothing installed.
dotnet publish -c Release -r linux-x64 --self-contained true
# One file, runtime included. Short form of --self-contained is --sc.
dotnet publish -c Release -r linux-x64 --sc \
-p:PublishSingleFile=trueChoosing a RID for a container base image
Mismatching these is a common container failure, and the symptom is unhelpful: the app exits immediately with a missing shared library message. Pick the RID from the base image's libc and architecture, not from the machine you are building on.
# Alpine and other musl-based images need the musl RID.
dotnet publish -c Release -r linux-musl-x64 --no-self-contained
# Debian and Ubuntu based images use the glibc RID.
dotnet publish -c Release -r linux-x64 --no-self-contained
# Apple silicon and AWS Graviton are arm64, not x64.
dotnet publish -c Release -r linux-arm64 --no-self-contained
Discussion