Skip to main content
Redhat Developers  Logo
  • Products

    Featured

    • Red Hat Enterprise Linux
      Red Hat Enterprise Linux Icon
    • Red Hat OpenShift AI
      Red Hat OpenShift AI
    • Red Hat Enterprise Linux AI
      Linux icon inside of a brain
    • Image mode for Red Hat Enterprise Linux
      RHEL image mode
    • Red Hat OpenShift
      Openshift icon
    • Red Hat Ansible Automation Platform
      Ansible icon
    • Red Hat Developer Hub
      Developer Hub
    • View All Red Hat Products
    • Linux

      • Red Hat Enterprise Linux
      • Image mode for Red Hat Enterprise Linux
      • Red Hat Universal Base Images (UBI)
    • Java runtimes & frameworks

      • JBoss Enterprise Application Platform
      • Red Hat build of OpenJDK
    • Kubernetes

      • Red Hat OpenShift
      • Microsoft Azure Red Hat OpenShift
      • Red Hat OpenShift Virtualization
      • Red Hat OpenShift Lightspeed
    • Integration & App Connectivity

      • Red Hat Build of Apache Camel
      • Red Hat Service Interconnect
      • Red Hat Connectivity Link
    • AI/ML

      • Red Hat OpenShift AI
      • Red Hat Enterprise Linux AI
    • Automation

      • Red Hat Ansible Automation Platform
      • Red Hat Ansible Lightspeed
    • Developer tools

      • Red Hat Trusted Software Supply Chain
      • Podman Desktop
      • Red Hat OpenShift Dev Spaces
    • Developer Sandbox

      Developer Sandbox
      Try Red Hat products and technologies without setup or configuration fees for 30 days with this shared Openshift and Kubernetes cluster.
    • Try at no cost
  • Technologies

    Featured

    • AI/ML
      AI/ML Icon
    • Linux
      Linux Icon
    • Kubernetes
      Cloud icon
    • Automation
      Automation Icon showing arrows moving in a circle around a gear
    • View All Technologies
    • Programming Languages & Frameworks

      • Java
      • Python
      • JavaScript
    • System Design & Architecture

      • Red Hat architecture and design patterns
      • Microservices
      • Event-Driven Architecture
      • Databases
    • Developer Productivity

      • Developer productivity
      • Developer Tools
      • GitOps
    • Secure Development & Architectures

      • Security
      • Secure coding
    • Platform Engineering

      • DevOps
      • DevSecOps
      • Ansible automation for applications and services
    • Automated Data Processing

      • AI/ML
      • Data Science
      • Apache Kafka on Kubernetes
      • View All Technologies
    • Start exploring in the Developer Sandbox for free

      sandbox graphic
      Try Red Hat's products and technologies without setup or configuration.
    • Try at no cost
  • Learn

    Featured

    • Kubernetes & Cloud Native
      Openshift icon
    • Linux
      Rhel icon
    • Automation
      Ansible cloud icon
    • Java
      Java icon
    • AI/ML
      AI/ML Icon
    • View All Learning Resources

    E-Books

    • GitOps Cookbook
    • Podman in Action
    • Kubernetes Operators
    • The Path to GitOps
    • View All E-books

    Cheat Sheets

    • Linux Commands
    • Bash Commands
    • Git
    • systemd Commands
    • View All Cheat Sheets

    Documentation

    • API Catalog
    • Product Documentation
    • Legacy Documentation
    • Red Hat Learning

      Learning image
      Boost your technical skills to expert-level with the help of interactive lessons offered by various Red Hat Learning programs.
    • Explore Red Hat Learning
  • Developer Sandbox

    Developer Sandbox

    • Access Red Hat’s products and technologies without setup or configuration, and start developing quicker than ever before with our new, no-cost sandbox environments.
    • Explore Developer Sandbox

    Featured Developer Sandbox activities

    • Get started with your Developer Sandbox
    • OpenShift virtualization and application modernization using the Developer Sandbox
    • Explore all Developer Sandbox activities

    Ready to start developing apps?

    • Try at no cost
  • Blog
  • Events
  • Videos

Containerize .NET applications with .NET 8

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

Share:

    In this article, we'll take a deep dive into the container 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 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 PublishProfile to DefaultContainer:

    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
    

    The 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:

    <ContainerFamily>alpine</ContainerFamily>
    

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

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

    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_UNAME and 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 quay.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 is latest. To use another, you can either set ContainerImageTag or 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:

    <RuntimeIdentifier>linux-arm64</RuntimeIdentifier>
    

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

    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 UseAppHost to 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.

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

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

    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 PublishContainer target.

    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.

    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 setting ContainerGenerateLabels to 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.

    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 through source-link, which is included with the SDK (since .NET 8). To add these labels, PublishRepositoryUrl must be set to true.

    Environment variables

    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>
    

    Ports

    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 ASPNETCORE_URLS, ASPNETCORE_HTTP_PORTS and ASPNETCORE_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, and c:\app for Windows containers. The default can be overwritten by setting the ContainerWorkingDirectory 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 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 ContainerUser property.

    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.

    For example:

    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 ContainerAppCommandInstruction to 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 ContainerAppCommandInstruction to Entrypoint so the .NET application starts.

    The SDK container tooling allows to fully customize both the entrypoint and the command through the ContainerEntrypoint and ContainerDefaultArgs item groups. If you want to take full control you can set ContainerAppCommandInstruction to None. 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 at learn.microsoft.com.

    Last updated: June 11, 2025

    Related Posts

    • Using OpenAPI with .NET Core

    • .NET 7 now available for RHEL and OpenShift

    • Set up continuous integration for .NET Core with OpenShift Pipelines

    • Tracing .NET Core applications

    • Containerize .NET for Red Hat OpenShift: Linux containers and .NET Core

    • Three ways to containerize .NET applications on Red Hat OpenShift

    Recent Posts

    • GuideLLM: Evaluate LLM deployments for real-world inference

    • Unleashing multimodal magic with RamaLama

    • Integrate Red Hat AI Inference Server & LangChain in agentic workflows

    • Streamline multi-cloud operations with Ansible and ServiceNow

    • Automate dynamic application security testing with RapiDAST

    What’s up next?

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

    Get the e-book
    Red Hat Developers logo LinkedIn YouTube Twitter Facebook

    Products

    • Red Hat Enterprise Linux
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform

    Build

    • Developer Sandbox
    • Developer Tools
    • Interactive Tutorials
    • API Catalog

    Quicklinks

    • Learning Resources
    • E-books
    • Cheat Sheets
    • Blog
    • Events
    • Newsletter

    Communicate

    • About us
    • Contact sales
    • Find a partner
    • Report a website issue
    • Site Status Dashboard
    • Report a security problem

    RED HAT DEVELOPER

    Build here. Go anywhere.

    We serve the builders. The problem solvers who create careers with code.

    Join us if you’re a developer, software engineer, web designer, front-end designer, UX designer, computer scientist, architect, tester, product manager, project manager or team lead.

    Sign me up

    Red Hat legal and privacy links

    • About Red Hat
    • Jobs
    • Events
    • Locations
    • Contact Red Hat
    • Red Hat Blog
    • Inclusion at Red Hat
    • Cool Stuff Store
    • Red Hat Summit

    Red Hat legal and privacy links

    • Privacy statement
    • Terms of use
    • All policies and guidelines
    • Digital accessibility

    Report a website issue