Skip to content
  • English Deutsch

benjamin-abt.com

Performance is a feature | Cloud - AI - Azure - .NET - Sustainability

  • Home
  • Blog
  • Portfolio
    • Overview
    • Workshops & Training
    • Publications
  • Contact
  • Home
  • Blog

Distroless, Chiseled or Alpine: Choosing a .NET Base Image

Distroless, Chiseled or Alpine: Choosing a .NET Base Image
October 27, 2026 10 min read BEN ABT
.NET .NET 11 Csharp Containers Docker Security

Distroless, Chiseled and Alpine are often compared as three equivalent Linux distributions. They are not.

Alpine is a distribution built around musl, BusyBox and apk. Distroless describes a runtime image without a general-purpose shell or package manager. Ubuntu Chiseled and Azure Linux Distroless are two Microsoft-supported .NET image families implementing that model on glibc-based systems.

That distinction determines the selection order:

  1. Which C library and native assets does the application require?
  2. Does it need ICU and time-zone data?
  3. Is the output framework-dependent, self-contained or Native AOT?
  4. Can diagnostics run outside the application image?
  5. Which operating-system family can the platform team patch and support?

Image size comes after those questions. A smaller image that cannot load a vendor library or formats customer data incorrectly is not optimized; it is incompatible.

Release status: The .NET 11 image names reflect Preview 7 from August 2026. Preview tags are mutable and must be replaced with final serviced tags and immutable digests before production use.

Recommended defaults

Image familyRecommended when
Ubuntu ChiseledA normal minimal ASP.NET Core service needs broad glibc compatibility
Azure Linux DistrolessThe platform is already governed and serviced around Azure Linux
AlpineEvery native dependency is deliberately tested against musl
Full Debian, Ubuntu, Azure Linux or AlpineRuntime package installation or shell-based vendor tooling is a real requirement

For a new conventional service, Ubuntu Chiseled is the recommended default. It removes the shell and package manager while retaining the glibc compatibility expected by most Linux vendors and native NuGet assets.

Running on Azure does not itself require Azure Linux. Existing platform governance is the useful reason to choose it.

Alpine is a platform decision, not a generic small-image switch.

The terminology that matters

Microsoft uses distroless as a category containing Ubuntu Chiseled and Azure Linux Distroless images. Google’s Distroless project popularized the name but does not publish Microsoft’s official .NET runtime images.

For a normal .NET workload, mcr.microsoft.com/dotnet images are preferable because runtime, non-root user, port metadata, servicing and feature variants are coordinated with the .NET release.

The practical comparison is:

CharacteristicAlpineUbuntu ChiseledAzure Linux Distroless
C librarymuslglibcglibc
Runtime shellBusyBox shNoneNone
Runtime package managerapkNoneNone
Minimal variant includes ICU and tzdataNoNoNo
extra variantYesYesYes
Best alignmentmusl and Alpine estateUbuntu and glibc estateAzure Linux estate

No shell reduces post-exploitation convenience. It does not create a sandbox. The process still has a runtime, network access, mounted credentials, kernel interfaces and writable volumes according to its deployment policy.

glibc versus musl is the hard boundary

Managed code is mostly portable across Linux distributions. Native code is not. This includes P/Invoke libraries, RID-specific NuGet assets, image and PDF libraries, database drivers, Kerberos, LDAP, compression modules, observability agents and vendor binaries.

The Runtime Identifier states the contract:

1glibc x64      linux-x64
2glibc Arm64    linux-arm64
3musl x64       linux-musl-x64
4musl Arm64     linux-musl-arm64

For Alpine, publishing should use a musl RID whenever the application or a package contains native assets:

Bash
1dotnet publish \
2  --configuration Release \
3  --runtime linux-musl-x64 \
4  --self-contained false

Copying linux-x64 output into Alpine can select a glibc native asset. The file may exist and match the CPU architecture yet still fail with DllNotFoundException because its loader or transitive C library is incompatible.

Alpine compatibility packages can solve specific cases. They do not turn musl into glibc. When an opaque vendor library only documents glibc, Chiseled or a full glibc image is safer than a production compatibility experiment.

Globalization is an explicit runtime dependency

The smallest variants omit ICU and time-zone data across all three image families. An application can declare invariant globalization:

XML
1<PropertyGroup>
2  <InvariantGlobalization>true</InvariantGlobalization>
3</PropertyGroup>

That is suitable for services whose contracts use ordinal identifiers, ISO timestamps and culture-independent numbers. It is wrong when the application requires culture-sensitive formatting, casing, sorting or parsing. Time-zone support is a separate dependency: invariant globalization does not itself remove named time zones, but a minimal image without tzdata cannot supply the normal IANA zone database.

Those applications should select the official extra variant instead of installing ICU manually:

