DEV Community

Daan Acohen
Daan Acohen

Posted on

Alpine-Like Container Security, Debian-Like Compatibility: Why I Picked Chiseled for .NET

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Minimal project file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>
</Project>
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Optional vulnerability scans:

trivy image demo-debian
trivy image demo-alpine
trivy image demo-chiseled
Enter fullscreen mode Exit fullscreen mode

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:

  1. Start with Ubuntu Chiseled for production services.
  2. Use Alpine when musl compatibility is proven for your full dependency chain.
  3. Use Debian when you intentionally prioritize convenience/broad compatibility over minimal footprint.
  4. Always validate native behavior inside the exact runtime image you ship.

That keeps the decision engineering-driven and reproducible.

Source material:

Top comments (0)