.NET container tooling
The SDK container tooling provides an easy way to build container images directly from a .NET project. It was introduced last year via a NuGet package for .NET 7. For .NET 8, it comes included with the SDK.
During .NET 8's development, the container tooling has seen many improvements, including support for building rootless images, support for Podman, and better interoperability with container registries (like Docker Hub, quay.io, and the Amazon container registry).
.NET 8 will be available in November. If you want to try what is shown in this article, you can download a release candidate from Microsoft download page.
If you are on Fedora, you can obtain it from the Fedora .NET SIG copr repo:
dnf copr enable @dotnet-sig/dotnet dnf install dotnet-sdk-8.0
Our first image
For our first container image, we'll containerize an ASP.NET Core application.
First, create an ASP.NET Core web project:
dotnet new web -o web cd web
Now publish it while setting
dotnet publish /p:PublishProfile=DefaultContainer
You can run the application image locally using Podman or Docker and visit the site on the exposed port.
podman run --rm -p 8080:8080 web
publish command outputs something like this:
MSBuild version 17.8.0-preview-23468-06+1b84c9b5c for .NET Determining projects to restore... All projects are up-to-date for restore. web -> /tmp/web/bin/Release/net8.0/web.dll web -> /tmp/web/bin/Release/net8.0/publish/ Building image 'web' with tags 'latest' on top of base image 'mcr.microsoft.com/dotnet/aspnet:8.0-rc.2'. Pushed image 'web:latest' to local registry via 'podman'.
If you look closer at the output, you can see the SDK used
mcr.microsoft.com/dotnet/aspnet:8.0.0-rc.2 as the base image, and created an application image named
web:latest. That's the name we used to run the container locally.
The SDK defaults to use Microsoft base images. It picks an image considering the properties that are set in the project. In this case, we're packing an ASP.NET application that targets .NET 8, so it picks the aspnet base image for that version. If the project changed to a self-contained application, the SDK would use the runtime-deps base image instead.
By default, the application image will be named the same as the output assembly of the .NET project. You can override the image name by setting the
ContainerRepository property in the project file.
Selecting the base image
Microsoft's default images are Debian based. Microsoft publishes additional images based on other distros as you can see from the tag names on the Microsoft runtime repository. The SDK allows you to choose one of the other distros by setting the
ContainerFamily property. This value will be added as a suffix to the base image tag. For example, to use Alpine images you can set:
The SDK isn't limited to using Microsoft images. You can choose a base image using the
ContainerBaseImage property. The following example changes the
TargetFramework to .NET 7 and uses the corresponding Red Hat UBI .NET image. We're using .NET 7 because UBI images for .NET 8 won't be available until .NET 8 GA (November 2023).
If you run the
publish command again, the application image is built on top of the Red Hat base image!
Pushing the image to a repository
To publish the image to a container repository, we need to provide our credentials. The SDK can find credentials entered via Docker/Podman login.
Alternatively, you can also set the credentials using the
SDK_CONTAINER_REGISTRY_PWORD environment variables.
To set the target repository, we need to specify the registry host (
ContainerRegistry), the image repository (
ContainerRepository), and the image tag (
ContainerImageTag). We can add them to the project file, or set them on the command line. The following example is for pushing the application image to
dotnet publish /p:PublishProfile=DefaultContainer /p:ContainerRegistry=quay.io /p:ContainerRepository=tmds/web /p:ContainerImageTag=latest
The default tag for the application image is
latest. To use another, you can either set
ContainerImageTags. The latter property can be set to a semi-colon separated list of tags. To pass it on the command-line with bash, some single and double quotes escaping is needed to make bash and MSBuild accept the property value.
dotnet publish … '/p:ContainerImageTags="tag1;tag2"'
Targeting another architecture
A container registry can support different architectures and operating systems. For example, Microsoft's .NET images (like
mcr.microsoft.com/dotnet/runtime:8.0) support x64, and arm64 on Windows and Linux and Red Hat's .NET images support x64, arm64, ppc64le, and s390x on Linux.
When building an application, .NET supports controlling the target platform through the RuntimeIdentifier property. The same property is used when building container images.
For example, on a Linux/Windows x64 development machine, an arm64 Linux container can be built by setting:
If you want to set the runtime identifier so it only affects the container image, you can set
ContainerRuntimeIdentifier instead of setting the
Similarly, you can target the architectures provided by Red Hat. By default, the SDK includes a native executable (the app host) for the target architecture to start the application. For Microsoft-provided architectures, the SDK will fetch this executable from nuget.org. It is not available for the Red Hat architectures, so we need to disable it. Note that disabling the app host has no effect on how the resulting image works.
<ContainerBaseImage>registry.access.redhat.com/ubi8/dotnet-70-runtime:latest</ContainerBaseImage> <RuntimeIdentifier>linux-ppc64le</RuntimeIdentifier> <UseAppHost>false</UseAppHost>
Alternatively, if we use
ContainerRuntimeIdentifier we don't need to set
false. The application will be published with the app host of the build platform. That executable doesn't work on the target architecture, but that is not an issue because it's not used to start the application in the container.
Publishing self-contained applications
When you build an image for a self-contained application, the published application will include binaries that need to be compatible with the base image.
It's best to explicitly set the runtime identifier to ensure no binaries are used that are meant for the build platform. This might for example happen when a Linux container gets built on a Windows system.
dotnet publish --sc -r linux-x64 /p:PublishProfile=DefaultContainer
To set these values in the .NET project file, you can use
RuntimeIdentifier (as we've done in the previous section) to set the
-r argument, and the
--sc argument can be set by adding a property
SelfContained and setting it to
When you run the command, you'll see the SDK has changed from using the aspnet base image to the runtime-deps image. That makes sense: because ASP.NET is now included with the self-contained application, the base image no longer needs to provide it.
If you want to target Alpine Linux, instead of using the runtime identifier
linux-x64 we need to use
linux-musl-x64. The binaries for
linux-musl-x64 are compatible with Linux distributions that use the musl C library instead of the glibc C library. The SDK does not (yet) pick a proper base image tag for this rid so we also need to set
dotnet publish --sc -r linux-musl-x64 /p:ContainerFamily=alpine /p:PublishProfile=DefaultContainer
Containerizing console applications
.NET 8's built-in SDK container tooling does not support building container images for console applications. These are the .NET projects that use the
Microsoft.NET.SDK as can be seen at the top of the project file:
<Project Sdk="Microsoft.NET.Sdk">. To containerize a console application, you need to use the container support from the NuGet package, and invoke the
dotnet add package Microsoft.NET.Build.Containers dotnet publish /t:PublishContainer
Customizing the application image
In the following sections we'll cover how you can customize the application image further. We'll look at adding container labels, setting environment variables, adding ports, controlling the working directory and user, and customizing the entrypoint and command used to start the application.
Container images can include metadata through labels. These labels can be added from the project file as shown in the following example which adds a build host label that gets set to the machine name.
<ItemGroup> <ContainerLabel Include="com.my-company.buildhost" Value="$([System.Environment]::MachineName)" /> </ItemGroup>
The SDK will add some well-known labels by default. This can be disabled by setting
false. It can also disabled for each label separately.
The following table shows the labels added by the SDK, what properties determine their value, and the property that can be used to disable adding the label (by setting it to
false). Some values are initialized from well-known properties, and can be overridden through container-specific properties.
The source repository and revision information are provided through source-link, which is included with the SDK (since .NET 8). To add these labels,
PublishRepositoryUrl must be set to
Environment variables can be set in the container image through
ContainerEnvironmentVariable as shown in the following example.
<ItemGroup> <ContainerEnvironmentVariable Include="ASPNETCORE_ENVIRONMENT" Value="Production" /> </ItemGroup>
Container images describe the ports they expose. Note that these ports are image metadata, and they do not limit external access to specific ports on a container image. Ports can be added through
ContainerPort as shown in the following example:
<ItemGroup> <ContainerPort Include="5000" Type="tcp"/> </ItemGroup>
The SDK will expose the ports from the base image, and automatically add ports based on the well-known ASP.NET Core environment variables
Microsoft images prior to .NET 8 set the environment variable to use port 80. Starting with .NET 8, the Microsoft images use the same port as Red Hat's images: 8080.
When the container image runs, the working directory is
/app for Linux containers, and
c:\app for Windows containers. The default can be overwritten by setting the
The working directory is also where the .NET application gets published to. This fulfills ASP.NET Core's default behavior which is to use the working directory as the content root (which is used to look up AppSettings and Razor files).
Since .NET 8, Microsoft .NET Linux container images include a non-root user named
app. Like with previous .NET versions, the base image runs as the
root user. If you build an application image through a
Dockerfile, you can choose to make it run under the
app user by adding an
USER $APP_UID instruction. The
$APP_UID used here is an environment variable set by the base image. It contains the
app user's user id (uid).
For all .NET versions, Red Hat .NET images contain a non-root user and the image runs as that non-root user by default.
When the SDK containerizes a .NET application, that application will run as the base image user. If the
APP_UID environment variable is set, that user is used instead.
In practice this means that with Microsoft images for .NET 8 or Red Hat images for any .NET version, the application image runs as a non-root user by default. With Microsoft base images for earlier versions of .NET, the application image will run as the root user by default.
The .NET application that is added to the image is owned by the root user. Consequently, the non-root user that runs the application in the container has no permissions to change the application files. Note that this includes the
appsettings.json file that comes with an ASP.NET Core application.
If you want to explicitly control the user for the application image, you can set it using the
When a .NET image runs on the Red Hat OpenShift container platform, OpenShift will run it under a random user ID and with a group ID (gid) of 0. The random uid is an additional security measure preventing the uid to map to another user on the host. Unlike the uid of 0 (which means root), a group id of 0 has no privileges attached to it. To accommodate working on OpenShift, the Red Hat base image set the
HOME environment to the rootless user's home directory, and this directory is configured with write permissions for its group (gid 0).
Controlling the entrypoint and command
Container images have an entrypoint (
ENTRYPOINT) and a command (
CMD). The entrypoint controls the fixed command line that gets executed when you run a container, and the command controls the default arguments that are passed.
podman run myimage podman run myimage arg1 arg2
For the first command line, the container will execute the image entrypoint and pass it the image command. For the second command line, the container will execute the image entrypoint and pass it the command line arguments instead of the image command.
The entrypoint can be overwritten as well (through the
--entrypoint argument). Also, both values can be controlled on Kubernetes/OpenShift as part of the pod spec. The common practice is to not override these values. For configuration, instead of arguments, environment variables and ConfigMaps are used. This makes the distinction between using an entrypoint or a command unimportant to the Kubernetes consumer.
By default, the application image generated by the SDK contains an entrypoint that starts the application and an empty command.
If you tried to publish the application using the .NET 7 Red Hat images, you may have noticed this warning message:
warning CONTAINER2022: The base image has an entrypoint that will be overwritten to start the application. Set ContainerAppCommandInstruction to 'Entrypoint' if this is desired. To preserve the base image entrypoint, set ContainerAppCommandInstruction to 'DefaultArgs'.
This message indicates that the Red Hat images have an entrypoint. This entrypoint is a helper script that performs initialization before starting the command. If you have a base image with such a helper script, it's best to preserve that logic by setting
DefaultArgs. This causes the .NET application to be started using an image command instead of the image entrypoint. If the base image has an entrypoint has an entrypoint which isn't a helper script, you probably want to override it by setting
Entrypoint so the .NET application starts.
The SDK container tooling allows to fully customize both the entrypoint and the command through the
ContainerDefaultArgs item groups. If you want to take full control you can set
None. This stops the SDK from adding an instruction to start the .NET application.
The .NET 8 built-in support for building container images provides a convenient way to containerize .NET applications without going through the hassle of writing Dockerfiles. In this article, we've covered the ins and outs of the tooling. It's a good time to adopt this feature thanks to all the improvements made as part of the .NET 8 release. To learn more, you can find the official documentation at learn.microsoft.com.