- Published on
- 🍵 4 min read
My Experiences with .NET Chiseled Images
- Authors
- Name
- Emin Vergil
- @eminvergil
Introduction
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:
Optimization | Description |
---|---|
Query Optimization | Detect the queries that have performance issues, create necessary indexes, and check the queries. (Important note: Do not use SELECT * ) |
Update .NET Version | Updated the project to .NET 8 |
Remove Unnecessary Libraries, Implementations | This project was cloned from a template, so I had to remove some unused libraries and implementations |
Multi-tenant Structure | This project was planned to launch on cloud so many companies would use it, so we had to implement a multi-tenant structure |
Add Redis | To 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 RabbitMQ | For pub/sub mechanism, RabbitMQ was used in this project. I reduced query size of payload to handle messages faster |
Memory/CPU Controls | Increased memory and CPU sizes of applications and also added multi-node structure to increase availability |
Logging Structure | There 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 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 /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 /opt/chisel/chisel /usr/bin/
COPY / /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 /rootfs /
WORKDIR /app
COPY /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.