Runtime identifiers: portable RIDs like linux-x64 and win-arm64, when a RID is needed (self-contained, single-file, AOT, native assets), RuntimeIdentifier versus RuntimeIdentifiers, the .NET 8 move to portable RIDs, and RID-specific NuGet assets.
A runtime identifier, or RID, names the platform you build for, as OS plus architecture: win-x64, linux-x64, linux-musl-x64 for Alpine, linux-arm64 or osx-arm64. A normal framework-dependent app doesn't need one. It stays portable, and packages with native code ship runtimes/<rid>/ folders that the runtime picks from at start-up. You set a RID when the output must be platform-specific: self-contained, single-file, ReadyToRun or Native AOT. Since .NET 8 the SDK only understands portable RIDs, so distro-specific ones like ubuntu.22.04-x64 no longer work. Also since .NET 8, -r alone doesn't make the app self-contained.
"A runtime identifier names the platform you build for as OS plus architecture:
win-x64,linux-x64,linux-arm64,osx-arm64, andlinux-musl-x64for Alpine. It's different from an OS-specific TFM:net10.0-windowschooses which APIs you compile against, whilewin-x64chooses where the output runs.A normal framework-dependent app doesn't need one. It stays portable, and packages with native code ship
runtimes/<rid>/folders that the host picks from at start-up. I set a RID when the output must be platform-specific: self-contained, single-file, ReadyToRun or Native AOT.RuntimeIdentifierfixes one RID for every build and publish, whileRuntimeIdentifiersonly tells restore to prepare assets for several, so I can restore once and publish each with-rand--no-restore.Two .NET 8 changes still apply. The SDK uses a portable-only RID graph, so distro-specific RIDs like
ubuntu.22.04-x64fail with NETSDK1083;UseRidGraphbrings the old graph back, but only as a temporary escape hatch. And-rno longer implies self-contained, so I pass--self-containedexplicitly. The mistake I see is publishinglinux-x64for an Alpine image: musl isn't glibc, so a native library fails to load at run time. So I keep RIDs out of the default build, match the RID to the base image, and add a start-up smoke test in CI."
<os>[-<libc>]-<arch>:
| RID | Platform |
|---|---|
win-x64, win-arm64 |
Windows |
linux-x64, linux-arm64 |
glibc-based Linux (Debian, Ubuntu, RHEL, Azure Linux) |
linux-musl-x64, linux-musl-arm64 |
musl-based Linux, mainly Alpine |
osx-x64, osx-arm64 |
macOS Intel and Apple silicon |
| Scenario | RID needed? |
|---|---|
Framework-dependent, portable (dotnet publish) |
No |
| Framework-dependent but platform-specific (smaller output, native apphost for one OS) | Optional: -r linux-x64 |
| Self-contained | Yes |
| Single-file, ReadyToRun, Native AOT | Yes |
| SDK container publish | Uses one (it defaults to the container's platform) |
Packages such as Microsoft.Data.SqlClient or SkiaSharp ship native libraries per platform:
runtimes/
win-x64/native/libfoo.dll
linux-x64/native/libfoo.so
linux-musl-x64/native/libfoo.so
osx-arm64/native/libfoo.dylib
deps.json.<RuntimeIdentifier>linux-x64</RuntimeIdentifier> fixes the RID for every build and publish of the project. Output goes to bin/<Config>/<TFM>/linux-x64/.<RuntimeIdentifiers>linux-x64;win-x64</RuntimeIdentifiers> tells restore to prepare assets for those RIDs, so dotnet publish -r win-x64 --no-restore works later, without changing the default build.ubuntu.22.04-x64 or rhel.8-x64 now produce errors or warnings; use linux-x64.-r doesn't imply self-contained: pass --self-contained explicitly.Both still apply in .NET 10.
A worker published with -r linux-x64 ran fine on Ubuntu VMs but crashed on start-up in a new Alpine-based image, with a native library load error from a compression package. Alpine uses musl, so the image needed linux-musl-x64. Publishing with the right RID (or moving to a glibc base image) fixed it, and a smoke test in the pipeline now starts the container before it's pushed.
Checks platform awareness for deployment.
Green flags: knows the RID format and musl versus glibc; explains when a RID is needed; knows the .NET 8 portable-RID and self-contained changes; understands runtimes/ native assets.
Red flags: sets a RID on every project by habit; uses distro-specific RIDs; can't explain why an app fails on Alpine.
linux - musl - arm64
| | |
| | +-- architecture: x64, x86, arm64, arm
| +---------- C library (Linux only): omitted = glibc, musl = Alpine and similar
+----------------- operating system: win, linux, osx, (ios, android, browser ...)
| RID | Runs on |
|---|---|
win-x64, win-x86, win-arm64 |
Windows |
linux-x64, linux-arm64, linux-arm |
glibc-based Linux: Debian, Ubuntu, RHEL, Azure Linux, Amazon Linux |
linux-musl-x64, linux-musl-arm64 |
musl-based Linux, mainly Alpine |
osx-x64, osx-arm64 |
macOS on Intel and Apple silicon |
These are portable RIDs. Since .NET 8 they are the only ones the SDK and runtime understand by default.
PackageReference (with runtimes/* assets)
|
restore -- RuntimeIdentifiers=linux-x64;win-x64
| --> RID-specific assets recorded in obj/project.assets.json
| --> runtime packs downloaded if self-contained
|
build -- RuntimeIdentifier=linux-x64
| --> bin/Release/net10.0/linux-x64/
| --> apphost for Linux, linux-x64 native files only
|
publish -- --self-contained --> + the .NET runtime for linux-x64
-- PublishSingleFile --> one executable for linux-x64
-- PublishAot --> native executable for linux-x64
portable (no RID):
bin/Release/net10.0/
App.dll, App.deps.json, App.runtimeconfig.json
App / App.exe <- apphost for the build machine's OS
runtimes/win-x64/native/...
runtimes/linux-x64/native/...
runtimes/osx-arm64/native/...
the host reads App.deps.json at start-up and loads the matching folder
| Aspect | No RID (portable) | With a RID |
|---|---|---|
| Output folder | bin/<Config>/<TFM>/ |
bin/<Config>/<TFM>/<RID>/ (unless AppendRuntimeIdentifierToOutputPath=false) |
| Native assets | All platforms copied | Only the matching platform |
| Apphost | For the OS you build on | For the target RID |
| Self-contained possible | No | Yes, with --self-contained |
| Runs on | Any OS with the .NET runtime | That OS and architecture only |
| Scenario | RID? | Why |
|---|---|---|
| Framework-dependent web API in a container or App Service | No (optional) | The runtime is already there, and portable output works everywhere |
| Smaller framework-dependent output for one OS | Optional | Drops other platforms' native files |
| Self-contained | Yes | The SDK must know which runtime to bundle |
| Single-file | Yes | The bundle includes a platform-specific host |
| ReadyToRun | Yes | Precompiled code is per architecture |
| Native AOT | Yes | The output is a native binary |
SDK container publish (-t:PublishContainer) |
Chosen for you | Defaults to the base image's platform; ContainerRuntimeIdentifier overrides it |
| A RID-specific .NET tool package (new in .NET 10) | Yes | ToolPackageRuntimeIdentifiers lists the platforms to pack |
Instead of -r, you can pass --os linux --arch arm64 and the SDK builds the RID for you. --arch alone uses the current OS.
RuntimeIdentifier (singular) |
RuntimeIdentifiers (plural) |
|
|---|---|---|
| Affects | Restore, build and publish | Restore only |
| Default build | Becomes RID-specific | Stays portable |
| Typical use | A project that only ever ships for one platform | Restoring once in CI, then publishing for several platforms with -r ... --no-restore |
| Command-line equivalent | -r linux-x64 |
(none; set in the project or Directory.Build.props) |
Publishing with --no-restore for a RID that wasn't restored fails with NETSDK1047, which asks you to add the RID to RuntimeIdentifiers.
Packages with native code (SQLite, SkiaSharp, gRPC, some compression and crypto libraries) ship one folder per platform:
runtimes/
win-x64/native/e_sqlite3.dll
linux-x64/native/libe_sqlite3.so
linux-musl-x64/native/libe_sqlite3.so
linux-arm64/native/libe_sqlite3.so
osx-arm64/native/libe_sqlite3.dylib
linux-musl-x64 falls back to linux-x64, then linux, then unix, then any).DllNotFoundException or a "cannot open shared object file" error.ubuntu.22.04-x64, rhel.8-x64, alpine.3.18-x64). The SDK now reports NETSDK1083 for them. <UseRidGraph>true</UseRidGraph> restores the old graph as a temporary workaround, mainly for old packages that only ship distro-specific assets. Move to packages with portable assets and remove the workaround.-r no longer implies self-contained. Before .NET 8, dotnet publish -r linux-x64 produced a self-contained app; now it's framework-dependent unless you add --self-contained.RuntimeInformation.RuntimeIdentifier returns the portable RID (linux-x64), not the distro RID, which is the right value to log.| Output | Build on a different OS? | Build for a different architecture? |
|---|---|---|
| Framework-dependent | Yes | Yes |
| Self-contained, single-file | Yes | Yes |
| ReadyToRun | Yes, with some limits | Yes |
| Native AOT | No: build on the target OS | Possible with extra setup (for example a Linux arm64 cross toolchain and sysroot, or Visual Studio's arm64 build tools on Windows) |
For AOT and multi-platform releases, CI usually runs a matrix of agents (Windows, Linux x64, Linux arm64, macOS).
| Base image | RID |
|---|---|
mcr.microsoft.com/dotnet/aspnet:10.0 (Debian or Ubuntu based, depending on tag) |
linux-x64 or linux-arm64 |
mcr.microsoft.com/dotnet/aspnet:10.0-alpine |
linux-musl-x64 or linux-musl-arm64 |
Chiseled images (10.0-noble-chiseled) |
linux-x64 or linux-arm64 |
runtime-deps images (for self-contained or AOT) |
Match the image's libc and architecture |
When you build the image with a Dockerfile on an Apple silicon Mac or an arm64 agent, the default platform is arm64. Use --platform linux/amd64 (or build for both) if production runs on x64.
| Symptom | Likely cause | Fix |
|---|---|---|
| NETSDK1083 "The specified RuntimeIdentifier is not recognized" | A distro-specific RID | Use a portable RID such as linux-x64 |
| NETSDK1047 "Assets file doesn't have a target for ..." | Publishing with --no-restore for a RID that wasn't restored |
Add it to RuntimeIdentifiers, or restore with -r |
DllNotFoundException or "cannot open shared object file" |
The native asset for this platform is missing, often musl versus glibc | Publish with the right RID, or change the base image |
| "exec format error" in a container | Wrong architecture (arm64 binary on x64 or the reverse) | Match -r and the image platform |
| App is larger than expected | Portable output with every platform's native files | Publish with the target RID |
| Local build output moved to a RID folder | RuntimeIdentifier set in the project |
Move it to publish time, or use RuntimeIdentifiers |
Log RuntimeInformation.RuntimeIdentifier, RuntimeInformation.OSArchitecture and RuntimeInformation.OSDescription at start-up so every native load failure comes with the platform in the log.
linux-arm64 build plus a performance test can be an easy saving.The portable RID model is stable. Watch the .NET 11 release notes for changes to supported OS versions and architectures, and for any new RIDs, before relying on them. Keep production builds on .NET 10 until you've tested the upgrade.
dotnet publish -o out/portable, and list out/portable (no RID folder).Microsoft.Data.Sqlite, publish again and look inside out/portable/runtimes/.dotnet publish -r linux-x64 -o out/linux and compare: only the Linux native file remains.dotnet publish -r ubuntu.22.04-x64 and read NETSDK1083.dotnet publish -r linux-musl-x64 --self-contained -o out/alpine and compare its size with step 3.<RuntimeIdentifiers>linux-x64;win-x64</RuntimeIdentifiers>, restore once, then publish both with --no-restore.RuntimeInformation.RuntimeIdentifier in the app and run it on your machine and in an Alpine container.<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<!--
RuntimeIdentifiers (plural) only tells restore to prepare assets for these platforms.
Normal builds stay portable; we pick a platform at publish time with -r.
Using RuntimeIdentifier (singular) here would make every local build linux-specific.
-->
<RuntimeIdentifiers>linux-x64;linux-musl-x64</RuntimeIdentifiers>
</PropertyGroup>
</Project>
# Restore once for both platforms
dotnet restore
# Debian/Ubuntu-based image (glibc)
dotnet publish -c Release -r linux-x64 --self-contained --no-restore -o out/linux-x64
# Alpine-based image (musl): a different RID because native libraries differ
dotnet publish -c Release -r linux-musl-x64 --self-contained --no-restore -o out/linux-musl-x64
// Log the platform at start-up: invaluable when a native load fails
using System.Runtime.InteropServices;
Console.WriteLine($"RID {RuntimeInformation.RuntimeIdentifier}, OS {RuntimeInformation.OSDescription}");
Mistake: publishing with linux-x64 for an Alpine image.
Alpine uses musl, not glibc, so native libraries built for linux-x64 fail to load, often only when a code path first calls them.
Instead: use linux-musl-x64 (or linux-musl-arm64) for Alpine, or switch to a Debian, Ubuntu or chiseled base image and keep linux-x64.
Mistake: using distro-specific RIDs such as ubuntu.22.04-x64.
Since .NET 8 the SDK only understands portable RIDs, so these fail with NETSDK1083. Copying them from old documentation or old pipelines breaks the build.
Instead: use portable RIDs (linux-x64). Use UseRidGraph=true only as a short-term workaround for an old package, and plan to remove it.
Mistake: setting RuntimeIdentifier in the project when you only need it for one publish.
Every local build becomes platform-specific, the output folder moves to bin/<Config>/<TFM>/<RID>/, and scripts that expect the old path break.
Instead: keep builds portable and pass -r at publish time. If you need restore to be ready for several platforms, use RuntimeIdentifiers.
Mistake: assuming -r makes the app self-contained.
Since .NET 8, dotnet publish -r linux-x64 produces a framework-dependent app. It then fails to start on a machine or image without the .NET runtime.
Instead: pass --self-contained explicitly when you want the runtime bundled, or use a base image that already has it.
Mistake: publishing with --no-restore for a RID that wasn't restored.
The assets file has no entry for that RID, so publish fails with NETSDK1047.
Instead: list every publish RID in RuntimeIdentifiers before the restore step, or restore with the same -r value.
Mistake: building Native AOT for Linux on a Windows agent. Native AOT doesn't cross-compile between operating systems, so the build fails or needs a Linux toolchain that isn't there. Instead: build AOT on the target OS, using a CI matrix with Linux and Windows agents, or build inside a Linux container.
Mistake: ignoring the CPU architecture of the build machine and the host.
An image built on an Apple silicon Mac or arm64 agent is arm64 by default, and fails on x64 hosts with "exec format error". The reverse happens when moving to cheaper arm64 instances.
Instead: set the RID and the image platform explicitly (-r linux-x64, --platform linux/amd64), or build multi-architecture images.
Mistake: confusing OS-specific TFMs with RIDs.
net10.0-windows chooses which APIs you compile against; win-x64 chooses which platform the output runs on. Setting one doesn't set the other.
Instead: use an OS-specific TFM when you need platform APIs such as WPF, and a RID when you need platform-specific output.
Mistake: forgetting that self-contained and AOT apps don't get runtime patches automatically. The runtime is baked into the output, so updating the host or base image doesn't fix a runtime vulnerability. Instead: rebuild and redeploy self-contained and AOT apps on every .NET security release, and track them in your patching process.
Mistake: not logging the platform at start-up.
When a native library fails to load, there's no record of which RID, OS and architecture the app was actually running on.
Instead: log RuntimeInformation.RuntimeIdentifier, OSArchitecture and OSDescription at start-up, and add a container smoke test in CI that starts the app before the image is pushed.
Most services should stay portable and framework-dependent, so the RID question only arises for specific deployment shapes. When it does, a senior engineer keeps RIDs out of the project's default build (using RuntimeIdentifiers for restore and -r at publish), matches the RID exactly to the base image (glibc versus musl, x64 versus arm64), and adds a start-up smoke test so native-asset mistakes are caught in CI rather than in production. With Arm64 instances often cheaper per unit of compute in Azure, being deliberate about linux-arm64 builds can also save real money.
By default dotnet publish gives a framework-dependent folder that needs the .NET runtime on the server. Self-contained (--self-contained -r linux-x64) bundles the runtime, so nothing needs installing, but the output is bigger and you patch the runtime by redeploying. On top of that: single-file packs everything into one executable, trimming removes unused code, ReadyToRun precompiles to speed up start-up, and Native AOT compiles to a native binary with very fast start-up and low memory but no dynamic code generation. The SDK can also build a container image directly. I choose based on where it runs, start-up and memory needs, and whether the libraries are trim- and AOT-safe, and I measure the result.
A target framework moniker, or TFM, such as net10.0, tells the SDK which API surface to compile against and which runtime the app needs. OS-specific TFMs like net10.0-windows add platform APIs such as WPF. For applications I target one framework, normally the newest LTS: that's net10.0 today, supported until November 2028. .NET 8 and .NET 9 both reach end of support on 10 November 2026, so they need upgrading now. Libraries can multi-target with <TargetFrameworks>net8.0;net10.0</TargetFrameworks> while they still have consumers on older versions. That builds the library once per framework, and #if NET10_0_OR_GREATER handles API differences. .NET 11 is a preview, so it's for experiments only.
Last updated 1 Nov 2026.