A Practical Guide to .NET Container Images

A Practical Guide to .NET Container Images

When you pull a container image from mcr.microsoft.com/dotnet/*, you’re not just getting a runtime. You’re pulling from a thoughtfully layered collection of images, each designed to be secure, lightweight, and optimized for a specific purpose.

Grasping these layers makes it easier to troubleshoot issues, improve performance, enhance security, and choose the right image for your application.

Understanding .NET Image Families

.NET container images are grouped into families, each tailored for a specific role: building apps, running them, hosting web applications, or acting as a base for self-contained applications.

Each family is layered on top of the one beneath it, adding only what’s necessary. These layers directly influence image size and the libraries included by default.


Article content

Container Image Size vs Family


Article content


Article content

Breaking Down the Layers

1. Runtime-Deps Layer

This is the base layer: a minimal Linux image with just enough to run a native .NET binary.

Use cases:

  • Self-contained apps that bundle their own runtime
  • Native AOT (Ahead-of-Time) compiled apps

Includes:

  • System libraries (e.g., libc, libssl)
  • CA certificates for HTTPS

dotnet publish -c Release -r linux-x64 --self-contained true -o ./out

Article content

2. .NET Runtime Layer

Adds the .NET runtime so framework-dependent apps can run.

Ideal for:

  • CLI tools, background workers, gRPC services
  • Non-web apps

Note: Does not include web-specific libraries or compilers.

3. ASP.NET Layer

Optimized for hosting ASP.NET Core applications.

Includes:

  • Kestrel web server
  • MVC, SignalR libraries

Best for:

  • Production web APIs and web apps

4. SDK Layer

Contains everything needed for building and testing .NET apps, but shouldn’t be shipped to production.

Includes:

  • Compilers and build tools (MSBuild)
  • NuGet package management
  • Git

Recommendation: Use multi-stage Dockerfiles to produce lean production images.

dockerfile

# Example multi-stage Dockerfile

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build

WORKDIR /src

COPY . .

RUN dotnet publish -c Release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:8.0

COPY --from=build /app /app


Article content

Image Tag Anatomy

Each .NET container image tag encodes five key choices:

  1. .NET version
  2. Base OS
  3. OS variant
  4. Runtime type
  5. CPU architecture

Article content

Understanding these components helps you make informed trade-offs for size, security, and compatibility rather than relying on defaults.

Image Variants

Variants modify base images to fit specific needs. They can:

  • Add/remove shells or package managers
  • Include globalization support
  • Optimize startup performance

These choices affect image size, performance, and security.

Example: “jammy” is an Ubuntu release codename used in some variants.

Article content
Article content

Composite Variant (-composite)

Composite images merge all shared-framework assemblies into a single precompiled binary blob, reducing JIT warm-up and speeding cold starts.

Pros:

  • Faster startup (great for serverless or short-lived tasks)

Cons:

  • Larger base layer
  • Version lock-in (any upgrade requires a full image rebuild)
  • Not suitable for plug-in systems or scenarios needing individual DLL swaps

Build a composite image with:

dotnet publish /p:PublishReadyToRun=true

Distroless Images

Distroless images are minimalist, reducing attack surface and image size.

Characteristics:

  • No shell, no package manager, no root access
  • Default runs as app user
  • Add -extra for full globalization support

Note: Alpine Linux uses musl instead of glibc. Many glibc binaries won’t run on Alpine without recompilation or a compatibility layer like gcompat.


Article content

Native AOT Images

Native AOT (Ahead-of-Time) images precompile apps into single native binaries, eliminating the need for CoreCLR or JIT.

Pros:

  • Ultra-fast startup
  • Low memory usage
  • Small image sizes (often <30 MB)
  • No .NET runtime required

Build command:

dotnet publish -c Release -r linux-x64 /p:PublishAot=true

Usage:

  • Build with sdk:*‑aot
  • Run with runtime-deps:*‑aot

Security Considerations

Every extra package increases potential vulnerabilities. Larger images often include shells, compilers, and debugging tools, which are convenient for development but risky in production.

Switching from a “fat” Debian-based ASP.NET image to a lean Alpine variant drastically reduces both package count and security exposure.

Conclusion

There’s no universal “best” .NET container image—only the one that fits your scenario.

  • Full SDK: convenient for building, not ideal for production
  • Distroless: small and secure, limited debug capability
  • Composite or AOT: optimized for fast startup and minimal footprint

Understanding official image layers helps you make intentional, informed choices for your app’s runtime environment. Pick images based on needs, not habits.

To view or add a comment, sign in

Explore topics