All the .NET Core Opsy Things Part 1: Choosing the right .NET core Image for your workload Bill 7 min read · 3 days ago 3 days ago -- Listen Share 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 . Image family vs Size Understanding runtime-deps The lowest layer: a minimal Linux image with no package managers and just enough to run a native .NET binary. Use when: Your app is self-contained i.e: includes its own runtime. 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 Zoom image will be displayed 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. Zoom image will be displayed Understanding the anatomy helps you make deliberate trade-offs for size, security, and compatibility, rather than relying on defaults. Zoom image will be displayed 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. Variants vs Size PS: jammy is a release code name for an ubuntu base Variant Size (MB) comparison 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 Note: Because Alpine Linux uses musl instead of glibc and the two aren’t ABI-compatible, many glibc-built binaries won’t run on Alpine unless you recompile them or use a compatibility layer (e.g., gcompat) 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. : No JIT means cold starts are significantly quicker. Lower memory footprint : Only the app code and linked native dependencies are loaded. : Only the app code and linked native dependencies are loaded. Smaller container size : Final images are typically under 30 MB. 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. 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 variant. Zoom image will be displayed 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.