/flags/dotnet-cli/dotnet-publish-self-contained
dotnet publish -r linux-x64 --self-contained: include the .NET runtime in the app
- tool
- .NET CLI
- command
- dotnet publish
- flag
- -r, --runtime RID; --self-contained [true|false]; --sc; --no-self-contained
- default
- framework-dependent (the target machine needs the .NET runtime)
dotnet publish -r linux-x64 --self-contained produces a folder that contains your app and the .NET runtime for Linux x64, so it runs on a machine where .NET is not installed. -r (--runtime) picks the target platform with a runtime identifier (RID) such as win-x64, linux-x64, linux-arm64 or osx-arm64. Without --self-contained, the published app is framework-dependent: smaller, but it needs a matching shared runtime on the target.
Framework-dependent versus self-contained
dotnet new console -o app > /dev/null && cd app
dotnet publish -r linux-x64 -o fd -v q -nologo > /dev/null 2>&1
dotnet publish -r linux-x64 --self-contained -o sc -v q -nologo > /dev/null 2>&1
for d in fd sc; do
echo "--- $d: $(ls $d | wc -l) files"
ls $d | grep -E "^(app|app\.dll|libcoreclr\.so|System\.Private\.CoreLib\.dll)$"
doneOutput
--- fd: 5 files app app.dll --- sc: 192 files System.Private.CoreLib.dll app app.dll libcoreclr.so
Both folders contain the Linux launcher app and app.dll. Only the self-contained one has the runtime itself (libcoreclr.so, System.Private.CoreLib.dll and almost 200 files in total). The SDK downloaded the Linux runtime pack from NuGet, which is why the command works on any operating system.
Options
| Option | Effect |
|---|---|
| -r linux-x64 | Target platform (RID); creates a native launcher for it |
| --self-contained (--sc) | Copy the .NET runtime into the output |
| --no-self-contained | Framework-dependent; the target needs the shared runtime |
| -p:PublishSingleFile=true | Bundle everything into one executable |
| -p:PublishTrimmed=true | Remove unused framework code (self-contained only) |
| -p:PublishAot=true | Native AOT compilation (set in the project file) |
| -o DIR | Output folder (default bin/Release/<tfm>/<rid>/publish/) |
Choosing
- Framework-dependent for servers and containers where you control the runtime (for example the
mcr.microsoft.com/dotnet/aspnetimage): small deploys, security patches arrive with runtime updates. - Self-contained for desktop tools, customer machines and minimal containers (
runtime-depsimages) where no runtime is installed; you ship runtime patches yourself by republishing. - Single file on top of either, for one executable to copy: see dotnet publish -p:PublishSingleFile=true.
In the project file
<PropertyGroup>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
</PropertyGroup>Setting the properties once avoids long command lines; use RuntimeIdentifiers (plural) to restore for several targets.
Pitfalls
- Since the .NET 8 SDK
-ralone no longer implies self-contained; scripts written for .NET 6 that relied on that now produce framework-dependent output. - The output for one RID does not run on another: a
linux-x64folder fails on ARM servers. Publish once per target. - Self-contained apps do not receive runtime security fixes from the OS; republish after every .NET patch release.
- Targeting a framework newer than the installed SDK fails with NETSDK1045.
FAQ
Does -r imply --self-contained?
Not any more. Since the .NET 8 SDK, specifying a runtime identifier produces a framework-dependent app for that platform; add --self-contained explicitly when you want the runtime included.
How big is a self-contained .NET app?
A hello world is roughly 70 MB on disk because the whole runtime and base libraries are copied. Trimming (-p:PublishTrimmed=true) or Native AOT reduce it a lot, at the cost of compatibility checks.
Can I build a Linux self-contained app on Windows?
Yes. The SDK downloads the runtime pack for the target RID from NuGet and copies it, so dotnet publish -r linux-x64 --self-contained works on any OS. Native AOT is the exception: it must be compiled on the target OS.