1mcr.microsoft.com/dotnet/aspnet:11.0-preview-alpine-extra
2mcr.microsoft.com/dotnet/aspnet:11.0-preview-resolute-chiseled-extra
3mcr.microsoft.com/dotnet/aspnet:11.0-preview-azurelinux4.0-distroless-extra

extra is not a failed optimization. It describes the real runtime contract and receives coordinated servicing.

Globalization should be tested inside the final image:

C#
 1using System.Globalization;
 2using Xunit;
 3
 4public sealed class ContainerGlobalizationTests
 5{
 6    [Theory]
 7    [InlineData("de-DE", "1.234,50 EUR")]
 8    [InlineData("en-US", "1,234.50 EUR")]
 9    public void CurrencyContract_FormatsUsingExpectedCulture(
10        string cultureName,
11        string expected)
12    {
13        CultureInfo culture = CultureInfo.GetCultureInfo(cultureName);
14        decimal amount = 1234.50m;
15
16        string actual = string.Format(
17            culture,
18            "{0:N2} EUR",
19            amount);
20
21        Assert.Equal(expected, actual);
22    }
23}

A test on a Windows workstation or full Ubuntu build agent says nothing about ICU in the runtime image.

Match the repository to the publish model

Publish modelRuntime repository
Framework-dependent ASP.NET Coredotnet/aspnet
Framework-dependent worker or consoledotnet/runtime
Self-containeddotnet/runtime-deps
Native AOTdotnet/runtime-deps

Using aspnet for a self-contained application ships a runtime it does not need. Using runtime-deps for a framework-dependent service omits the runtime it does need.

Suffixes describe separate features. extra adds ICU and tzdata. In .NET 10 and later, aot identifies SDK images with the native compilation toolchain; it is no longer a separate minimal runtime-deps variant. composite applies to framework-dependent ASP.NET Core images and combines framework assemblies into a composite ReadyToRun image, reducing disk size at the cost of tighter version coupling. Ordinary ASP.NET Core images already contain ReadyToRun code. These distinctions are documented in Microsoft’s image variant reference .

The final tag should be derived in this order:

OS family -> libc -> publish model -> globalization -> optional composite/AOT

Copying the shortest tag from a benchmark reverses that reasoning.

Correct final stages

Ubuntu Chiseled keeps glibc and removes runtime tooling:

Dockerfile
 1FROM mcr.microsoft.com/dotnet/sdk:11.0-preview AS build
 2WORKDIR /src
 3
 4COPY ["Orders.Api/Orders.Api.csproj", "Orders.Api/"]
 5RUN dotnet restore "Orders.Api/Orders.Api.csproj"
 6
 7COPY . .
 8RUN dotnet publish "Orders.Api/Orders.Api.csproj" \
 9    --configuration Release \
10    --output /app \
11    --no-restore \
12    --self-contained false
13
14FROM mcr.microsoft.com/dotnet/aspnet:11.0-preview-resolute-chiseled AS final
15WORKDIR /app
16COPY --from=build /app .
17USER $APP_UID
18ENTRYPOINT ["dotnet", "Orders.Api.dll"]

Alpine uses matching musl restore and publish:

Dockerfile
 1FROM mcr.microsoft.com/dotnet/sdk:11.0-preview-alpine AS build
 2WORKDIR /src
 3
 4COPY ["Orders.Api/Orders.Api.csproj", "Orders.Api/"]
 5RUN dotnet restore "Orders.Api/Orders.Api.csproj" \
 6    --runtime linux-musl-x64
 7
 8COPY . .
 9RUN dotnet publish "Orders.Api/Orders.Api.csproj" \
10    --configuration Release \
11    --runtime linux-musl-x64 \
12    --self-contained false \
13    --output /app \
14    --no-restore
15
16FROM mcr.microsoft.com/dotnet/aspnet:11.0-preview-alpine AS final
17WORKDIR /app
18COPY --from=build /app .
19USER $APP_UID
20ENTRYPOINT ["dotnet", "Orders.Api.dll"]

These two minimal final stages assume <InvariantGlobalization>true</InvariantGlobalization> in the application project. Applications requiring culture support should use the matching -extra final image instead. Real repositories must also copy restore inputs such as Directory.Packages.props, Directory.Build.props, NuGet.config and referenced projects before dotnet restore.

The build stage can contain normal tools. The final stage defines production attack surface and compatibility.

Exec-form ENTRYPOINT works without a shell and gives the application direct signal handling. Shell-form entry points fail in a shell-less image and add unnecessary process indirection elsewhere.

A Dockerfile is optional for normal images

When the image only adds published files and metadata, the SDK can use the exact base image:

