If you build .NET services in containers, you’ve likely heard the default advice: pick the smallest image.
For production workloads, that rule is incomplete. In practice, runtime image selection is a trade-off between native compatibility, security posture, and image footprint. To make that trade-off concrete, I built a focused benchmark comparing Debian, Alpine, and Ubuntu Chiseled in a native-library scenario: https://github.com/ConnectingApps/DockerImageSizes
For official context on Chiseled containers in .NET, see Microsoft’s announcement: https://devblogs.microsoft.com/dotnet/announcing-dotnet-chiseled-containers/
Why this test is useful for real .NET workloads
A managed-only sample often hides the real problems. Many .NET services eventually rely on native Linux libraries, either directly through P/Invoke or indirectly through dependencies.
So this benchmark uses a .NET 8 app that calls libuuid.so.1 from C#. That creates a clear runtime compatibility signal in each base image.
using System;
using System.Runtime.InteropServices;
class Program
{
[DllImport("libuuid.so.1")]
private static extern void uuid_generate(byte[] buffer);
static void Main()
{
Console.WriteLine("Generating UUID using native glibc library...");
try
{
var buffer = new byte[16];
uuid_generate(buffer);
Console.WriteLine("Success.");
}
catch (Exception ex)
{
Console.WriteLine("Exception:");
Console.WriteLine(ex);
}
}
}
Minimal project file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>
Docker image variants compared
The benchmark compares these runtime bases:
- Debian:
mcr.microsoft.com/dotnet/runtime:8.0 - Alpine:
mcr.microsoft.com/dotnet/runtime:8.0-alpine - Ubuntu Chiseled:
mcr.microsoft.com/dotnet/runtime:8.0-jammy-chiseled
Debian Dockerfile
# ===== Build stage =====
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY NativeDemo.csproj .
RUN dotnet restore
COPY Program.cs .
RUN dotnet publish -c Release -o /app
# ===== Runtime stage =====
FROM mcr.microsoft.com/dotnet/runtime:8.0
RUN apt update && apt install -y libuuid1
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "NativeDemo.dll"]
Alpine Dockerfile
# ===== Build stage =====
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY NativeDemo.csproj .
RUN dotnet restore
COPY Program.cs .
RUN dotnet publish -c Release -o /app
# ===== Runtime stage =====
FROM mcr.microsoft.com/dotnet/runtime:8.0-alpine
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "NativeDemo.dll"]
Ubuntu Chiseled Dockerfile
# ===== Build stage (.NET) =====
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY NativeDemo.csproj .
RUN dotnet restore
COPY Program.cs .
RUN dotnet publish -c Release -o /app
# ===== Extract libuuid from Ubuntu =====
FROM ubuntu:22.04 AS uuidstage
RUN apt update && apt install -y libuuid1
# ===== Runtime stage =====
FROM mcr.microsoft.com/dotnet/runtime:8.0-jammy-chiseled
WORKDIR /app
COPY --from=build /app .
COPY --from=uuidstage /usr/lib/x86_64-linux-gnu/libuuid.so.1 /usr/lib/
ENTRYPOINT ["dotnet", "NativeDemo.dll"]
Reproduction commands
docker build -f Dockerfile.debian -t demo-debian .
docker run demo-debian
docker build -f Dockerfile.alpine -t demo-alpine .
docker run demo-alpine
docker build -f Dockerfile.chiseled -t demo-chiseled .
docker run demo-chiseled
Optional vulnerability scans:
trivy image demo-debian
trivy image demo-alpine
trivy image demo-chiseled
Measured results
These values are from this repository’s benchmark measurement and can change over time as upstream base images are updated.
| Variant | Runtime base image | Measured size | Trivy summary (measured) | Native libuuid.so.1 call |
|---|---|---|---|---|
| Debian | mcr.microsoft.com/dotnet/runtime:8.0 |
213 MB | 87 CVEs (1 critical, 2 high, 24 medium, 60 low) | ✅ Works |
| Alpine | mcr.microsoft.com/dotnet/runtime:8.0-alpine |
84 MB | 0 CVEs | ❌ Fails (DllNotFoundException) |
| Ubuntu Chiseled | mcr.microsoft.com/dotnet/runtime:8.0-jammy-chiseled |
85.5 MB | 3 CVEs (all low) | ✅ Works |
Interpretation for .NET developers
The key point is not that one distro is always “best.” The key point is that measurable trade-offs appear immediately once native dependencies are involved.
In this benchmark:
- Alpine is smallest and cleanest on the reported scan, but fails the tested glibc-native call.
- Debian works, but has substantially larger footprint and higher vulnerability count in this measurement.
- Ubuntu Chiseled is near Alpine in size while still passing the tested native call, with low reported CVE count.
That combination is why Chiseled often looks attractive for production .NET services that need both lean images and practical native compatibility.
Practical decision model
A pragmatic policy for teams:
- Start with Ubuntu Chiseled for production services.
- Use Alpine when musl compatibility is proven for your full dependency chain.
- Use Debian when you intentionally prioritize convenience/broad compatibility over minimal footprint.
- Always validate native behavior inside the exact runtime image you ship.
That keeps the decision engineering-driven and reproducible.
Source material:
Top comments (0)