Choosing the right .NET image for your workloads



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.

Image Families

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.

Image family vs Size

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

Docker image for runtimedeps

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

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.

Tag anatomy

Understanding the anatomy helps you make deliberate trade-offs for size, security, and compatibility, rather than relying on defaults.

What each part of the tag means

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.

Ubuntu Image Variants and what's in them

Variant vs Size

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.

distroless image families and variant

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.

Figure: A quick grype run against apsnet:8.0 and aspnet:8.0-alpine



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.

Enjoyed the article?
Buy me a coffee


This content originally appeared on DEV Community and was authored by Bill