Published on

My Experiences with .NET Chiseled Images

Authors

There were awesome talks in the .NET Conf 2023 keynote. One of the videos that interested me was about new .NET images. You can watch the talk here.

At the time, I was doing some performance optimization on a project. There was a deadline, and I was working to make the project production-ready. Here are some of the improvements that I made:

OptimizationDescription
Query OptimizationDetect the queries that have performance issues, create necessary indexes, and check the queries. (Important note: Do not use SELECT *)
Update .NET VersionUpdated the project to .NET 8
Remove Unnecessary Libraries, ImplementationsThis project was cloned from a template, so I had to remove some unused libraries and implementations
Multi-tenant StructureThis project was planned to launch on cloud so many companies would use it, so we had to implement a multi-tenant structure
Add RedisTo improve performance, I had to add Redis. I also used hybrid caching as it resulted in better performance in some places
Optimize Payload Size on RabbitMQFor pub/sub mechanism, RabbitMQ was used in this project. I reduced query size of payload to handle messages faster
Memory/CPU ControlsIncreased memory and CPU sizes of applications and also added multi-node structure to increase availability
Logging StructureThere was no general logging structure used in this project. I chose Serilog first but for faster results, I ended up using NLog

After these optimizations, one of the things that bothered me was the Docker size of the project. It was 370MB at the time. First, I reduced the size by 80MB by removing unused libraries and implementations. Then I watched the talk in .NET Conf 2023 keynote about .NET chiseled images. It was good for both performance and security because in chiseled images there is no root user and there are no unnecessary packages (there is no package manager so you can't use apt).

You can see the detailed explanations in this article.

Also, you can check the chiseled images and sizes on Microsoft here.

Example Dockerfile I used first:


# Learn about building .NET container images:
# https://github.com/dotnet/dotnet-docker/blob/main/samples/README.md
FROM --platform=$BUILDPLATFORM  mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build
ARG TARGETARCH
WORKDIR /source

# copy csproj and restore as distinct layers
COPY aspnetapp/*.csproj .
RUN dotnet restore -a $TARGETARCH

# copy everything else and build app
COPY aspnetapp/. .
RUN dotnet publish -a $TARGETARCH --no-restore -o /app


# final stage/image
FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled
EXPOSE 8080
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["./aspnetapp"]

But there was an error because the project I am working on has a dependency on libicu70 library. I found a workaround here.

So the final Dockerfile was similar to this:


FROM golang:1.20 as chisel

RUN git clone --depth 1 -b main https://github.com/canonical/chisel /opt/chisel
WORKDIR /opt/chisel
RUN go build ./cmd/chisel


FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build

RUN apt-get update \
    && apt-get install -y fdupes \
    && rm -rf /var/lib/apt/lists/*

COPY --from=chisel /opt/chisel/chisel /usr/bin/
COPY --from=mcr.microsoft.com/dotnet/nightly/runtime:6.0-jammy-chiseled / /runtime-ref

RUN mkdir /rootfs \
    && chisel cut --release "ubuntu-22.04" --root /rootfs \
        libicu70_libs \
    \
    # Remove duplicates from rootfs that exist in runtime-ref
    && fdupes /runtime-ref /rootfs -rdpN \
    \
    # Delete duplicate symlinks
    # Function to find and format symlinks w/o including root dir (format: /path/to/symlink /path/to/target)
    && getsymlinks() { find $1 -type l -printf '%p %l\n' | sed -n "s/^\\$1\\(.*\\)/\\1/p"; } \
    # Combine set of symlinks between rootfs and runtime-ref
    && (getsymlinks "/rootfs"; getsymlinks "/runtime-ref") \
        # Sort them
        | sort \
        # Find the duplicates
        | uniq -d \
        # Extract just the path to the symlink
        | cut -d' ' -f1 \
        # Prepend the rootfs directory to the paths
        | sed -e 's/^/\/rootfs/' \
        # Delete the files
        | xargs rm \
    \
    # Delete empty directories
    && find /rootfs -type d -empty -delete

WORKDIR /source

# copy csproj and restore as distinct layers
COPY *.csproj .
RUN dotnet restore

# copy and publish app and libraries
COPY . .
RUN dotnet publish -c release -o /app --no-restore


# final stage/image
FROM mcr.microsoft.com/dotnet/nightly/runtime:8.0-jammy-chiseled

ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false

COPY --from=build /rootfs /
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "dotnetapp.dll"]


After this, the Docker image's size was reduced by approximately 50% - now it is 180MB. The security benefits come with it as chiseled images don't have a root user.