Optimizing Kubernetes Images for Homelab Resources
Running a complex microservices stack on limited hardware. How we use .NET 10 Chiseled Ubuntu images and Native AOT to slash memory usage.
Introduction
BlueRobin runs on a modest Kubernetes cluster (K3s). We don’t have infinite cloud RAM. When running 15+ pods (API, Workers, Databases, Monitoring), every megabyte counts.
Standard .NET container images are “safe” but bloated. They contain shells, package managers, and binaries we never use.
Why Optimization Matters:
- Density: Run more services on the same hardware.
- Security: “Chiseled” images have no shell (
/bin/sh), minimizing attack surface. - Startup Time: Native AOT starts in milliseconds, critical for scaling.
What We’ll Build
We will transform our Dockerfile from a standard 300MB image to a highly optimized 30MB image using multi-stage builds and Chiseled Ubuntu.
Architecture Overview
We rely on Microsoft’s “Chiseled” images—stripped-down versions of Ubuntu designed solely for running an app, no administration tools included.
Section 1: The Multi-Stage Build
We compile in a full SDK container, but publish to a minimal runtime.
# Build Stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish "src/Archives.Api" -c Release -o /app/publish /p:UseAppHost=false
# Runtime Stage (Chiseled)
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "Archives.Api.dll"]
This simple change drops the image size from ~250MB (Debian default) to ~90MB.
Section 2: Native AOT (Ahead-of-Time)
For our Worker services (which process queues), we can go further. Native AOT compiles the C# code directly to machine code, removing the need for the JIT compiler and part of the runtime.
Project File Changes:
<PropertyGroup>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
Dockerfile for AOT:
# Runtime Stage (Deps only)
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled-aot
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["./Archives.Workers"]
Result:
- Image Size: ~25MB
- Startup Time: 15ms
- Memory Footprint: ~18MB RAM (idle)
Section 3: Tree Shaking
Even without AOT, .NET 10 performs “trimming” (Tree Shaking) during publish. It analyzes your code and removes unused classes from the System libraries.
We ensure this is enabled in our Directory.Build.props:
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
</PropertyGroup>
Conclusion
By caring about our artifacts, we reduced the total memory footprint of the BlueRobin application layer by 60%. This allows us to allocate more RAM to where it’s actually needed: the Database and Vector Index.
Next Steps:
- Verify performance gains with Benchmarking.
- See how this enables faster Contract Testing pipelines.