# .NET deployment models and optimizations To be honest, this article is more like a personal note & experiments on .NET deployment models, the knowledge will mainly taken from the [Microsoft's official docs](https://learn.microsoft.com/en-us/dotnet/core/deploying/). I’ll try to keep the article clear and useful. The sample application source code can be referenced here https://github.com/alex289/CleanArchitecture/tree/main # Deployment models .NET applications can be deployed in two main ways: **Framework-dependent** and **Self-contained**. The first one, as its name already tells, requires local .NET runtime that's compatible with the application, the other packs everything up and distributes as a single runnable binary. Let’s break them down. **TL;DR** | Feature | Framework-dependent | Self-contained | |----------------------------------|-----------------------------------------------|-------------------------------------------------| | **Size** | ✅ Smaller output size | ❌ Larger output size | | **Runtime requirement** | ❌ Requires .NET runtime installed | ✅ Bundles the .NET runtime | | **Portability** | ❌ Less portable (depends on target system) | ✅ Portable across systems | | **Update behavior** | ✅ Automatically benefits from runtime updates | ❌ You manage runtime updates manually | | **Startup performance** | ⚠️ May be slower if runtime is cold-loaded | ✅ Can optimize with AOT/ReadyToRun | | **App isolation** | ❌ Possible runtime version conflicts | ✅ Fully isolated from system environment | | **Publish size consistency** | ✅ Consistent across target machines | ❌ Size varies by platform/runtime | | **Best for** | Internal apps, controlled environments | External apps, cross-platform distribution | ## Framework-dependent deployment ### What it is This one assumes that the target machine already has the right .NET runtime installed. So the app just ships the DLLs and relies on the existing .NET runtime to actually run. ### How it works The app's compiled assemblies (DLLs), a startup executable, and a small amount of glue code are included. But no runtime – it expects .NET to be there. ### Outputs Depends on the publish command, it will contain the app assemblies (DLL) and the app executable (for specific operating system). No .NET runtime included, which makes the outputs minimal. ``` # produces DLLs and executable dotnet publish -c Release -o publish CleanArchitecture.Api/CleanArchitecture.Api.csproj ``` ![image](https://hackmd.io/_uploads/rkm55Rgbge.png) ![image](https://hackmd.io/_uploads/S1nM0Rebex.png) ``` # produces DLL only dotnet publish -c Release -p:UseAppHost=false -o publish CleanArchitecture.Api/CleanArchitecture.Api.csproj ``` ### Single-file deployment A common mistake for developers that they think including the flag to enable single file publish will have the same effect as self-contained app. In fact, it's not. [Single file deployment](https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview?tabs=cli) can be enabled with both framework-dependent and self-contained deployment. It will bundle all application-dependent files into a single binary for simple distribution. ``` dotnet publish -c Release -p:PublishSingleFile=true -p:SelfContained=false -o publish CleanArchitecture.Api/CleanArchitecture.Api.csproj ``` ![image](https://hackmd.io/_uploads/HJQjRAebee.png) *Please note that an executable is needed for publish single file (`UseAppHost=true`)* ## Self-contained deployment ### What it is This one brings the whole party with it. The application + the .NET runtime + everything else it needs to run. (It still requires native dependencies of .NET, for example [.NET on Linux prerequisites](https://github.com/dotnet/core/blob/main/release-notes/8.0/linux-packages.md)). ### How it works When you publish, the .NET SDK takes the local runtime and bundles it with your app. This means the app can run anywhere, regardless of whether .NET is installed or not. ### Outputs All binaries + the runtime + the app’s dependencies. Think of it like a full portable app package. ``` # the RID is required for specific platform dotnet publish -c Release -r osx-arm64 -p:SelfContained=true -o publish CleanArchitecture.Api/CleanArchitecture.Api.csproj ``` ![image](https://hackmd.io/_uploads/B1eteyWZex.png) You can see that the total size of DLL files is almost **4 times** the framework-dependent case because we already included a .NET runtime in these DLLs. ### Runtime roll forward A self-contained application deployment publishes the highest patch runtime on your machine. This enables your deployed application to run with security fixes (and other fixes) available during publish. ### Single-file deployment Everything in one file, which makes it ultra portable and mega big. ``` dotnet publish -c Release -r osx-arm64 -p:SelfContained=true -p:PublishSingleFile=true -o publish -p:UseAppHost=true CleanArchitecture.Api/CleanArchitecture.Api.csproj ``` ![image](https://hackmd.io/_uploads/SJxAlybWxg.png) # Optimizations You’ve got the deployment – now let’s talk about optimizing the most out of your build. Smaller size, faster start-up, and fewer dependencies. ## Trim self-contained deployments ### Overview Depending on the complexity of the application, only a subset of the framework assemblies are referenced, and a subset of the code within each assembly is required to run the application. The unused parts of the libraries are unnecessary and can be trimmed from the packaged application. ### Experiment Try this: ``` dotnet publish -c Release -r osx-arm64 -p:SelfContained=true -p:PublishTrimmed=true -o publish CleanArchitecture.Api/CleanArchitecture.Api.csproj ``` ![image](https://hackmd.io/_uploads/SkXWzyWbxl.png) The DLL size is only **40%** of the original non-trimmed version, which is incredible! But it would come with a cost. ### Trim warnings It can be difficult to determine what is unused, or more precisely, what is used. The trimmer produces trim warnings when it finds code that might not be compatible with trimming. If there are any trim warnings, the app should be carefully tested after trimming to ensure that there are no behavior changes. ![image](https://hackmd.io/_uploads/ryaq7ybZgg.png) *example trimming warnings* The detailed explanation and mitigate method can be found in the [official doc](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/fixing-warnings). ### Trimming options Trimming options can also be referenced [here](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options). Some useful options: - `PublishTrimmed`: enables trimming. - `ILLinkTreatWarningsAsErrors`: treat warnings as errors. - `TrimmerSingleWarn`: show detailed warnings. ## AOT deployment ### Overview AOT = Ahead-of-Time compilation. The app gets compiled all the way down to native machine code, skipping the JIT at runtime. These apps can run on machines that don't have the .NET runtime installed. Use AOT to get lightning-fast startup and smaller memory footprints. It’s great for microservices, CLI tools, or anything where speed is mandatory. ### Experiment AOT will also enable trimming, publish single file and self-contained by default. ``` dotnet publish -c Release -r osx-arm64 -p:PublishAot=true -o publish CleanArchitecture.Api/CleanArchitecture.Api.csproj ``` ![image](https://hackmd.io/_uploads/S10vOk--ge.png) *200s publish time* ![image](https://hackmd.io/_uploads/Hkb3IJW-el.png) The result binary is **~100M**, which is smaller than self-contained + publish single file, but larger than trimming due to direct native code generated. ### Pros/cons **Pros:** - Fast startup - No JIT overhead - Smaller memory usage **Cons:** - Longer publish time - Bigger binaries - Some features (like dynamic loading) need workarounds The detailed list of [limitations](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=macOS%2Cnet8#limitations-of-native-aot-deployment). ## ReadyToRun deployment ### Overview R2R compiles IL to native code ahead of time (like AOT), but still includes some IL in the package. Think of it like a hybrid. ### Use case Improves startup performance without going full AOT. Good middle-ground for web apps or services. Enable it like this: ``` dotnet publish -c Release -r osx-x64 -p:PublishReadyToRun=true -o publish CleanArchitecture.Api/CleanArchitecture.Api.csproj ``` ![image](https://hackmd.io/_uploads/r11Q_Jbbll.png) *60s publish time* ![image](https://hackmd.io/_uploads/SyJU_k-Zxl.png) *75MB of DLLs* # Extra: Container deployments ### Overview You’ve published your app – but what if you want to ship it in a container, the modern way to distribute apps across systems? Since .NET 8, the SDK has native support for container builds. That means: **no Dockerfile required**, just use the built-in MSBuild target: `PublishContainer`. This is useful for simplifying CI/CD pipelines and keeping the deployment stack minimal and clean. ### Getting started You can run this command to publish directly into a container image: ``` dotnet publish -c Release -p:PublishProfile=DefaultContainer -p:ContainerImageName=clean-api -p:ContainerImageTag=latest CleanArchitecture.Api/CleanArchitecture.Api.csproj ``` That’s it — the SDK will: - Build the self-contained app - Package it - Create a container image - Use the default base image (e.g., `mcr.microsoft.com/dotnet/aspnet`) ### Customizing the container You can customize a bunch of stuff using MSBuild properties: | Property | Description | Example | |-------------------------------|-----------------------------------------------------------|--------------------------------------------------| | `ContainerRepository` | Name of the image | `clean-api` | | `ContainerImageTag` | Tag/version of the image | `v1.0.0`, `latest` | | `ContainerBaseImage` | Base image to use | `mcr.microsoft.com/dotnet/aspnet:8.0` | | `ContainerRegistry` | Push destination registry (omit for only build) | `docker.io` | | `ContainerEnvironmentVariables`| Env vars for the container | `ASPNETCORE_ENVIRONMENT=Production` | Example: ``` dotnet publish -c Release -p:PublishProfile=DefaultContainer \ -p:ContainerRepository=clean-api \ -p:ContainerImageTag=latest \ -p:ContainerBaseImage=mcr.microsoft.com/dotnet/aspnet:8.0 \ -p:ContainerEnvironmentVariables=ASPNETCORE_ENVIRONMENT=Production \ CleanArchitecture.Api/CleanArchitecture.Api.csproj ``` ### Where's the image? Once published, the image will be available locally: ``` docker images ``` ![image](https://hackmd.io/_uploads/BJu2h1bWel.png) You should see your image (`clean-api:latest`) listed. You can now: - Run it with `docker run` - Tag and push it to a registry - Use it in your deployment pipeline # Conclusion .NET offers flexible deployment options for different needs — from lightweight framework-dependent builds to fully self-contained, optimized, and even native AOT deployments. With built-in support for trimming, ReadyToRun, and containerization, you can fine-tune your app’s performance, size, and portability. Choose the right strategy based on your environment, performance goals, and distribution model.