XML
 1<Project Sdk="Microsoft.NET.Sdk.Web">
 2  <PropertyGroup>
 3    <TargetFramework>net11.0</TargetFramework>
 4    <Nullable>enable</Nullable>
 5    <ImplicitUsings>enable</ImplicitUsings>
 6    <ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:11.0-preview-resolute-chiseled-extra</ContainerBaseImage>
 7    <ContainerRepository>orders-api</ContainerRepository>
 8    <ContainerImageTag>1.0.0-preview.7</ContainerImageTag>
 9  </PropertyGroup>
10</Project>
Bash
1dotnet publish \
2  --configuration Release \
3  --os linux \
4  --arch x64 \
5  /t:PublishContainer

OS package installation requires a Dockerfile or an organization-owned custom base. A custom base is preferable when several services need the same capability; otherwise every application team inherits package updates, transitive vulnerabilities and inconsistent hardening.

If runtime package changes are frequent and application-specific, a full distribution image is more honest than rebuilding a pseudo-distroless filesystem in every repository.

Minimal images change diagnostics

kubectl exec ... -- sh is unavailable in Chiseled and Distroless images. This is an operating constraint to design for, not a reason to add a shell back.

The application exposes structured logs, OpenTelemetry, health endpoints, runtime counters and bounded diagnostic endpoints. Health probes use HTTP or TCP rather than curl inside the container.

For runtime diagnostics, dotnet-monitor can run as a sidecar over a shared diagnostic socket. Kubernetes ephemeral debug containers add temporary tooling without mutating the production image. Dumps write to a declared volume with capacity, retention, sensitivity and upload policies.

These paths must be tested before an incident. A runbook that assumes a shell is not a runbook for a distroless deployment.

Alpine has BusyBox sh, but its tools and flags differ from GNU userland. The existence of a shell does not guarantee the commands an operator expects.

Smaller images help, but size is not security

Removing packages reduces components that can contain vulnerabilities and tools available after code execution. It also reduces scanner noise. The final security posture still depends on non-root execution, capabilities, seccomp, read-only filesystems, network policy, workload identity, provenance and patch cadence.

Raw CVE counts are not comparable across Alpine, Ubuntu and Azure Linux because advisory databases and backport policies differ. Analysis should determine whether the package exists, whether the path is reachable, whether the distribution has patched it, whether the image remains serviced and how quickly the application can rebuild.

Containers are patched by rebuilding from a serviced base, not by running a package manager inside a live pod. Removing apt or apk only helps when external rebuild automation exists.

Non-root remains independent from the image label

Microsoft’s modern Linux images define app and APP_UID. The user should be stated explicitly in Dockerfiles and reinforced in deployment policy:

YAML
1securityContext:
2  runAsNonRoot: true
3  allowPrivilegeEscalation: false
4  readOnlyRootFilesystem: true
5  capabilities:
6    drop:
7      - ALL

Temporary data, data-protection keys, SQLite files, certificates and dumps need explicit writable mounts. Running as root to solve a volume-permission error removes a useful boundary regardless of whether the base image is called Alpine or Chiseled.

Native AOT keeps the libc decision

Native AOT output uses runtime-deps, but it still links native dependencies. An Alpine binary needs a musl RID and compatible toolchain:

Bash
1dotnet publish \
2  --configuration Release \
3  --runtime linux-musl-x64 \
4  --self-contained true \
5  -p:PublishAot=true

For .NET 11, compile with a matching sdk image carrying the -aot toolchain and run the result on a compatible runtime-deps image. The minimal Chiseled and Distroless runtime images omit globalization data; select -extra when ICU and tzdata are required and verify any additional native library separately. Older .NET releases used distinct runtime-deps tags ending in -aot, so their tag guidance should not be copied to .NET 11.

A correct base image does not prove Native AOT compatibility. Reflection, dynamic code, trimming, serialization metadata and library support are separate application constraints.

Measure comparable artifacts

Image comparisons often mix compressed registry size, local uncompressed size, shared layers, framework-dependent output and Native AOT output.

A useful measurement holds .NET patch, architecture, publish mode, globalization, composite setting and application payload constant. Layer reuse matters at fleet scale: hundreds of services sharing one slightly larger base can consume less network and node storage than several individually smaller unrelated bases.

Pull size also does not predict application startup after layers are cached. Startup depends much more on application initialization, JIT or ReadyToRun, dependencies and platform scheduling.

Adoption test matrix

Changing image family is a runtime migration. Tests should cover:

  1. Startup, non-root UID, read-only root filesystem and graceful SIGTERM on each architecture.
  2. Every native code path, RID asset, database driver, imaging library, authentication module and telemetry agent.
  3. Culture formatting, parsing, Unicode behavior, time zones and daylight-saving transitions.
  4. DNS, TLS, private CAs, proxies, Kerberos, LDAP and HTTP/3 where used.
  5. Liveness, readiness, traces, dumps and the approved ephemeral-debug procedure.
  6. Final-image SBOM, distribution-aware vulnerability scan, digest pinning, signatures and provenance.

