This content originally appeared on DEV Community and was authored by Bill
Originally posted on https://medium.com/c-sharp-programming/all-the-net-core-opsy-things-37b2e21eabb4
This guide began as a conversation between me and someone exploring how to containerize .NET apps. The same questions kept coming up; from new developers to infrastructure and DevOps engineers and I kept pointing people to the docs. I decided to turn it into a practical walk through and post it here for anyone who finds it useful.
When you pull an image from mcr.microsoft.com/dotnet/*
, you’re getting more than a runtime; you’re pulling from a carefully layered set of container images, each designed to be lightweight, secure, and purpose-built.
Understanding these layers makes it easier to troubleshoot, secure, optimize performance, and pick the right image for your use case.
Image Families
.NET container images are organized into families. Each serves a different job: running, building, hosting web apps, or acting as a base for self-contained apps.
Each family builds on the one below it, adding only what’s needed. That layering impacts size and what’s included by default.
Container Image Size vs Image Family
Sizes are uncompressed and taken directly from docker image ls.
Understanding runtime-deps
The lowest layer: a minimal Linux image with just enough to run a native .NET binary no package managers.
Use when:
Your app is self-contained i.e: includes its own runtime.
You’re using Native AOT (compiled to native code).
Includes only:
- System libraries (e.g., libc, libssl)
- CA certificates for HTTPS
dotnet publish -c Release -r linux-x64 --self-contained true -o ./out
The .NET runtime Layer
This layer includes the .NET runtime, allowing framework-dependent apps to run:
Suitable for non-web apps like background workers, CLI tools, and gRPC services.
Does not include web-specific libraries or compilers.
The aspnet Layer
Tailored for hosting ASP.NET Core applications:
- Comes pre-installed with Kestrel, MVC, and SignalR.
- Ideal for web APIs and web applications in production.
The sdk Layer
Use this only for building and testing your .NET apps and don’t ship it to Production:
- Contains compilers, build tools (MSBuild), NuGet package management, and git.
- Not intended for deployment, use multi-stage Dockerfiles to keep production lean.
Example use in a multi-stage Dockerfile:
Using the SDK as the build stage to end up with Smaller, secure, production-ready container images.
Tag anatomy
A .NET container image tag packs five key decisions into a single line. It specifies the .NET version, base OS, distro variant, runtime type, and CPU architecture.
Understanding the anatomy helps you make deliberate trade-offs for size, security, and compatibility, rather than relying on defaults.
Image Variants
Variants customize the base image to suit different needs, adding or removing features like shells, package managers, globalization support, or startup optimizations. They further affect the size, the attack surface, performance, or compatibility.
Variant vs Size
The Composite variant (suffix -composite)
Composite images merge all .NET shared‑framework assemblies into a single pre‑compiled binary blob that the CLR memory‑maps at start‑up. By skipping per‑assembly probing and much of the JIT warm‑up, they deliver noticeably faster cold‑starts, an advantage for serverless or short‑lived tasks.
The trade‑offs are tighter version lock‑in and a bulkier base layer: you can’t swap individual framework DLLs, so any upgrade requires a full image rebuild, and the composite blob may be larger than a trimmed set of separate DLLs. They shine in latency‑sensitive environments but aren’t ideal for plug‑in or extensibility scenarios that rely on replacing framework libraries. To build against a composite runtime, publish with PublishReadyToRun=true
and tag your runtime image with ‑composite
.
Distroless images
Distroless images are stripped-down containers designed for minimal attack surface and minimal size. They’re ideal when you want to run .NET apps and you do not have the need to debug or customize them interactively.
These images remove everything unnecessary to execute an app: no shell, no package manager, no root access, no globalization. You’ll be running as the app
user by default. To regain full globalization support, append -extra
to your tag.
Native AOT images
Native AOT (Ahead-of-Time) images eliminate the need for the CoreCLR entirely. Instead of relying on the traditional .NET runtime and JIT compilation, your app is pre-compiled into a single native binary at build time.
These images are designed for self-contained apps that use Native AOT compilation, ideal for scenarios where startup speed, low memory usage, and small image size matter.
dotnet publish -c Release -r linux-x64 /p:PublishAot=true
Use sdk:‑aot for building, runtime-deps:‑aot for running.
Benefits:
- Faster startup : No JIT means cold starts are significantly quicker.
- Lower memory footprint : Only the app code and linked native dependencies are loaded.
- Smaller container size : Final images are typically under 30 MB.
- No .NET runtime needed : Runs on any compatible OS without installing .NET.
Native AOT images are used with the runtime-deps
family. You build the binary using an sdk:*‑aot
image, then copy it into a matching runtime-deps:*‑aot
image for production.
Security Matters
Every additional package in a container is another potential vulnerability and adds to the attack surface of your workloads. Larger images often include shells, compilers, or debugging tools that make development easier, but also expand the attack surface in production. The GIF below illustrates that difference by scanning two official images mcr.microsoft.com/dotnet/aspnet:8.0
and mcr.microsoft.com/dotnet/aspnet:8.0‑alpine
which has a much leaner Alpine base.
Watch how the package count and vulnerability tally drop when we move from the “fat” Debian image to the slimmer Alpine base.
Conclusion
There’s no single “best” .NET container image, only the best fit for your scenario. Each variant, whether full, chiseled, distroless, or AOT; trades convenience for control, size for compatibility, and debuggability for security. The defaults will work, but they are not always optimal. Understanding the official image layers lets you make deliberate, informed choices that match how your app runs and where it runs. Choose with intent, not habit.
Glossary
Here are terms you might have encountered in this article and I did not give a description for.
Term | Meaning |
---|---|
glibc | The GNU C library. Standard on many distros. |
Distroless | Image with no shell or package manager. Minimal attack surface. |
libssl | OpenSSL library used for HTTPS communication. |
CA certificates | Root certs used to validate HTTPS/TLS connections. |
AOT | Ahead‑of‑Time compilation. Produces faster, native binaries. |
JIT | Just‑In‑Time compilation. Traditional .NET runtime optimization. |
Self‑contained app | Includes its own .NET runtime (SelfContained=true ). |
Kestrel | Lightweight web server built into ASP.NET Core. |
MVC | Model‑View‑Controller pattern used in ASP.NET Core. |
SignalR | Real‑time communication framework for ASP.NET Core. |
ICU | International Components for Unicode (globalization lib). |
shared‑framework assemblies | The set of core DLLs (e.g., System.* , Microsoft.AspNetCore.* ) that ship with .NET. |
CLR memory‑maps, per‑assembly probing and JIT warm‑up | The normal start‑up work where the CLR locates each DLL on disk and performs initial Just‑In‑Time compilation. |
framework DLLs | The individual .NET libraries that make up the shared framework. |
CLR | Common Language Runtime, the execution engine for .NET. |
DLL | Dynamic‑Link Library — a compiled binary containing reusable code and resources. |
Thanks for taking the time to read my article.
This content originally appeared on DEV Community and was authored by Bill