
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:
- Which C library and native assets does the application require?
- Does it need ICU and time-zone data?
- Is the output framework-dependent, self-contained or Native AOT?
- Can diagnostics run outside the application image?
- 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 family | Recommended when |
|---|---|
| Ubuntu Chiseled | A normal minimal ASP.NET Core service needs broad glibc compatibility |
| Azure Linux Distroless | The platform is already governed and serviced around Azure Linux |
| Alpine | Every native dependency is deliberately tested against musl |
| Full Debian, Ubuntu, Azure Linux or Alpine | Runtime 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:
| Characteristic | Alpine | Ubuntu Chiseled | Azure Linux Distroless |
|---|---|---|---|
| C library | musl | glibc | glibc |
| Runtime shell | BusyBox sh | None | None |
| Runtime package manager | apk | None | None |
| Minimal variant includes ICU and tzdata | No | No | No |
extra variant | Yes | Yes | Yes |
| Best alignment | musl and Alpine estate | Ubuntu and glibc estate | Azure 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-arm64For Alpine, publishing should use a musl RID whenever the application or a package contains native assets:
1dotnet publish \
2 --configuration Release \
3 --runtime linux-musl-x64 \
4 --self-contained falseCopying 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:
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-extraextra is not a failed optimization. It describes the real runtime contract and receives coordinated servicing.
Globalization should be tested inside the final image:
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 model | Runtime repository |
|---|---|
| Framework-dependent ASP.NET Core | dotnet/aspnet |
| Framework-dependent worker or console | dotnet/runtime |
| Self-contained | dotnet/runtime-deps |
| Native AOT | dotnet/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/AOTCopying the shortest tag from a benchmark reverses that reasoning.
Correct final stages
Ubuntu Chiseled keeps glibc and removes runtime tooling:
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:
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:
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>1dotnet publish \
2 --configuration Release \
3 --os linux \
4 --arch x64 \
5 /t:PublishContainerOS 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:
1securityContext:
2 runAsNonRoot: true
3 allowPrivilegeEscalation: false
4 readOnlyRootFilesystem: true
5 capabilities:
6 drop:
7 - ALLTemporary 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:
1dotnet publish \
2 --configuration Release \
3 --runtime linux-musl-x64 \
4 --self-contained true \
5 -p:PublishAot=trueFor .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:
- Startup, non-root UID, read-only root filesystem and graceful
SIGTERMon each architecture. - Every native code path, RID asset, database driver, imaging library, authentication module and telemetry agent.
- Culture formatting, parsing, Unicode behavior, time zones and daylight-saving transitions.
- DNS, TLS, private CAs, proxies, Kerberos, LDAP and HTTP/3 where used.
- Liveness, readiness, traces, dumps and the approved ephemeral-debug procedure.
- 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
Related articles

- 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 …

- 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 …

- 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 …
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.