A successful docker run only proves that the process started.

The decision

Most ASP.NET Core services should start with official Ubuntu Chiseled, using extra whenever globalization is part of the application contract. Azure Linux Distroless is equally reasonable when the platform already owns that ecosystem. Alpine is appropriate when musl support is a tested organizational choice.

A full distribution remains correct when runtime package or shell requirements are genuine. Minimal images are not a maturity badge.

The best image is the smallest serviced image that satisfies the application’s tested libc, globalization, native dependency, diagnostics and security contracts. Size is the result of that decision, not its input.

For building the selected image with dotnet publish, OCI archives and immutable tags, see Containerizing .NET in 2026 Without a Dockerfile .

References

  • Microsoft: .NET container images
  • Microsoft .NET Docker: Image variants
  • Microsoft .NET Docker: Ubuntu Chiseled
  • Microsoft .NET Docker: Distroless images
  • Canonical: Chisel
  • GoogleContainerTools: Distroless

Related articles

Containerizing .NET in 2026 Without a Dockerfile

Oct 20, 2026 - 11 min read

Containerizing .NET in 2026 Without a Dockerfile

Most .NET Dockerfiles repeat the same five steps: restore, publish, select the matching Microsoft runtime image, copy the output and start …

Stop Using nginx as Your Default Static File Server in Docker

Oct 24, 2026 - 15 min read

Stop Using nginx as Your Default Static File Server in Docker

In the week of October 21, 2026, government portals in the Netherlands, Finland and Germany, hospital websites and several large companies …

Problem Details in ASP.NET Core and .NET 11

Oct 5, 2026 - 12 min read

Problem Details in ASP.NET Core and .NET 11

builder.Services.AddProblemDetails() registers infrastructure. It does not create an error contract. An ASP.NET Core request can fail during …


BEN ABT

BEN ABT

Ben is Chief Engineering Officer and Principal Software Engineer at Medialesson.de, with a strong focus on .NET and Microsoft Azure. In his professional role, he designs and builds highly scalable platforms for hybrid and cloud-based scenarios. He also advises companies and C-levels on migrating to the sovereign cloud.

He runs myCSharp.de, the largest and most active German-speaking C# community forum, actively contributes to both his own and external open-source projects and regularly shares his knowledge through his blog, at community events, conferences and as a writer for technical magazines.

Let's Work Together

Looking for an experienced Platform Architect or Engineer for your next project? Whether it's cloud migration, platform modernization or building new solutions from scratch - I'm here to help you succeed.

New Platforms

Modernization

Training & Consulting

Contact Me
X Facebook LinkedIn WhatsApp
Back

Open for work!

Contact Me
BEN ABT BEN ABT Software Engineer · Microsoft MVP Passionate about .NET, Azure, performance optimization and sustainable software engineering.

Medialesson GmbH Logo

Microsoft MVP Logo

Recent posts

View all
Distroless, Chiseled or Alpine: Choosing a .NET Base Image

Distroless, Chiseled or Alpine: Choosing a .NET Base Image

Oct 27, 2026
Stop Using nginx as Your Default Static File Server in Docker

Stop Using nginx as Your Default Static File Server in …

Oct 24, 2026
Containerizing .NET in 2026 Without a Dockerfile

Containerizing .NET in 2026 Without a Dockerfile

Oct 20, 2026
Problem Details in ASP.NET Core and .NET 11

Problem Details in ASP.NET Core and .NET 11

Oct 5, 2026
Resilience in .NET with Microsoft.Extensions.Resilience

Resilience in .NET with Microsoft.Extensions.Resilience

Sep 21, 2026

Tags

.NET (193) Csharp (97) ASP.NET Core (48) Azure (41) DevOps (17) Docker (15) Microsoft (14) EF Core (11) Microsoft MVP (11) AI (10) GitHub (10) GitHub Copilot (10) Security (10) ASP.NET (9) Synology (9) Architecture (8) Raspberry (8) Azure Functions (7) Cloudflare (7) .NET 11 (6)
benjamin-abt.com

About BEN

Benjamin Abt is a Software Engineer, Platform Architect and Microsoft MVP specializing in .NET, Azure, cloud architecture, performance optimization and sustainable software development.

Connect

  • Follow me on GitHub
  • Follow me on LinkedIn
  • Follow me on Mastodon
  • Microsoft MVP profile
  • Follow SchwabenCode on GitHub
  • Follow me on X

Quick Links

  • Portfolio
  • Blog
  • Workshops & Training
  • Publications
  • Get in Touch
  • Sitemap
Open for Work

© Copyright Benjamin Abt & SchwabenCode | 2011 - 2026

Content licensed under CC BY 4.0 unless otherwise noted.

  • Impressum
  • Privacy
  • Contact