Containerize .NET applications with .NET 8

October 31, 2023
Related topics:
.NETContainersLinuxKubernetes
Related products:
Red Hat Enterprise Linux

Share:

In this article, we'll take a deep dive into thecontainer tooling of the upcoming.NET 8 release.

.NET container tooling

The SDK container tooling provides an easy way to build container images directly from a .NET project. It wasintroduced 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 forPodman, 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 candownload a release candidate from Microsoft download page.

If you are on Fedora, you can obtain it from theFedora .NET SIG copr repo:

dnf copr enable @dotnet-sig/dotnetdnf 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 webcd web

Now publish it while settingPublishProfile toDefaultContainer:

dotnet publish  /p:PublishProfile=DefaultContainer

That's it!

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

Thepublish 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 usedmcr.microsoft.com/dotnet/aspnet:8.0.0-rc.2 as the base image, and created an application image namedweb: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 theaspnet base image for that version. If the project changed to a self-contained application, the SDK would use theruntime-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 theContainerRepository property in the project file.

Selecting the base image

Microsoft's default images areDebian based. Microsoft publishes additional images based on other distros as you can see from the tag names on theMicrosoft runtime repository. The SDK allows you to choose one of the other distros by setting theContainerFamily property. This value will be added as a suffix to the base image tag. For example, to useAlpine images you can set:

<ContainerFamily>alpine</ContainerFamily>

The SDK isn't limited to using Microsoft images. You can choose a base image using theContainerBaseImage property. The following example changes theTargetFramework to .NET 7 and uses the correspondingRed 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).

<TargetFramework>net7.0</TargetFramework><ContainerBaseImage>registry.access.redhat.com/ubi8/dotnet-70-runtime:latest</ContainerBaseImage>

If you run thepublish 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 theSDK_CONTAINER_REGISTRY_UNAME andSDK_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 toquay.io/tmds/web:latest.

dotnet publish /p:PublishProfile=DefaultContainer /p:ContainerRegistry=quay.io /p:ContainerRepository=tmds/web /p:ContainerImageTag=latest

The default tag for the application image islatest. To use another, you can either setContainerImageTag orContainerImageTags. 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 (likemcr.microsoft.com/dotnet/runtime:8.0) support x64, and arm64 on Windows andLinux 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 theRuntimeIdentifier 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:

<RuntimeIdentifier>linux-arm64</RuntimeIdentifier>

If you want to set the runtime identifier so it only affects the container image, you can setContainerRuntimeIdentifier instead of setting theRuntimeIdentifier.

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 useContainerRuntimeIdentifier we don't need to setUseAppHost tofalse. 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.

<ContainerBaseImage>registry.access.redhat.com/ubi8/dotnet-70-runtime:latest</ContainerBaseImage><ContainerRuntimeIdentifier>linux-ppc64le</ContainerRuntimeIdentifier>

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 useRuntimeIdentifier (as we've done in the previous section) to set the-r argument, and the--sc argument can be set by adding a propertySelfContained and setting it totrue.

When you run the command, you'll see the SDK has changed from using theaspnet base image to theruntime-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 identifierlinux-x64 we need to uselinux-musl-x64. The binaries forlinux-musl-x64 are compatible with Linux distributions that use themusl C library instead of theglibc C library. The SDK does not (yet) pick a proper base image tag for this rid so we also need to setContainerFamily.

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 theMicrosoft.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 thePublishContainer target.

dotnet add package Microsoft.NET.Build.Containersdotnet 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.

Labels

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 settingContainerGenerateLabels tofalse. 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 tofalse). Some values are initialized from well-known properties, and can be overridden through container-specific properties.

labelvaluedisable through
org.opencontainers.image.created
org.opencontainers.artifact.created
NowContainerGenerateLabelsImageCreated
org.opencontainers.artifact.description
org.opencontainers.image.description
ContainerDescription/DescriptionContainerGenerateLabelsImageDescription
org.opencontainers.image.authorsContainerAuthors/AuthorsContainerGenerateLabelsImageAuthors
org.opencontainers.image.urlContainerInformationUrl/PackageProjectUrlContainerGenerateLabelsImageUrl
org.opencontainers.image.documentationContainerDocumentationUrl/PackageProjectUrlContainerGenerateLabelsImageDocumentation
org.opencontainers.image.versionContainerVersion/PackageVersionContainerGenerateLabelsImageVersion
org.opencontainers.image.vendorContainerVendorContainerGenerateLabelsImageVendor
org.opencontainers.image.licensesContainerLicenseExpression/PackageLicenseExpressionContainerGenerateLabelsImageLicenses
org.opencontainers.image.titleContainerTitle/TitleContainerGenerateLabelsImageTitle
org.opencontainers.image.base.nameContainerBaseImageContainerGenerateLabelsImageBaseName
org.opencontainers.image.sourcePrivateRepositoryUrlContainerGenerateLabelsImageSource
org.opencontainers.image.revisionSourceRevisionIdContainerGenerateLabelsImageRevision

The source repository and revision information are provided throughsource-link, which is included with the SDK (since .NET 8). To add these labels,PublishRepositoryUrl must be set totrue.

Environment variables

Environment variables can be set in the container image throughContainerEnvironmentVariable as shown in the following example.

<ItemGroup>  <ContainerEnvironmentVariable Include="ASPNETCORE_ENVIRONMENT" Value="Production" /></ItemGroup>

Ports

Container images describe the ports they expose. Note that these ports are image metadata, and they donot limit external access to specific ports on a container image. Ports can be added throughContainerPort 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 variablesASPNETCORE_URLS,ASPNETCORE_HTTP_PORTS andASPNETCORE_HTTPS_PORTS.

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.

Working directory

When the container image runs, the working directory is/app for Linux containers, andc:\app for Windows containers. The default can be overwritten by setting theContainerWorkingDirectory property.

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).

Container user

Since .NET 8, Microsoft .NET Linux container imagesinclude a non-root user namedapp. Like with previous .NET versions, the base image runs as theroot user. If you build an application image through aDockerfile, you can choose to make it run under theapp user by adding anUSER $APP_UID instruction. The$APP_UID used here is an environment variable set by the base image. It contains theapp 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 theAPP_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 theappsettings.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 theContainerUser property.

When a .NET image runs on theRed 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 meansroot), a group id of 0 has no privileges attached to it. To accommodate working on OpenShift, the Red Hat base image set theHOME 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.

For example:

podman run myimagepodman 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 onKubernetes/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 settingContainerAppCommandInstruction toDefaultArgs. 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 settingContainerAppCommandInstruction toEntrypoint so the .NET application starts.

The SDK container tooling allows to fully customize both the entrypoint and the command through theContainerEntrypoint andContainerDefaultArgs item groups. If you want to take full control you can setContainerAppCommandInstruction toNone. This stops the SDK from adding an instruction to start the .NET application.

Conclusion

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 atlearn.microsoft.com.

Last updated: June 11, 2025

What’s up next?

DownloadOpenShift for .NET Developers and learn techniques for building, testing, and debugging .NET applications within the Red Hat OpenShift environment

Get the e-book