dotnet-docker Documentation

Official container images for .NET

4,876 stars Dockerfile Dockerfile 39 files · ~49,376 tokens
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy dotnet-docker on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name dotnet-docker --restart always dotnet/dotnet-docker:latest

Technical Documentation: dotnet/dotnet-docker

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (`README.md`)

.NET

About

.NET is a general purpose development platform maintained by Microsoft and the .NET community on GitHub. It is cross-platform, supports Windows, macOS, and Linux, and can be used in device, cloud, and embedded/IoT scenarios.

.NET has several capabilities that make development productive, including automatic memory management, (runtime) generic types, reflection, asynchronous constructs, concurrency, and native interop. Millions of developers take advantage of these capabilities to efficiently build high-quality applications.

You can use C# or F# to write .NET apps.

  • C# is powerful, type-safe, and object-oriented while retaining the expressiveness and elegance of C-style languages. Anyone familiar with C and similar languages will find it straightforward to write in C#.
  • F# is a cross-platform, open-source, functional programming language for .NET. It also includes object-oriented and imperative programming.

.NET is open source (MIT and Apache 2 licenses) and was contributed to the .NET Foundation by Microsoft in 2014. It can be freely adopted by individuals and companies, including for personal, academic or commercial purposes. Multiple companies use .NET as part of apps, tools, new platforms and hosting services.

You are invited to contribute new features, fixes, or updates, large or small; we are always thrilled to receive pull requests, and do our best to process them as fast as we can.

.NET Documentation

Watch discussions for Docker-related .NET announcements.

Usage

The .NET Docker samples show various ways to use .NET and Docker together. See Introduction to .NET and Docker to learn more.

Container sample: Run a simple application

You can quickly run a container with a pre-built .NET Docker image, based on the .NET console sample.

Type the following command to run a sample console application:

console
docker run --rm mcr.microsoft.com/dotnet/samples

Container sample: Run a web application

You can quickly run a container with a pre-built .NET Docker image, based on the ASP.NET Core sample.

Type the following command to run a sample web application:

console
docker run -it --rm -p 8000:8080 --name aspnetcore_sample mcr.microsoft.com/dotnet/samples:aspnetapp

After the application starts, navigate to http://localhost:8000 in your web browser. You can also view the ASP.NET Core site running in the container from another machine with a local IP address such as http://192.168.1.18:8000.

Note: ASP.NET Core apps (in official images) listen to port 8080 by default, starting with .NET 8. The -p argument in these examples maps host port 8000 to container port 8080 (host:container mapping). The container will not be accessible without this mapping. ASP.NET Core can be configured to listen on a different or additional port.

See Hosting ASP.NET Core Images with Docker over HTTPS to use HTTPS with this image.

Image Variants

.NET container images have several variants that offer different combinations of flexibility and deployment size.
The Image Variants documentation contains a summary of the image variants and their use-cases.

Distroless images

.NET distroless container images contain only the minimal set of packages .NET needs, with everything else removed.
Due to their limited set of packages, distroless containers have a minimized security attack surface, smaller deployment sizes, and faster start-up time compared to their non-distroless counterparts.
They contain the following features:

  • Minimal set of packages required for .NET applications
  • Non-root user by default
  • No package manager
  • No shell

.NET offers distroless images for Azure Linux and Ubuntu (Chiseled).

.NET:

.NET Framework:

Support

Lifecycle

Image Update Policy

  • Base Image Updates: Images are re-built within 12 hours of any updates to their base images (e.g. debian:bookworm-slim, windows/nanoserver:ltsc2022, etc.).
  • .NET Releases: Images are re-built as part of releasing new .NET versions. This includes new major versions, minor versions, and servicing releases.
  • Critical CVEs: Images are re-built to pick up critical CVE fixes as described by the CVE Update Policy below.
  • Monthly Re-builds: Images are re-built monthly, typically on the second Tuesday of the month, in order to pick up lower-severity CVE fixes.
  • Out-Of-Band Updates: Images can sometimes be re-built when out-of-band updates are necessary to address critical issues. If this happens, new fixed version tags will be updated according to the Fixed version tags documentation.

CVE Update Policy

.NET container images are regularly monitored for the presence of CVEs. A given image will be rebuilt to pick up fixes for a CVE when:

  • We detect the image contains a CVE with a CVSS score of "Critical"
  • AND the CVE is in a package that is added in our Dockerfile layers (meaning the CVE is in a package we explicitly install or any transitive dependencies of those packages)
  • AND there is a CVE fix for the package available in the affected base image's package repository.

Please refer to the Security Policy and Container Vulnerability Workflow for more detail about what to do when a CVE is encountered in a .NET image.

Feedback

License


Overview (`samples/README.md`)

.NET container samples

The following samples and guidance demonstrate how to use .NET and Docker for development, testing and production. You can use the samples for learning about containers or as the basis of your own container images.

Kubernetes samples are provided in the kubernetes directory.

[!IMPORTANT]

.NET 10 containers breaking changes

.NET 9 containers breaking changes

.NET 8 containers breaking changes

Building images

Development guidance

Hosting guidance

Other documentation

Try pre-built images

The following commands will run a .NET console app in a container:

console
docker run --rm mcr.microsoft.com/dotnet/samples

The following command will run an ASP.NET Core console app in a container that you can access in your web browser at http://localhost:8000.

console
docker run --rm -it -p 8000:8080 mcr.microsoft.com/dotnet/samples:aspnetapp

Docker Repositories


Overview (`documentation/scenarios/README.md`)

.NET with Docker Scenarios

These scenarios contain tips and tricks, best practices, and samples of commonly used technology. They provide helpful demonstrations of solving commonly encountered issues.

Scenarios

Community

You are invited to contribute new scenarios, large or small; we are always thrilled to receive pull requests, and do our best to process them as fast as we can.

Before you start to code, please read the .NET contribution guidelines.


Overview (`eng/docker-tools/readme.md`)

Don't touch this folder

text
uuuuuuuuuuuuuuuuuuuu
          u" uuuuuuuuuuuuuuuuuu "u
        u" u$$$$$$$$$$$$$$$$$$$$u "u
      u" u$$$$$$$$$$$$$$$$$$$$$$$$u "u
    u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u
  u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u
u" u$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$u "u
$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $
$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $
$ $$$" ... "$...  ...$" ... "$$$  ... "$$$ $
$ $$$u `"$$$$$$$  $$$  $$$$$  $$  $$$  $$$ $
$ $$$$$$uu "$$$$  $$$  $$$$$  $$  """ u$$$ $
$ $$$""$$$  $$$$  $$$u "$$$" u$$  $$$$$$$$ $
$ $$$$....,$$$$$..$$$$$....,$$$$..$$$$$$$$ $
$ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ $
"u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u"
  "u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u"
    "u "$$$$$$$$$$$$$$$$$$$$$$$$$$$$" u"
      "u "$$$$$$$$$$$$$$$$$$$$$$$$" u"
        "u "$$$$$$$$$$$$$$$$$$$$" u"
          "u """""""""""""""""" u"
            """"""""""""""""""""

!!! Changes made in this directory are subject to being overwritten by automation !!!

The files in this directory are shared by all .NET Docker repos. If you need to make changes to these files, open an issue or submit a pull request in https://github.com/dotnet/docker-tools.

For guidance on using this infrastructure, see the Developer Guide.


Overview (`eng/pipelines/README.md`)

.NET Docker Pipelines

The contents of the pipelines folder are used by the .NET Core engineering infrastructure to build and publish the images.


Overview (`eng/shared/README.md`)

Microsoft.DotNet.Docker.Shared

This class library contains code that's shared between the
update-dependencies tool and the
.NET Docker test project.


Overview (`eng/update-dependencies/README.md`)

update-dependencies

This folder contains the update-dependencies tool that automates updating
component versions for images built from this repo. It is used to:

  • Update versions recorded in manifest.versions.json.
  • Regenerate Dockerfiles and READMEs from their templates (via the build
    scripts) using those updated versions.
  • Create / update a pull request with the changes.

Implementation

The tool has two layers of implementation code:

  1. Modern CLI built with System.CommandLine and dependency injection.
    These commands retrieve version information from official .NET build assets
    (BAR builds/channels, staging pipeline artifacts) and then map that
    information into a standardized set of product versions.
  2. Legacy code built on Microsoft.DotNet.VersionTools.Automation that
    performs regex replacement of versions, checksums, and URLs in
    manifest.versions.json. This logic is
    encapsulated behind SpecificCommand and related helper classes
    (e.g. VersionUpdater, DockerfileShaUpdater).

Currently, the modern CLI front-end of the tool resolves product versions and
hands them over to the SpecificCommand class to perform file updates. New
features should only be added to the modern CLI front-end. SpecificCommand
will be removed/re-implemented eventually since it is tightly coupled to an
unsupported package dependency (Microsoft.DotNet.VersionTools.Automation -
removal tracked by this GitHub issue).

Adding support for new dependencies

.NET dependencies

To add support for new .NET repos to the from-build and from-channel
commands:

  • Add the new repo to the BuildRepo enum.
  • Update the GetBuildRepo() extension method to support
    the new enum value.
  • Implement a new IBuildUpdaterService, using
    existing implementations as a reference.
  • Register a new keyed singleton in Program.cs using the new
    repo enum value as the key.

If the dependency can be resolved from a pipeline artifact, GitHub release,
static file, or something similar, then you may be able to treat it like an
external dependency.

External projects

  1. Add your component's version to
    manifest.versions.json with the format:
    |<dockerfile-version>|build-version. For example:
    chisel|10.0|build-version or mingit|latest|build-version. This version
    can be used directly in Dockerfile templates or can be used to compose other
    variables in the versions file.
  2. Create a new class that implements
    IDependencyVersionSource. You can use
    whatever method you need to fetch version updates. Example implementation:
    ChiselVersionSource (determines version using
    GitHub Releases).
  3. Register your IDependencyVersionSource implementation in
    Program.cs using the same product key from step 1.
  4. You can now update your product's version by running the following command:
    dotnet run --project ./eng/update-dependencies/update-dependencies.csproj -- from-component <dockerfile-version>
  5. (Optional) Add a job to the
    update-dependencies pipeline
    to enable automatic update pull requests.

Overview (`samples/AspNetCoreNativeAot/README.md`)

Release Json Report API

This app demonstrates publishing an app as native AOT in containers.

A similar console app sample supports single file deployment (non-AOT scenario). This app could also be deployed that way.

Build image

You can build and run the sample:

console
docker build --pull -t releasesapi 'https://github.com/dotnet/dotnet-docker.git#:samples/AspNetCoreNativeAot'
docker run --rm -it -p 8000:8080 -e ASPNETCORE_HTTP_PORTS=8080 releasesapi

It exposes two endpoints:

  • http://localhost:8000/releases
  • http://localhost:8000/healthz

App

The app is intended as a sort of compliance report for .NET. The report includes supported major releases and those recently out of support. It includes the latest and latest security patch versions for each of those major releases.

This same information is available from the release JSON files that the team maintains, but that requires a bit of code to provide the same report.

Dockerfiles

The sample includes several Dockerfiles with varying functionality. The base images they use are currently experimental (not supported).

These Dockerfiles work on AMD64 and ARM64 when targeting the same architecture.

For cross-compilation support, you will need to install a few extra packages during the build.

Cross-compilation

The following Dockerfiles demonstrate how to add cross-compilation support for native AOT .NET Dockerfiles.
This means you can build ARM64 images using an AMD64 machine and vice-versa.

For example, to build an Ubuntu ARM64 native AOT .NET image on an AMD64 machine, you can run the following command:

console
docker build --pull --platform linux/arm64 -t releasesapi -f Dockerfile.ubuntu-cross-x64-arm64 'https://github.com/dotnet/dotnet-docker.git#:samples/AspNetCoreNativeAot'

Additional native AOT cross-compilation options are described in the dotnet/runtime repo.

Note that non-AOT .NET images don't need any additional packages for cross-compilation. See Building images for a specific platform for more information.


Overview (`samples/AspNetCoreRazorApp/README.md`)

ASP.NET Core Docker Sample

This sample demonstrates how to build container images for ASP.NET Core web apps. It is based on the dotnet new webapp template. See .NET Docker Samples for more samples.

Run the sample image

You can start by launching a sample from our container registry and accessing it in your web browser at http://localhost:8000.

console
docker run --rm -it -p 8000:8080 -e ASPNETCORE_HTTP_PORTS=8080 mcr.microsoft.com/dotnet/samples:aspnetapp

You should see the following console output as the application starts:

console
> docker run --rm -it -p 8000:8080 -e ASPNETCORE_HTTP_PORTS=8080 aspnetapp
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://[::]:8080
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.

After the application starts, navigate to http://localhost:8000 in your web browser.
You can also reach the app's API endpoint from the command line:

bash
$ curl http://localhost:8000/Environment
{
  "runtimeVersion": ".NET 11.0.0-rc.1",
  "osVersion": "Ubuntu 26.04 LTS",
  "osArchitecture": "X64",
  "user": "app",
  "processorCount": 32,
  "totalAvailableMemoryBytes": 67396280320,
  "memoryLimit": 0,
  "memoryUsage": 62160896,
  "hostName": "d021ffa2e15f"
}

You can see the app running via docker ps:

bash
$ docker ps
CONTAINER ID   IMAGE                                        COMMAND         CREATED          STATUS                    PORTS                  NAMES
CONTAINER ID   IMAGE       COMMAND         CREATED         STATUS         PORTS                                         NAMES
d021ffa2e15f   aspnetapp   "./AspNetCoreRazorApp"   2 minutes ago   Up 2 minutes   0.0.0.0:8000->8080/tcp, [::]:8000->8080/tcp   reverent_aryabhata

Change port

ASP.NET Core apps (in official .NET images) listen to port 8080 by default. The -p argument in these examples maps host port 8000 to container port 8080 (host:container mapping). The web server hosted by the container will not be accessible without this mapping.

ASP.NET Core can be configured to listen on a different or additional port.
For example, setting either of the following evnironment variables will change the container port to 80:

  • ASPNETCORE_HTTP_PORTS=80
  • ASPNETCORE_URLS=http://+:80

[!NOTE]
Ports 1 through 1023 are restricted to root users only, and will not work when running as the non-root root user provided in .NET images.

ASPNETCORE_URLS overrides ASPNETCORE_HTTP_PORTS if set.
The ASPNETCORE_HTTP_PORTS envrionment variable is used in the ASP.NET Core
images to set the default port.

Enable HTTPS

To host the sample image with HTTPS, follow the instructions for Running pre-built Container Images with HTTPS.
For a more in-depth guide to developing ASP.NET Core apps with HTTPS, check out Developing ASP.NET Core Applications with Docker over HTTPS.

Build the sample image

You can build the sample image using the following command (cloninig the repo isn't necessary):

console
docker build --pull -t aspnetapp 'https://github.com/dotnet/dotnet-docker.git#:samples/AspNetCoreRazorApp'

If you have cloned the repo, you can build the image using your local copy:

console
cd samples/AspNetCoreRazorApp
docker build --pull -t aspnetapp .

Add the argument -f <Dockerfile> to build the sample in a different configuration.
For example, build an Ubuntu Chiseled image using Dockerfile.chiseled:

console
docker build --pull -t dotnetapp -f Dockerfile.chiseled 'https://github.com/dotnet/dotnet-docker.git#:samples/ConsoleApp'

You can run your local image the same way as described in Run the sample image:

console
docker run --rm -it -p 8000:8080 -e ASPNETCORE_HTTP_PORTS=8080 aspnetapp

Multi-platform build

.NET sample Dockerfiles support multi-platform builds via cross-compilation.
First, check out the Docker multi-platform build prerequisites

Once you have the prerequisites set up, you can build the ASP.NET app sample for a specific architecture:

console
# From an amd64 machine:
docker buildx build --platform linux/arm64 -t aspnetapp .

# From an arm64 machine:
docker buildx build --platform linux/amd64 -t aspnetapp .

You can also build both platforms at once:

console
docker buildx build --platform linux/amd64,linux/arm64 -t aspnetapp .

This works thanks to .NET's support for cross-compilation.
The build runs on your build machine's architecture and outputs IL for the target architecture.
The app is then copied to the final stage without running any commands on the target image - there's no emulation involved.

[!NOTE]
.NET does not support running under QEMU emulation. See .NET and QEMU for more information.

Build image with the SDK

The easiest way to build .NET images is using the SDK.

console
cd samples/AspNetCoreRazorApp
dotnet publish -p PublishProfile=DefaultContainer

You can control many aspects of the generated container through MSBuild properties.
For example, the following command uses a different base image and publishes the final image to DockerHub:

console
dotnet publish \
    -p PublishProfile=DefaultContainer \
    -p ContainerBaseImage=mcr.microsoft.com/dotnet/aspnet:11.0-resolute-chiseled \
    -p ContainerRegistry=docker.io \
    -p ContainerRepository=youraccount/aspnetapp

These properties can also be specified in your project file.
For a full list of supported properties, see the .NET SDK publishing reference.

Supported Linux distros

The .NET Team publishes images for multiple distros.

Sample Dockerfiles are provided for:

Supported Windows versions

The .NET Team publishes images for multiple Windows versions. You must have Windows containers enabled to use these images.

Samples are provided for

You can pull a pre-built Windows sample image using the following tag:

  • mcr.microsoft.com/dotnet/samples:aspnetapp-nanoserver-ltsc2025

Overview (`samples/ConsoleApp/README.md`)

.NET Docker Sample

This sample demonstrates how to build container images for .NET console apps. See .NET Docker Samples for more samples.

Run the sample image

You can start by launching a sample from our container registry.

console
docker run --rm mcr.microsoft.com/dotnet/samples:dotnetapp-chiseled

Build the image

You can build and run an image using the following instructions (cloninig the repo isn't necessary):

console
docker build --pull -t dotnetapp 'https://github.com/dotnet/dotnet-docker.git#:samples/ConsoleApp'
docker run --rm dotnetapp

Add the argument -f <Dockerfile> to build the sample in a different configuration.
For example, build an Ubuntu Chiseled image using Dockerfile.chiseled:

console
docker build --pull -t dotnetapp -f Dockerfile.chiseled 'https://github.com/dotnet/dotnet-docker.git#:samples/ConsoleApp'

Supported Linux distros

The .NET Team publishes images for multiple distros.

Sample Dockerfiles are provided for:

Supported Windows versions

The .NET Team publishes images for multiple Windows versions. You must have Windows containers enabled to use these images.

Samples are provided for

You can pull a pre-built Windows sample image using the following tag:

  • mcr.microsoft.com/dotnet/samples:dotnetapp-nanoserver-ltsc2025

Overview (`samples/ConsoleAppSelfContained/README.md`)

.NET Release Report App

This app demonstrates publishing an app published with single file deployment in containers.

A similar web API sample supports native AOT deployment. This app could also be deployed that way.

Usage

You can build and run the sample:

bash
docker build --pull -t releasesapp .
docker run --rm releasesapp

It will produce output similar to this:

Command output

text
/* Detailed source-code truncated for AI context efficiency. */

App

The app is intended as a sort of compliance report for .NET. The report includes supported major releases and those recently out of support. It includes the latest and latest security patch versions for each of those major releases.

This same information is available from the release JSON files that the team maintains, but that requires a bit of code to provide the same report.

Dockerfiles

The sample includes support for the following distributions:


Overview (`samples/ConsoleAppWithGlobalization/README.md`)

Globalization Test App

This app is intended for testing .NET APIs that use globalization APIs and concepts. The behavior of these APIs varies depending on how a container (or other environment) is configured. The Dockerfile is intended to aid testing and isn't itself an example of a production Dockerfile.

See .NET Docker Samples for more samples.

Configuration

The behavior of these APIs is affected by:

  • Presence of icu and tzdata packages.
  • The value of globalization invariant mode.
  • The value (or absence) of the TZ environment variable.
  • The value (or absence) of /etc/timezone

The recommended way to configure tzdata and timezones is to set the container timezone by using the TZ environment variables, as is demonstrated below.

console
$ docker run -it --rm -e TZ=$(cat /etc/timezone) mcr.microsoft.com/dotnet/runtime-deps:11.0

root@4770a50f643e# date
Wed Sep 10 17:27:24 UTC 2025

root@4770a50f643e# TZ="Etc/UTC" date
Wed Sep 10 17:27:42 UTC 2025

root@4770a50f643e# TZ="America/New_York" date
Wed Sep 10 13:28:26 EDT 2025

root@4770a50f643e# TZ="America/Los_Angeles" date
Wed Sep 10 10:29:07 PDT 2025

root@4771a50f643e# exit

$ docker run --rm -e TZ=$(cat /etc/timezone) mcr.microsoft.com/dotnet/runtime-deps:11.0 date
Wed Sep 10 10:29:07 PDT 2025

The first approach uses the default timezone, which is UTC. The other examples pass specific timezones, including UTC. The last pattern passes the timezone of the host.

A machine configured to UTC will produce the following:

console
$ cat /etc/timezone
Etc/UTC

Running the app

The app produces the following output, for the "America/Los_Angeles" timezone:

console
$ docker build --pull -t globalapp .
$ docker run --rm -it -e TZ="America/Los_Angeles" globalapp
Hello, World!

****Print baseline timezones**
Utc: (UTC) Coordinated Universal Time; 09/10/2025 17:35:50
Local: (UTC-08:00) Pacific Time (Los Angeles); 09/10/2025 10:35:50

****Print specific timezone**
Home timezone: America/Los_Angeles
DateTime at home: 09/10/2025 10:35:50

****Culture-specific dates**
Current: 09/10/2025
English (United States) -- en-US:
9/10/2025 5:35:50 PM
9/10/2025
5:35 PM
English (Canada) -- en-CA:
2025-09-10 5:35:50 p.m.
2025-09-10
5:35 p.m.
French (Canada) -- fr-CA:
2025-09-10 17 h 35 min 50 s
2025-09-10
17 h 35
Croatian (Croatia) -- hr-HR:
10. 09. 2025. 17:35:50
10. 09. 2025.
17:35
jp (Japan) -- jp-JP:
9/10/2025 17:35:50
9/10/2025
17:35
Korean (South Korea) -- ko-KR:
2025. 9. 10. 오후 5:35:50
2025. 9. 10.
오후 5:35
Portuguese (Brazil) -- pt-BR:
10/09/2025 17:35:50
10/09/2025
17:35
Chinese (China) -- zh-CN:
2025/9/10 17:35:50
2025/9/10
17:35

****Culture-specific currency:**
Current: ¤1,337.00
en-US: $1,337.00
en-CA: $1,337.00
fr-CA: 1 337,00 $
hr-HR: 1.337,00 €
jp-JP: ¥ 1337
ko-KR: ₩1,337
pt-BR: R$ 1.337,00
zh-CN: ¥1,337.00

****Japanese calendar**
08/18/2019
01/08/18
平成元年8月18日
平成元年8月18日

****String comparison**
Comparison results: `0` mean equal, `-1` is less than and `1` is greater
Test: compare i to (Turkish) İ; first test should be equal and second not
0
-1
Test: compare Å Å; should be equal
0

Overview (`samples/MultiProjectApp/README.md`)

Building and testing multiple projects with Docker

It is easy to build and test multiple projects with Docker by following some basic patterns. Docker comes with some requirements and some useful mechanisms that can help you manage various container-based workflows.

The MultiProjectApp sample is intended to act as a very simple "complex application". It is composed of multiple projects, including a test project. It is used to demonstrate various workflows.

Simpler workflows are provided at .NET Docker samples.

The instructions assume that you have cloned this repo, have Docker installed, and have a command prompt open within the samples/MultiProjectApp directory within the repo.

Building an image including multiple projects

The most common way to build images is using following pattern:

console
docker build -t tag .

The most important aspect of that docker command is the . at the end. It represents the path to the build context, where Docker will look for assets that are referenced in the Dockerfile and for a Dockerfile (if not explicitly specified with the -f option). The build context is packaged up and then sent to the Docker daemon (the server), which performs the image build. It is required that all referenced assets are available within the build context, otherwise, they will not be available while the image is being built, which may produce the wrong image or produce an error. You can inspect intermediate or final images to validate that they contain the correct content if you see results you don't expect.

In the case of an application with multiple project dependencies, it may be intuitive to place the Dockerfile beside the project file for the application project, and to assign the build context to that same location. This will not work because project dependencies will not exist within the build context.

Instead, in a multi-project solution, the best pattern is to place the Dockerfile at the same location you would place a solution file, which is typically one directory above the application project. At that location, you can use the pattern demonstrated above, using . for the build context at the same location as the Dockerfile, and enabling all resources to be naturally located within that context.

This is the approach used with MultiProjectApp. The Dockerfile for the sample is at the same location as the .slnx file, and all projects are available when that same location is used as the build context. There are other options, but this approach is the easiest.

You can build and run MultiProjectApp using the following commands:

console
docker build --pull -t complexapp .
docker run --rm complexapp

It will restore and build all required projects and produce an application image.

One or more of the projects in your solution might be test projects. It can be useful to include testing within same container-based workflow as the application. This is helpful for at least two reasons:

  • Integrating testing into this workflow will ensure that tests run in the same environment as the application.
  • Get test feedback to validate images before publishing to a registry and deploying to production.

There are two primary ways to test within the workflow of an application container image:

  • Run dotnet test as a RUN step within the image build.
  • Expose an opt-in ENTRYPOINT as part of a Dockerfile stage.

This is different than running tests within a .NET SDK container, which establishes a generic environment (which also works well). The rest of this document is focused on running tests within the same container environment as the application.

[!NOTE]
See Establishing docker environment for more information on correctly configuring Dockerfiles and docker build commands.

Running tests

There are many ways to run automated tests against your projects.
Running Tests with Docker is one example that covers running this sample's tests using the separate executable test stage in the MultiProjectApp Dockerfile.

More Samples


Overview (`samples/kubernetes/README.md`)

Using .NET with Kubernetes

Kubernetes is an orchestration system for containers. You can host .NET in Kubernetes in the same way as other dev platforms. We've provided samples that demonstrate best practices for configuring .NET apps with Kubernetes. More general container samples are provided in the samples directory.

The samples were tested with:

  • kubectl 1.30
  • minikube 1.33 (Kubernetes 1.30.0)
  • Docker Desktop 26.0.0 (Kubernetes 1.29.2)

The instruction rely on a correctly configured Kubernetes environment.

Run sample

Apply sample to your cluster.

bash
kubectl apply -f https://raw.githubusercontent.com/dotnet/dotnet-docker/main/samples/kubernetes/hello-dotnet/hello-dotnet.yaml
kubectl port-forward service/hello-dotnet 8080

View the sample app at http://localhost:8080/. See hello-dotnet for information.

Examples

The following examples demonstrate various Kubernetes and .NET capabilities.

The hello-dotnet example demonstrates multiple capabilities in a single sample.


Overview (`samples/snippets/README.md`)

Content has Moved

This file has moved to a new location.


Overview (`samples/kubernetes/dotnet-monitor/README.md`)

Monitor your app with `dotnet-monitor` in Kubernetes

dotnet-monitor allows you to gather diagnostic data from running applications using HTTP endpoints. It can be used as a sidecar for your app in your Kubernetes deployment.

Use `dotnet-monitor`

Apply dotnet-monitor.yaml to your cluster.

bash
kubectl apply -f https://raw.githubusercontent.com/dotnet/dotnet-docker/main/samples/kubernetes/dotnet-monitor/dotnet-monitor.yaml

Apply the local file if you've cloned the repo.

bash
kubectl apply -f dotnet-monitor.yaml

Create a proxy to the service, on all three ports.

bash
kubectl port-forward service/dotnet-monitor 8080 52323 52325

View the sample app at http://localhost:8080/ or call curl http://localhost:8080/Environment.

You can query the app.

bash
% curl http://localhost:8080/Environment
{"runtimeVersion":".NET 8.0.5","osVersion":"Alpine Linux v3.19","osArchitecture":"X64","user":"root","processorCount":1,"totalAvailableMemoryBytes":78643200,"memoryLimit":104857600,"memoryUsage":34238464,"hostName":"dotnet-resource-limits-8685bd4b75-jltn5"}

You can query basic information from dotnet-monitor with the following approach.

bash
% curl http://localhost:52323/info
{"version":"8.0.2-servicing.24258.10+25e00f40ba2a55512ea59365e73ddb4b27c73280","runtimeVersion":"8.0.5","diagnosticPortMode":"Listen","diagnosticPortName":"/diag/dotnet-monitor.sock"}
 % curl http://localhost:52323/processes
[{"pid":1,"uid":"6dd610b6-bc0c-4bf1-a882-4fa0afea34d2","name":"AspNetCoreRazorApp","isDefault":true}]
% curl "http://localhost:52323/processes?pid=1"
[{"pid":1,"uid":"6dd610b6-bc0c-4bf1-a882-4fa0afea34d2","name":"AspNetCoreRazorApp","isDefault":true}]

See API documentation for a complete list of endpoints.

The livemetrics endpoint provides access to more information.

bash
% curl "http://localhost:52323/livemetrics?pid=1"
{"timestamp":"2023-04-11T02:05:22.4036612+00:00","provider":"Microsoft.AspNetCore.Hosting","name":"requests-per-second","displayName":"Request Rate","unit":"count","counterType":"Rate","tags":"","value":7}
{"timestamp":"2023-04-11T02:05:22.4037646+00:00","provider":"Microsoft.AspNetCore.Hosting","name":"total-requests","displayName":"Total Requests","unit":"","counterType":"Metric","tags":"","value":27}
{"timestamp":"2023-04-11T02:05:22.4037831+00:00","provider":"Microsoft.AspNetCore.Hosting","name":"current-requests","displayName":"Current Requests","unit":"","counterType":"Metric","tags":"","value":0}
{"timestamp":"2023-04-11T02:05:22.4037996+00:00","provider":"Microsoft.AspNetCore.Hosting","name":"failed-requests","displayName":"Failed Requests","unit":"","counterType":"Metric","tags":"","value":0}

The metrics endpoint exposes the same data in Prometheus exposition format. Here, we're requested it on port 52325.

bash
% curl http://localhost:52325/metrics   
# HELP microsoftaspnetcorehosting_requests_per_second Request Rate
# TYPE microsoftaspnetcorehosting_requests_per_second gauge
microsoftaspnetcorehosting_requests_per_second 0 1681509071290
microsoftaspnetcorehosting_requests_per_second 0 1681509076289
microsoftaspnetcorehosting_requests_per_second 0 1681509081292
# HELP microsoftaspnetcorehosting_total_requests Total Requests
# TYPE microsoftaspnetcorehosting_total_requests gauge
microsoftaspnetcorehosting_total_requests 5 1681509066304
microsoftaspnetcorehosting_total_requests 5 1681509071290
microsoftaspnetcorehosting_total_requests 5 1681509076290
# HELP microsoftaspnetcorehosting_current_requests Current Requests
# TYPE microsoftaspnetcorehosting_current_requests gauge
microsoftaspnetcorehosting_current_requests 0 1681509066304
microsoftaspnetcorehosting_current_requests 0 1681509071290
microsoftaspnetcorehosting_current_requests 0 1681509076290
# HELP microsoftaspnetcorehosting_failed_requests Failed Requests
# TYPE microsoftaspnetcorehosting_failed_requests gauge
microsoftaspnetcorehosting_failed_requests 0 1681509066304
microsoftaspnetcorehosting_failed_requests 0 1681509071290
microsoftaspnetcorehosting_failed_requests 0 1681509076290

Port 52325 is special since it limits access to just the metrics endpoint, making it safe to expose to external systems. The other endpoints -- only accessible on port 52323 -- may provide access to privileged data, like dumps. This is why port 52323 is configured for the loopback interface, making it only accessible to the pod.

Monitor with Prometheus

Prometheus is a popular monitoring solution. dotnet-monitor exports metrics data in Prometheus exposition format via its metrics endpoint. That makes it straightforward to connect the two systems.

Apply prometheus-app.yaml to your cluster.

bash
kubectl apply -f https://raw.githubusercontent.com/dotnet/dotnet-docker/main/samples/kubernetes/dotnet-monitor/prometheus-app.yaml

Apply the local file if you've cloned the repo.

bash
kubectl apply -f prometheus-app.yaml

Create a proxy to the prometheus service.

bash
kubectl port-forward service/prometheus 9090

View the Prometheus site at http://localhost:9090.

It should looks something like the following image.

The "metrics explorer" icon to the left of the "Execute" button provides a list of metrics that have been collected.

Delete resources

Delete the resources (remote URL or local manifest).

bash
kubectl delete -f https://raw.githubusercontent.com/dotnet/dotnet-docker/main/samples/kubernetes/dotnet-monitor/dotnet-monitor.yaml
kubectl delete -f https://raw.githubusercontent.com/dotnet/dotnet-docker/main/samples/kubernetes/dotnet-monitor/prometheus-app.yaml

Overview (`samples/kubernetes/graceful-shutdown/README.md`)

Graceful shutdown

Enable graceful shutdown of an app, particularly with long-running code. This pattern is described in more depth in Graceful shutdown of ASP.NET-based applications.

The following pattern can be used:

csharp
CancellationTokenSource cancellation = new();
app.Lifetime.ApplicationStopping.Register( () =>
{
    cancellation.Cancel();
});

// This API demonstrates how to use task cancellation
// to support graceful container shutdown via SIGTERM.
// The method itself is an example and not useful.
app.MapGet("/Delay/{value}", async (int value) =>
{
    try
    {
        await Task.Delay(value, cancellation.Token);
    }
    catch(TaskCanceledException)
    {
    }
    
    return new DelayValue(value);
});

Test the pattern with a single replica deployment. This test requires multiple terminal windows (with careful timing). The pod names and other data won't look the same.

First terminal:

bash
kubectl apply -f https://raw.githubusercontent.com/dotnet/dotnet-docker/main/samples/kubernetes/hello-dotnet/hello-dotnet.yaml
kubectl port-forward service/hello-dotnet 8080:80

Second terminal:

bash
$ kubectl get -w po
NAME                            READY   STATUS    RESTARTS   AGE
hello-dotnet-6fd9fc7cd8-msj9g   0/1     Pending   0          0s
hello-dotnet-6fd9fc7cd8-msj9g   0/1     Pending   0          0s
hello-dotnet-6fd9fc7cd8-msj9g   0/1     ContainerCreating   0          0s
hello-dotnet-6fd9fc7cd8-msj9g   1/1     Running             0          3s
hello-dotnet-6fd9fc7cd8-msj9g   1/1     Terminating         0          74s
hello-dotnet-6fd9fc7cd8-znmfj   0/1     Pending             0          0s
hello-dotnet-6fd9fc7cd8-znmfj   0/1     Pending             0          0s
hello-dotnet-6fd9fc7cd8-znmfj   0/1     ContainerCreating   0          0s

Third terminal:

bash
$ kubectl get po
NAME                            READY   STATUS    RESTARTS   AGE
hello-dotnet-6fd9fc7cd8-msj9g   1/1     Running   0          88s
$ kubectl logs -f hello-dotnet-6fd9fc7cd8-msj9g
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
      Storing keys in a directory '/root/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
      No XML encryptor configured. Key {06fa0193-972e-43a7-9da9-01c23ee0b99e} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
      Failed to determine the https port for redirect.

Fourth terminal:

bash
$ curl http://localhost:8080/Delay/500
{"delay":500}
$ curl http://localhost:8080/Delay/50000

You have 50s to perform the task in the fifth terminal

Fifth terminal:

bash
$ kubectl delete pod  hello-dotnet-6fd9fc7cd8-msj9g
pod "hello-dotnet-6fd9fc7cd8-msj9g" deleted

This command should take ~1s.

Let's look at the other terminal windows to see what happened.

Second terminal:

bash
$ kubectl get -w po
NAME                            READY   STATUS    RESTARTS   AGE
hello-dotnet-6fd9fc7cd8-msj9g   0/1     Pending   0          0s
hello-dotnet-6fd9fc7cd8-msj9g   0/1     Pending   0          0s
hello-dotnet-6fd9fc7cd8-msj9g   0/1     ContainerCreating   0          0s
hello-dotnet-6fd9fc7cd8-msj9g   1/1     Running             0          3s
hello-dotnet-6fd9fc7cd8-msj9g   1/1     Terminating         0          74s
hello-dotnet-6fd9fc7cd8-znmfj   0/1     Pending             0          0s
hello-dotnet-6fd9fc7cd8-znmfj   0/1     Pending             0          0s
hello-dotnet-6fd9fc7cd8-znmfj   0/1     ContainerCreating   0          0s
hello-dotnet-6fd9fc7cd8-msj9g   0/1     Terminating         0          76s
hello-dotnet-6fd9fc7cd8-msj9g   0/1     Terminating         0          76s
hello-dotnet-6fd9fc7cd8-msj9g   0/1     Terminating         0          76s
hello-dotnet-6fd9fc7cd8-znmfj   1/1     Running             0          3s

The pod quickly recycled upon deletion.

Third terminal:

bash
$ kubectl logs -f hello-dotnet-6fd9fc7cd8-msj9g
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
      Storing keys in a directory '/root/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
      No XML encryptor configured. Key {06fa0193-972e-43a7-9da9-01c23ee0b99e} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
      Failed to determine the https port for redirect.
info: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...

ASP.NET Core gracefully shutdown.


Overview (`samples/kubernetes/hello-dotnet/README.md`)

Configure a .NET app with Kubernetes

This .NET app sample demonstrates multiple Kubernetes settings. It uses/requires a non-root user, has container limits set, uses multiple replicas, and registers a liveness probe.

The following instructions demonstrate how to apply the ClusterIP and LoadBalancer variants of the hello-dotnet sample. ClusterIP is the default service type and works well for local clusters and private cloud deployments. LoadBalancer is intended for exposing an app to the internet. They are other service types, such as Ingress.

Run on your local cluster

Apply to your local cluster, using a ClusterIP service.

bash
$ kubectl apply -f https://raw.githubusercontent.com/dotnet/dotnet-docker/main/samples/kubernetes/hello-dotnet/hello-dotnet.yaml
$ kubectl port-forward service/hello-dotnet 8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

View the sample app at http://localhost:8080/ and call curl http://localhost:8080/Environment.

bash
$ curl http://localhost:8080/Environment 
{"runtimeVersion":".NET 8.0.4","osVersion":"Ubuntu 22.04.4 LTS","osArchitecture":"Arm64","user":"app","processorCount":1,"totalAvailableMemoryBytes":78643200,"memoryLimit":104857600,"memoryUsage":31797248,"hostName":"hello-dotnet-5b887d9fbd-b6bmh"}

The LoadBalancer variant can also be applied locally. It will use port 80.

The following example uses Docker Desktop.

bash
$ kubectl apply -f hello-dotnet-loadbalancer.yaml 
deployment.apps/hello-dotnet created
service/hello-dotnet created
$ kubectl get po
NAME                           READY   STATUS    RESTARTS   AGE
hello-dotnet-bd5fdfcfb-f5vjq   1/1     Running   0          4s
hello-dotnet-bd5fdfcfb-nmshh   1/1     Running   0          4s
hello-dotnet-bd5fdfcfb-twp9d   1/1     Running   0          4s
$ kubectl get service
NAME           TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
hello-dotnet   LoadBalancer   10.101.23.131   localhost     80:31466/TCP   12s
kubernetes     ClusterIP      10.96.0.1       <none>        443/TCP        29h

And then call the Environment endpoint with curl.

bash
$ curl http://localhost/Environment 
{"runtimeVersion":".NET 8.0.4","osVersion":"Ubuntu 22.04.4 LTS","osArchitecture":"Arm64","user":"app","processorCount":1,"totalAvailableMemoryBytes":78643200,"memoryLimit":104857600,"memoryUsage":54845440,"hostName":"hello-dotnet-bd5fdfcfb-f5vjq"}

The following example uses minikube.

bash
$ kubectl apply -f hello-dotnet-loadbalancer.yaml 
deployment.apps/hello-dotnet created
service/hello-dotnet created
$ kubectl get svc -w
NAME           TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
hello-dotnet   LoadBalancer   10.96.208.21   <pending>     80:32597/TCP   29s
kubernetes     ClusterIP      10.96.0.1      <none>        443/TCP        2m28s

The EXTERNAL-IP will stay pending forever.

In another terminal, run minikube tunnel.

bash
$ minikube tunnel  
✅  Tunnel successfully started

As soon as that is run, the EXTERNAL-IP will be provided, back in the original terminal.

bash
$ kubectl get svc -w
NAME           TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
hello-dotnet   LoadBalancer   10.96.208.21   <pending>     80:32597/TCP   29s
kubernetes     ClusterIP      10.96.0.1      <none>        443/TCP        2m28s
hello-dotnet   LoadBalancer   10.96.208.21   127.0.0.1     80:32597/TCP   2m9s

And then a call with curl.

bash
$ curl http://localhost/Environment
{"runtimeVersion":".NET 8.0.4","osVersion":"Ubuntu 22.04.4 LTS","osArchitecture":"Arm64","user":"app","processorCount":1,"totalAvailableMemoryBytes":78643200,"memoryLimit":104857600,"memoryUsage":42151936,"hostName":"hello-dotnet-86f4cffb9d-blcnb"}

This is the same as with Docker Desktop, just with the additional requirement of using minikube tunnel.

Run on your cloud cluster

Apply to your local cluster, using a LoadBalancer service.

The example uses Azure Kubernetes Service (AKS) as the Kubernetes environment

bash
$ az aks get-credentials --resource-group myResourceGroup --name myAKSCluster
$ kubectl get nodes                  
NAME                                STATUS   ROLES    AGE   VERSION
aks-agentpool-17841206-vmss000000   Ready    <none>   31m   v1.29.2
aks-agentpool-17841206-vmss000001   Ready    <none>   32m   v1.29.2
aks-userpool-17841206-vmss000000    Ready    <none>   32m   v1.29.2
aks-userpool-17841206-vmss000001    Ready    <none>   32m   v1.29.2

Apply the LoadBalancer deployment to that cluster.

bash
$ kubectl apply -f https://raw.githubusercontent.com/dotnet/dotnet-docker/main/samples/kubernetes/hello-dotnet/hello-dotnet-loadbalancer.yaml
$ kubectl get service -w
NAME           TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
hello-dotnet   LoadBalancer   10.0.120.232   <pending>     80:31567/TCP   5s
kubernetes     ClusterIP      10.0.0.1       <none>        443/TCP        42h
hello-dotnet   LoadBalancer   10.0.120.232   20.51.80.224   80:31567/TCP   9s

The EXTERNAL-IP will resolve (after a brief wait) and you will be able to access it in the same way, via curl or the browser, on port 80.

bash
$ curl http://20.51.80.224/Environment
{"runtimeVersion":".NET 8.0.4","osVersion":"Ubuntu 22.04.4 LTS","osArchitecture":"X64","user":"app","processorCount":1,"totalAvailableMemoryBytes":78643200,"memoryLimit":104857600,"memoryUsage":42323968,"hostName":"hello-dotnet-bd5fdfcfb-twgqs"}

You may notice that this approach is using raw HTTP on an internet-facing endpoint. That's only acceptable for brief testing like this. Enforce HTTPS in ASP.NET Core and What is Application Gateway Ingress Controller? describe options for exposing secure HTTPS endpoints.


Overview (`samples/kubernetes/manual-deployment/README.md`)

Manual deployment

kubectl enables manual deployment of resources (without manifests).

You can do that with a few quick commands

bash
kubectl create deployment dotnet-app --image mcr.microsoft.com/dotnet/samples:aspnetapp
kubectl expose deployment dotnet-app --type=ClusterIP --port=8080

View the resources that have been deployed.

bash
kubectl get pod
kubectl get service
kubectl get deployment

If the container takes a while to download and launch, you can add -w to get pod to watch updates.

Create a proxy to the service.

bash
kubectl port-forward service/dotnet-app 8080:8080

View the sample app at http://localhost:8080/ or call curl http://localhost:8080/Environment.

Should look like:

bash
$ curl http://localhost:8080/Environment
{"runtimeVersion":".NET 8.0.1","osVersion":"Alpine Linux v3.18","osArchitecture":"X64","user":"root","processorCount":8,"totalAvailableMemoryBytes":67373219840,"memoryLimit":0,"memoryUsage":30572544,"hostName":"dotnet-app-547757d7f4-xqmsk"}

Delete the resources.

bash
kubectl delete service dotnet-app
kubectl delete deployment dotnet-app

Overview (`samples/kubernetes/non-root/README.md`)

Securing containers with a non-root user with Kubernetes

Kubernetes provides a way to validate that a non-root user is used.

Apply non-root.yaml to your cluster.

bash
kubectl apply -f https://raw.githubusercontent.com/dotnet/dotnet-docker/main/samples/kubernetes/non-root/non-root.yaml

Or use the manifest directly if you've cloned the repo.

bash
kubectl apply -f non-root.yaml

You can validate the pod was correctly deployed. If you see Running, that's good.

bash
$ kubectl get po
NAME                               READY   STATUS    RESTARTS   AGE
dotnet-non-root-7db6ff8b6d-zdc9n   1/1     Running   0          21s

You can also validate that runAsNonRoot was set.

bash
$ kubectl get pod dotnet-non-root-7db6ff8b6d-zdc9n -o jsonpath="{.spec.securityContext}" 
{"runAsNonRoot":true}

In many cases, kubectl exec can be used to determine the user, like the following.

bash
$kubectl get po
NAME                               READY   STATUS    RESTARTS   AGE
dotnet-fakepod-7db6ff8b6d-zdc9n   1/1     Running   0          21s
$ kubectl exec dotnet-fakepod-7db6ff8b6d-zdc9n -- whoami
app

This pattern won't work for this sample because whoami doesn't exist in .NET chiseled images. They are appliance images with no shell and no extra tools (like whoami, top, and ps). This pattern will work fine with apps built with our regular images (like 8.0, 8.0-alpine and 8.0-jammy).

If you see CreateContainerConfigError, then the pod failed to deploy.

bash
$ kubectl get po                 
NAME                               READY   STATUS                       RESTARTS   AGE
dotnet-non-root-775d58594f-6w959   0/1     CreateContainerConfigError   0          3s

In this example, a container image with root as the user was used, while runAsNonRoot is set. That combination is incompatible and Kubernetes fails to load the container in that case (as expected).

Now onto using the app. Create a proxy to the service.

bash
$ kubectl port-forward service/dotnet-non-root 8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

View the sample app at http://localhost:8080/ or call curl http://localhost:8080/Environment.

bash
% curl http://localhost:8080/Environment
{"runtimeVersion":".NET 8.0.4","osVersion":"Ubuntu 22.04.4 LTS","osArchitecture":"Arm64","user":"app","processorCount":8,"totalAvailableMemoryBytes":4113563648,"memoryLimit":0,"memoryUsage":34082816,"hostName":"dotnet-non-root-8467576789-9dj8g"}

user is displayed as app, as expected.

Delete the resources (remote URL or local manifest).

bash
kubectl delete -f https://raw.githubusercontent.com/dotnet/dotnet-docker/main/samples/kubernetes/non-root/non-root.yaml

SecurityContext

SecurityContext holds security configuration that will be applied to a container, in a Pod or Deployment manifest. These fields can be used to configure and/or validate that the container image is running as non-root. This sample follows Kubernetes "Restricted" hardening best practices.

This securityContext object can be used to enforce non-root hosting:

yml
spec:
      securityContext:
        runAsNonRoot: true
      containers:
      - name: aspnetapp
        image: mcr.microsoft.com/dotnet/samples:aspnetapp-chiseled
        ports:
        - containerPort: 8080
        securityContext:
          allowPrivilegeEscalation: false
  • allowPrivilegeEscalation -- Prevents (if false) a process from gaining greater privileges than its parent process. This is a good setting, but not directly related to users.
  • runAsNonRoot -- Tests that the user (via uid) is a non-root user, otherwise fail.
  • runAsUser -- Sets the user, via UID. Only needed if not set in the container image or overriding the container image user configuration. This setting is not present in the example above.

securityContext is set twice. In the first case, runAsNonRoot is set of all containers in the pod. There is only one in this example. Setting this property at the pod root sets clear intent and avoids duplication. allowPrivilegeEscalation can only be set for a specific container.

The USER Dockerfile instruction must be set via UID for the the runAsNonRoot setting to work correctly, as demonstrated by Dockerfile and the following example.

dockerfile
USER $APP_UID

Overview (`samples/kubernetes/replicas-and-health/README.md`)

Configuring pod replicas and health checks with Kubernetes

Kubernetes enables creating multiple replicas and registering health checks.

There are multiple forms of health checks. The sample demonstrates a liveness check. The following warning is copied from Kubernetes docs.

Caution: Liveness probes can be a powerful way to recover from application failures, but they should be used with caution. Liveness probes must be configured carefully to ensure that they truly indicate unrecoverable application failure, for example a deadlock.

Apply replica-health.yaml to your cluster.

bash
kubectl apply -f https://raw.githubusercontent.com/dotnet/dotnet-docker/main/samples/kubernetes/replicas-and-health/replica-health.yaml

Apply the local file if you've cloned the repo.

bash
kubectl apply -f replica-health.yaml

Look at the replicas created.

bash
$ kubectl get po
NAME                                     READY   STATUS    RESTARTS   AGE
dotnet-replica-health-64d49554c9-f8mlp   1/1     Running   0          8s
dotnet-replica-health-64d49554c9-fz5ms   1/1     Running   0          8s
dotnet-replica-health-64d49554c9-m6cfr   1/1     Running   0          8s

There should be three.

Look at the events for one of the replicas.

bash
$ kubectl describe pod dotnet-replica-health-64d49554c9-f8mlp
Name:             dotnet-replica-health-64d49554c9-f8mlp
Namespace:        default
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  31s   default-scheduler  Successfully assigned default/dotnet-replica-health-64d49554c9-f8mlp to minikube
  Normal  Pulled     31s   kubelet            Container image "mcr.microsoft.com/dotnet/samples:aspnetapp" already present on machine
  Normal  Created    31s   kubelet            Created container aspnetapp
  Normal  Started    30s   kubelet            Started container aspnetapp

Much of the output has been removed in this example. The key part is the events. Kubernetes will show a failing container, as is demonstrated in Configure Liveness, Readiness and Startup Probes, as follows.

bash
Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Normal   Scheduled  57s                default-scheduler  Successfully assigned default/liveness-exec to node01
  Normal   Pulling    55s                kubelet, node01    Pulling image "registry.k8s.io/busybox"
  Normal   Pulled     53s                kubelet, node01    Successfully pulled image "registry.k8s.io/busybox"
  Normal   Created    53s                kubelet, node01    Created container liveness
  Normal   Started    53s                kubelet, node01    Started container liveness
  Warning  Unhealthy  10s (x3 over 20s)  kubelet, node01    Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
  Normal   Killing    10s                kubelet, node01    Container liveness failed liveness probe, will be restarted

It is the Unhealthy Reason that is used when a liveness probe fails. The container will be killed after that.

Create a proxy to the service.

bash
kubectl port-forward service/dotnet-replica-health 8080

View the sample app at http://localhost:8080/ or call curl http://localhost:8080/Environment.

You can call the healthz endpoint to test the liveness probe.

bash
$ curl http://localhost:8080/healthz
Healthy

Depending on the image, you may be able to call the health check manually with the following kubectl exec pattern.

bash
$ kubectl get po   
NAME                                     READY   STATUS    RESTARTS   AGE
dotnet-replica-health-78c5c58799-cgdgz   1/1     Running   0          16s
dotnet-replica-health-78c5c58799-vpmzj   1/1     Running   0          16s
dotnet-replica-health-78c5c58799-xl4mg   1/1     Running   0          16s
$ kubectl exec dotnet-replica-health-78c5c58799-cgdgz -- wget -qO- -t1 http://localhost:8080/healthz
Healthy

Delete the resources (remote URL or local manifest).

bash
kubectl delete -f https://raw.githubusercontent.com/dotnet/dotnet-docker/main/samples/kubernetes/replicas-and-health/replica-health.yaml

Overview (`samples/kubernetes/resource-limits/README.md`)

Limiting container resources with Kubernetes

Kubernetes enables limiting the CPU and/or memory resources that can be used by a container.

Apply resource-limits.yaml to your cluster.

bash
kubectl apply -f https://raw.githubusercontent.com/dotnet/dotnet-docker/main/samples/kubernetes/resource-limits/resource-limits.yaml

Apply the local file if you've cloned the repo.

bash
kubectl apply -f resource-limits.yaml

See resource limits for the deployment.

bash
kubectl describe deployment

Or look at the pod (only relevant output shown).

bash
$ kubectl get po
NAME                                      READY   STATUS    RESTARTS   AGE
dotnet-resource-limits-54b5c75fdd-jjcbp   1/1     Running   0          12s
$ kubectl describe pod dotnet-resource-limits-54b5c75fdd-jjcbp
    Limits:
      cpu:     500m
      memory:  100Mi
    Requests:
      cpu:        250m
      memory:     60Mi

Create a proxy to the service.

bash
kubectl port-forward service/dotnet-resource-limits 8080

View the sample app at http://localhost:8080/ or call curl http://localhost:8080/Environment. You can see memory and CPU usage displayed. CPU is reported as an integer and always rounded up to the next whole integer (for example, 0.25 is rounded up to 1).

bash
% curl http://localhost:8080/Environment
{"runtimeVersion":".NET 8.0.5","osVersion":"Alpine Linux v3.19","osArchitecture":"X64","user":"root","processorCount":1,"totalAvailableMemoryBytes":78643200,"memoryLimit":104857600,"memoryUsage":34095104,"hostName":"dotnet-resource-limits-8685bd4b75-jltn5"}

processorCount is display as 1, as expected.

Delete the resources (remote URL or local manifest).

bash
kubectl delete -f https://raw.githubusercontent.com/dotnet/dotnet-docker/main/samples/kubernetes/resource-limits/resource-limits.yaml

Overview (`tests/Microsoft.DotNet.Docker.Tests/TestAppArtifacts/otlptestlistener/README.md`)

OTLP Test Listener

A small server that implements an OTLP endpoint.

Point an application at it and it will collect the OTLP output and generate stats on what it saw

Call /report to get a json response with the stats. For example:

text
/* Detailed source-code truncated for AI context efficiency. */

Overview (`tests/Microsoft.DotNet.Docker.Tests/TestAppArtifacts/otlptestlistener/OtlpTestListener/otlp/opentelemetry/proto/README.md`)

OpenTelemetry Proto

See open-telemetry on GitHub for all the sources.

We only support v1 at this point.


Overview (`tests/Microsoft.DotNet.Docker.Tests/TestAppArtifacts/otlptestlistener/OtlpTestListener/otlp/opentelemetry/proto/collector/README.md`)

OpenTelemetry Collector Proto

This package describes the OpenTelemetry collector protocol.

Packages

  1. common package contains the common messages shared between different services.
  2. trace package contains the Trace Service protos.
  3. metrics package contains the Metrics Service protos.
  4. logs package contains the Logs Service protos.

Documentation Azurelinux (`documentation/azurelinux.md`)

Azure Linux

Azure Linux is an MIT-licensed, RPM-based Linux distribution published by Microsoft.
Azure Linux .NET images are publicly supported by Microsoft.

.NET 10

  • 10.0-azurelinux3.0
    • docker pull mcr.microsoft.com/dotnet/sdk:10.0-azurelinux3.0
    • docker pull mcr.microsoft.com/dotnet/aspnet:10.0-azurelinux3.0
    • docker pull mcr.microsoft.com/dotnet/runtime:10.0-azurelinux3.0
    • docker pull mcr.microsoft.com/dotnet/runtime-deps:10.0-azurelinux3.0

.NET 9

  • 9.0-azurelinux3.0
    • docker pull mcr.microsoft.com/dotnet/sdk:9.0-azurelinux3.0
    • docker pull mcr.microsoft.com/dotnet/aspnet:9.0-azurelinux3.0
    • docker pull mcr.microsoft.com/dotnet/runtime:9.0-azurelinux3.0
    • docker pull mcr.microsoft.com/dotnet/runtime-deps:9.0-azurelinux3.0

.NET 8

  • 8.0-azurelinux3.0
    • docker pull mcr.microsoft.com/dotnet/sdk:8.0-azurelinux3.0
    • docker pull mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0
    • docker pull mcr.microsoft.com/dotnet/runtime:8.0-azurelinux3.0
    • docker pull mcr.microsoft.com/dotnet/runtime-deps:8.0-azurelinux3.0

Azure Linux Distroless

Azure Linux distroless .NET images contain only the minimal set of packages .NET needs, with everything else removed.
See the distroless .NET containers documentation for more info.

Azure Linux distroless .NET images are available for all supported .NET versions in the following image repos:

You can use the following image tags:

  • 10.0-azurelinux3.0-distroless
  • 9.0-azurelinux3.0-distroless
  • 8.0-azurelinux3.0-distroless

Installing Additional Packages

If your app requires additional packages besides icu and tzdata, you can follow the same pattern that .NET uses to install the .NET runtime dependencies.

text
/* Detailed source-code truncated for AI context efficiency. */

Debugging with Container Fast Mode

In order to reduce image size and attack surface, Azure Linux .NET Runtime images do not contain the tools required to debug .NET apps using Visual Studio.
The easiest way to enable local Visual Studio debugging while not modifying the production image is to use Container Fast Mode.

To enable Container Fast Mode debugging without affecting your app's production image, you can create a new stage based off the base stage (called debug in the example) that contains the debugging tools, and then point the VS Fast Mode tools to that debug stage.

Here's an example chart showing the inheritance of the build stages:

mermaid
flowchart TD
    base --> debug
    base --> final
    build --> publish

Here's an example configuration based off of a simple ASP.NET Core app:

*.csproj

diff
<PropertyGroup>
  ...
+ <DockerfileFastModeStage>debug</DockerfileFastModeStage>
  ...
</PropertyGroup>

Dockerfile

diff
# Learn about building .NET container images:
# https://github.com/dotnet/dotnet-docker/blob/main/samples/README.md
FROM mcr.microsoft.com/dotnet/aspnet:11.0-azurelinux3.0 AS base
- USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

+ # this stage is used by VS for fast local debugging; it does not appear in the final image
+ FROM base AS debug
+ RUN tdnf install -y procps-ng # <-- Install tools needed for debugging (e.g. the `pidof` command)

FROM mcr.microsoft.com/dotnet/sdk:11.0-azurelinux3.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["AspNetCoreRazorApp/AspNetCoreRazorApp.csproj", "AspNetCoreRazorApp/"]
RUN dotnet restore "./AspNetCoreRazorApp/AspNetCoreRazorApp.csproj"
COPY . .
WORKDIR "/src/AspNetCoreRazorApp"
RUN dotnet build "./AspNetCoreRazorApp.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./AspNetCoreRazorApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
+ # Make sure non-root user is enabled after the debug stage so that we have permission to install the debug dependencies
+ USER $APP_UID
ENTRYPOINT ["dotnet", "AspNetCoreRazorApp.dll"]

If this example doesn't work for your scenario, see Container Tools build properties for more information on
customizing the Fast Mode stage, or setting a custom DockerDebuggeeKillProgram.


Documentation Distroless (`documentation/distroless.md`)

Distroless .NET Images

.NET "distroless" container images contain only the minimal set of packages .NET needs, with everything else removed.
Due to their limited set of packages, distroless containers have a minimized security attack surface, smaller deployment sizes, and faster start-up time compared to their non-distroless counterparts.
They contain the following features:

  • Minimal set of packages required for .NET applications
  • Non-root user by default
  • No package manager
  • No shell

We offer distroless .NET images for two operating systems: Ubuntu Chiseled and Azure Linux.

.NET distroless images are available for the following image repos:

We’re not offering distroless SDK images at the moment as there hasn't been a strong need for one, and because they may be difficult to use to use for some scenarios due to the lack of a shell.
You can use the existing SDK images to build your apps to run on distroless base images.
If you have a compelling use case for a distroless SDK image, please leave a comment on this issue and we’ll be happy to reconsider.

FAQs

How do I use globalization with distroless images?

Our distroless images are focused on size. That means the default distroless images do not include the icu or tzdata
libraries. However, we offer an extra image variant that includes tzdata and icu by default.
You can use this in place of the default distroless images by appending the -extra suffix to the image tag like so:

Ubuntu:

  • mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled-extra
  • mcr.microsoft.com/dotnet/runtime:10.0-noble-chiseled-extra
  • mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-extra

Azure Linux:

  • mcr.microsoft.com/dotnet/runtime-deps:10.0-azurelinux3.0-extra
  • mcr.microsoft.com/dotnet/runtime:10.0-azurelinux3.0-extra
  • mcr.microsoft.com/dotnet/aspnet:10.0-azurelinux3.0-extra

Please see "Announcement: New approach for differentiating .NET 8+ images" for more info.

How can I scan distroless images for security vulnerabilities?

Both Ubuntu Chiseled and Azure Linux .NET images contain package installation data and are supported by many major scanning tools such as Qualys, Trivy, Syft, etc.
For example, you can scan for CVEs with Docker Scout using the following command:

Ubuntu:

bash
docker scout cves mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled

Azure Linux:

bash
docker scout cves mcr.microsoft.com/dotnet/runtime-deps:10.0-azurelinux3.0-distroless

How do I write my Dockerfile to work without a shell?

If you switch your containers to distroless images, you may run into one of the following errors:

  • stat /bin/sh: no such file or directory
  • docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown.

These errors happen when you try to invoke a shell command in a container that doesn't include sh or bash.
To avoid these, you'll need to make sure your Dockerfile is architected correctly and your .NET app doesn't depend on the shell.

First, use the exec form instead of the shell form for instructions in your Dockerfile's distroless stage. For example:

dockerfile
# "Exec" form - Works without a shell
RUN ["dotnet", "--list-runtimes"]
ENTRYPOINT ["dotnet", "myapp.dll"]
CMD ["dotnet", "myapp.dll", "--", "args"]

# "Shell" form - Doesn't work without a shell
RUN dotnet --list-runtimes
ENTRYPOINT dotnet myapp.dll
CMD dotnet myapp.dll -- args

Please see Docker's Dockerfile documentation for more info on instruction formatting.

If you need to run any shell commands or other utilities at build time, you can do so in the build stage of the multi-stage Dockerfile and copy the results to the final stage.
For example, you could download and extract an archive so that the files will be available in your container.

dockerfile
# build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS build
# OR
# FROM mcr.microsoft.com/dotnet/sdk:10.0-azurelinux3.0 AS build
...
RUN wget -O somefile.tar.gz <URL> \
    && tar -oxzf aspnetcore.tar.gz -C /somefile-extracted
...

# final stage/image
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled
# OR
# FROM mcr.microsoft.com/dotnet/sdk:10.0-azurelinux3.0-distroless AS build
...
COPY --from=build /somefile-extracted .
...

How do I handle file permissions when running as a non-root user?

If your app writes to the disk, you may encounter permission issues at runtime, such as:

text
System.UnauthorizedAccessException: Access to the path "<path>" is denied

Distroless .NET images use a non-root user by default.
This error happens when apps try to write to a directory or file that the current user doesn't have access to.
By design, an app shouldn't be able to modify the directory it's running from. Instead, you should try to write to the non-root user's home directory.

For example, instead of:

cs
File.WriteAllLines("myFile.txt", myText);

Try writing to the user's home directory:

cs
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "myFile.txt");
File.WriteAllLines(path, myText);

If you are writing to bind or volume mounted directories, you will need to make sure the directory gives the correct permissions to the non-root user.
You can check the user ID that a container will use by running the following command:

Ubuntu:

console
docker image inspect mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled -f "{{ .Config.User }}"

Azure Linux:

console
docker image inspect mcr.microsoft.com/dotnet/runtime-deps:10.0-azurelinux3.0-distroless -f "{{ .Config.User }}"

Additionally, from within the container, you can get the UID of the non-root user by reading the APP_UID environment variable.


Documentation Guiding Principles (`documentation/guiding-principles.md`)

Guiding Principles

These are the guiding principles for the content, tagging and production of the official .NET Docker images. They establish a framework for the behavior customers can expect and the decision-making process when making changes to the images. They are not hard rules which are stringently enforced as there will be occasions where it is sensible to make special exceptions.

Image Content

  1. Images are intended to satisfy the common usage scenarios. They are not intended to satisfy every possible usage scenario. As a result of this, decisions will be made (e.g. components excluded, configurations made, etc.) in order to keep the image size manageable.

    It is expected there will be scenarios in which customers will need to create derived images that add the required components/settings. A few examples include:

    1. Alpine images have the .NET Invariant Mode enabled by default (details). Use cases that cannot tolerate the globalization invariant mode can reset the DOTNET_SYSTEM_GLOBALIZATION_INVARIANT environment variable.
    2. The System.Drawing.Common native dependencies are not included by default. If System.Drawing.Common is being used, the native dependencies will need to be added.

    Component authors can package their bits in ways which make them more easily consumable from Docker without including them within the .NET images. Examples include:

    1. Support xcopy installs
    2. Package as a .NET global tool
    3. Ship within their own Docker image
      1. Support standalone execution and multi-stage builds
      2. Support running within a side car image
  2. Components installed within the images are required to have the same or longer support lifecycle as .NET. For example, if a component is included with an LTSC release of .NET, then that major.minor component version will need to be supported for the life of the LTSC release. Components are expected to be serviced over their lifetime as appropriate.

  3. Breaking changes are not allowed within a release. This includes changes such as adding/removing components, adding/removing ENVs, and major/minor version changes to included components.

  4. There should be parity within the supported image matrix. Examples include:

    1. If support for a new distro is added for a particular .NET version, then new runtime-deps, runtime, aspnet, and sdk images should all be added for the new distro.
    2. If a new component is added, it should be available across all supported OS types and architectures.
  5. The matrix of supported container operating systems and versions will evolve on a continuous basis.

Image Tagging

See the supported tags for the tagging practices and policies used by the .NET team.

Engineering

  1. Images will be included as part of the .NET release process. The Docker images will be released at the same time as the core product.

  2. Images will be rebuilt only as necessary in order to limit downstream image rebuilds and deployments for consumers of .NET images.
    The Image Update Policy section of the README contains the exact guidelines for when images will be updated.

  3. Images will never be deleted from the official .NET Docker repositories on the Microsoft Artifact Registry. This does not apply to the nightly repositories.

  4. The Dockerfiles used to produce all of the images will be publicly available. Customers will be able to take the Dockerfiles and build them to produce their own equivalent images. No special build steps or permissions should be needed to build the Dockerfiles.

  5. No experimental Docker features will be utilized within the infrastructure used to produce the images. Utilizing experimental features can negatively affect the reliability of image production and introduces a risk to the integrity of the resulting artifacts.


Documentation Image Artifact Details (`documentation/image-artifact-details.md`)

Determining License and Source Pedigree for .NET Container Images

This document is intended to be complimentary to the Instructions for Finding Linux Legal Metadata. This document describes how to interrogate the .NET images to determine licensing and version information for .NET and other components.

[!WARNING]
Distroless images require special treatment since they don't contain a shell by default.
See the distroless images section below.

.NET and Other Components

.NET and other components are carried in .NET images, all of which are from Microsoft or the .NET Foundation. The following list describes the complete set of other software (beyond base images and packages):

  • .NET Runtime
  • ASP.NET Core Runtime
  • .NET SDK
  • PowerShell

You can see these components installed in the runtime, aspnet, and sdk Dockerfiles.

.NET Runtime and ASP.NET Core Images

The runtime and ASP.NET Core images include a license and third party notice file.

console
$ docker run --rm mcr.microsoft.com/dotnet/runtime:10.0 /bin/sh -c "find ./usr/share/dotnet | grep -i 'license\|third'"
./usr/share/dotnet/LICENSE.txt
./usr/share/dotnet/ThirdPartyNotices.txt
console
$ docker run --rm mcr.microsoft.com/dotnet/aspnet:10.0 /bin/sh -c "find ./usr/share/dotnet | grep -i 'license\|third'"
./usr/share/dotnet/LICENSE.txt
./usr/share/dotnet/ThirdPartyNotices.txt

.NET SDK Image

The SDK image includes the .NET SDK, which includes various .NET components, with associated licenses and third party notice files.

text
/* Detailed source-code truncated for AI context efficiency. */

Distroless Images and Appliance Images

The commands listed above won't work when targeting a distroless image since distroless images don't contain a shell by default.
Instead, this repo contains a Dockerfile that copies a distroless image's entire filesystem into a non-distroless base image so that its contents can be easily inspected using a shell.

First, build the Dockerfile, specifying the distroless image tag you wish to inspect.
Cloning the repo or downloading the Dockerfile is not necessary.

console
docker build -t distroless-wrapper -f ./Dockerfile.distroless-wrapper --build-arg DISTROLESS_IMAGE=mcr.microsoft.com/dotnet/runtime:10.0-noble-chiseled https://github.com/dotnet/dotnet-docker.git#main:documentation/scripts

Now that you've got the wrapper image, you can execute the commands that are documented targeting the wrapper image instead:

console
$ docker run --rm distroless-wrapper /bin/sh -c "find ./usr/share/dotnet | grep -i 'license\|third'"
./usr/share/dotnet/LICENSE.txt
./usr/share/dotnet/ThirdPartyNotices.txt

Appliance Images

.NET Monitor, Aspire Dashboard, and YARP images are all distroless images and should be inspected the same way as other distroless .NET images.

Each appliance image includes its own license and third party notices files.
Appliance image binaries and metadata are located under the /app directory.
Example using .NET Monitor:

console
$ docker build -t distroless-wrapper -f ./Dockerfile.distroless-wrapper --build-arg DISTROLESS_IMAGE=mcr.microsoft.com/dotnet/monitor https://github.com/dotnet/dotnet-docker.git#main:documentation/scripts
...

$ docker run --rm distroless-wrapper /bin/sh -c "find ./app | grep -i 'license\|third'"
./app/extensions/S3Storage/LICENSE.TXT
./app/extensions/S3Storage/THIRD-PARTY-NOTICES.TXT
./app/extensions/AzureBlobStorage/LICENSE.TXT
./app/extensions/AzureBlobStorage/THIRD-PARTY-NOTICES.TXT
./app/LICENSE.TXT
./app/THIRD-PARTY-NOTICES.TXT

Documentation Image Signatures (`documentation/image-signatures.md`)

.NET Container Image Signatures

.NET container images are signed using Notary Project signatures.

Inspect signature artifacts

Signatures are stored as OCI referrer artifacts
attached to the signed image in the registry. This structure is visible when
using the ORAS CLI to list referrer artifacts associated with an image:

console
> oras discover mcr.microsoft.com/dotnet/sdk:10.0
mcr.microsoft.com/dotnet/sdk@sha256:<digest>
└── application/vnd.cncf.notary.signature
    └── sha256:<signature-digest>
        └── [annotations]
            ├── org.opencontainers.image.created: "2026-04-10T20:33:39.8412151Z"
            └── io.cncf.notary.x509chain.thumbprint#S256: '["84813e9c...","9b1894f2...","23ffe2b8..."]'

In this example, mcr.microsoft.com/dotnet/sdk:10.0 has a single referrer
artifact with the application/vnd.cncf.notary.signature media type. The
Notation CLI reveals more detailed information about the attached signature
artifact:

text
/* Detailed source-code truncated for AI context efficiency. */

Verify signatures using Notation CLI

Prerequisites

Download the root CA certificates

.NET container image signatures use the following certificates:

Set up Trust Store and Trust Policy

See the Notary Project's Trust Store and Trust Policy Specification.

First, add both signatures listed above to a new notation trust store:

console
notation certificate add --type ca --store msft-supplychain "Microsoft Supply Chain RSA Root CA 2022.crt"
notation certificate add --type tsa --store msft-supplychain "MicRooCerAut_2010-06-23.crt"

Next, set up a trust policy according to the Trust Policy
section of the Notation spec. Here is an example trust policy configuration
for verifying .NET images:

json
{
  "version": "1.0",
  "trustPolicies": [
    {
      "name": "supplychain",
      "registryScopes": [
          "mcr.microsoft.com/aspire/dashboard",
          "mcr.microsoft.com/dotnet/aspire-dashboard",
          "mcr.microsoft.com/dotnet/aspnet",
          "mcr.microsoft.com/dotnet/monitor",
          "mcr.microsoft.com/dotnet/monitor/base",
          "mcr.microsoft.com/dotnet/runtime",
          "mcr.microsoft.com/dotnet/runtime-deps",
          "mcr.microsoft.com/dotnet/sdk"
      ],
      "signatureVerification": { "level": "strict" },
      "trustStores": ["ca:msft-supplychain", "tsa:msft-supplychain"],
      "trustedIdentities": [
        "x509.subject: CN=Microsoft SCD Products RSA Signing,O=Microsoft Corporation,L=Redmond,S=Washington,C=US"
      ]
    }
  ]
}

[!CAUTION]
A misconfigured trust policy can cause signature verification to silently
pass without logging errors for invalid or missing signatures. Read the trust
policy specification carefully.

Import the trust policy configuration using notation:

console
notation policy import trustpolicy.json

Resolve multi-platform image digests

For multi-platform tags, it is important to verify signatures of both the
multi-platform and the platform-specific manifests. See supported tags
for more details on supported single-platform and multi-platform tags.

Use oras manifest fetch $image or docker buildx imagetools inspect $image
to determine whether an image is multi-platform or single-platform.
Multi-platform images have one of the following values for mediaType:

  • application/vnd.docker.distribution.manifest.list.v2+json
  • application/vnd.oci.image.index.v1+json

For multi-platform images, all of the platform-specific image digests will be
listed under the manifests property. Each multi-platform image and each of
its constituent single-platform images has its own unique signature artifact.

Verify image signature

[!IMPORTANT]
Always use full image digests when verifying signatures, since image tags are
mutable.

To verify the signature of an image digest, run:

console
> notation verify mcr.microsoft.com/dotnet/sdk@sha256:<digest>
Successfully verified signature for mcr.microsoft.com/dotnet/sdk@sha256:<digest>

Documentation Image Variants (`documentation/image-variants.md`)

.NET Image Variants

.NET container images offer several image variants.

.NET images for Ubuntu, Debian, Azure Linux, Windows Server Core, and Nano Server images are full-featured and include ICU and time zone libraries which provide full Unicode and globalization support.
These images are intended to satisfy the most common use cases of .NET developers.

Alpine and Ubuntu Chiseled .NET images are focused on size.
By default, these images do not include icu or tzdata, meaning that these images only work with apps that are configured for globalization-invariant mode.
Apps that require globalization support can use the extra image variant (see below).
Alpine, Azure Linux, and Ubuntu Chiseled images also come in extra, composite, and aot variants (see below).

Distroless

Ubuntu Chiseled and Azure Linux distroless .NET images contain only the minimum set of libraries necessary to run .NET applications with everything else removed.
For more information, see the distroless .NET images documentation.

`extra` (.NET 8+)

For apps that depend on globalization functionality, the extra image variant is offered for Ubuntu Chiseled and Azure Linux distroless runtime-deps, runtime, and aspnet images as well as runtime-deps, runtime, and aspnet images for Alpine Linux.
These extra images contain everything that the default images do, plus icu and tzdata.

`composite` (.NET 8+)

ASP.NET Core Composite images provide a smaller size on disk while keeping the performance of the default ReadyToRun (R2R) setting.
The caveat is that the composite images have tighter version coupling. This means the final app run on them cannot use handpicked custom versions of the framework and/or ASP.NET assemblies that are built into the composite binary.
For a full technical description on how the composites work, we have a feature doc here.

`aot` (.NET 10+, SDK-only)

SDK images with the -aot suffix include additional libraries required for native AOT compilation that aren't present in the default mcr.microsoft.com/dotnet/sdk images.

Native AOT compilation produces an app that's self-contained and that has been ahead-of-time (AOT) compiled to native code.
Native AOT apps have faster startup time and smaller memory footprints.
Native AOT apps can be deployed on the standard mcr.microsoft.com/dotnet/runtime-deps images.
See "Limitations of Native AOT deployment" to see if your app might be compatible.


Documentation Sample Image Size Report (`documentation/sample-image-size-report.md`)

.NET image size comparison

.NET offers a variety of deployment options for applications which pair well
with the container images that we publish. It's possible to produce very small
container images. This document summarizes the available options to help you
make the best base image choice for your apps and environment.

The table below shows how base image choice and publish type affects typical
image sizes and for a simple .NET minimal web API. These images were produced
from the "AspNetCoreNativeAot" sample.

Base Image Publish Type Distroless Globalization Compressed Size
aspnet:10.0 Framework-dependent ✖️ No ✅ Yes 92.48 MB
aspnet:10.0-noble-chiseled Framework-dependent ✅ Yes ✖️ No 52.81 MB
aspnet:10.0-noble-chiseled-extra Framework-dependent ✅ Yes ✅ Yes 67.68 MB
runtime-deps:10.0 Self-contained + Trimming ✖️ No ✖️ No 61.53 MB
runtime-deps:10.0 Self-contained + Trimming ✖️ No ✅ Yes 61.63 MB
runtime-deps:10.0-noble-chiseled Self-contained + Trimming ✅ Yes ✖️ No 21.86 MB
runtime-deps:10.0-noble-chiseled-extra Self-contained + Trimming ✅ Yes ✅ Yes 36.82 MB
runtime-deps:10.0 Native AOT ✖️ No ✖️ No 51.27 MB
runtime-deps:10.0 Native AOT ✖️ No ✅ Yes 51.36 MB
runtime-deps:10.0-noble-chiseled Native AOT ✅ Yes ✖️ No 11.60 MB
runtime-deps:10.0-noble-chiseled-extra Native AOT ✅ Yes ✅ Yes 26.56 MB
aspnet:10.0-alpine Framework-dependent ✖️ No ✖️ No 51.93 MB
aspnet:10.0-alpine-extra Framework-dependent ✖️ No ✅ Yes 66.50 MB
runtime-deps:10.0-alpine Self-contained + Trimming ✖️ No ✖️ No 20.95 MB
runtime-deps:10.0-alpine-extra Self-contained + Trimming ✖️ No ✅ Yes 35.52 MB
runtime-deps:10.0-alpine Native AOT ✖️ No ✖️ No 10.69 MB
runtime-deps:10.0-alpine-extra Native AOT ✖️ No ✅ Yes 25.25 MB

[!NOTE]
Please note that these image sizes are a snapshot of deployment sizes from
November 2025. Image sizes will fluctuate over time due to base image and
package updates.

Watch the announcements page
for the latest information on new features and changes in .NET container images.

About Publishing Options

.NET Image Variants

  • Ubuntu Chiseled images images are a type of distroless container image
    that contain only a minimal set of packages with everything else removed - no
    shell, no package manager, and no globalization support by default. This
    results in a dramatically smaller deployment size and attack surface.
  • extra images add icu and tzdata to Alpine and Chiseled/distroless
    images for full globalization support in .NET apps.

See .NET Image Variants
for more details.

Publishing Types

  • Self-contained deployments bundle the .NET Runtime with your app so that
    it can run without the .NET Runtime installed.
  • Trimming for self-contained apps removes unused code from the .NET
    Runtime and libraries to reduce application size.
  • Native AOT deployment produces an app that is compiled ahead-of-time
    (AOT) for the smallest deployment size and startup time.

See ".NET Application Publishing Overview"
for more details on all of the supported options for publishing .NET apps.


Documentation Supported Platforms (`documentation/supported-platforms.md`)

Supported Platforms

This document describes the platforms (OS and architectures) supported by the official .NET Docker images.

Operating Systems

.NET supports a broad set of operating systems and versions. When producing container images, it’s impractical to support the full matrix of OS, arch, and .NET version combinations. In practice, images are produced for a select set of operating systems and versions. If official .NET container images aren't provided for your preferred OS, let us know by opening a discussion. Alternatively, you can author your own .NET images.

  • Images for new OS versions are typically released within one month of the new OS release, with a goal to release same-day when possible.
  • New OS versions are available in dotnet/nightly repositories first, and are added to the officially supported repos afterwards.
  • All new OS releases will be accompanied by an announcement.
  • Unless otherwise specified (see Alpine Linux below), we stop publishing updates to images when the .NET version in the image reaches end of support or when the OS base image stops receiving updates, whichever happens first.

These policies are specific to .NET container images. For more information on overall .NET OS support, see .NET OS Support Tracking.

Linux

Since Linux distributions ("distros") each have unique release and support policies, a one-size-fits-all policy doesn't make sense.
Instead, we have separate policies for each distro.
We publish .NET container images for the following versions of each Linux distro:

  • Alpine Linux
    • .NET images will be published for the latest Alpine Linux version.
    • When new Alpine versions are released, new images will be added for all in-support .NET versions.
    • .NET images for the previous version of Alpine will published for 3 months after the new version is released.
  • Azure Linux and Ubuntu LTS
    • .NET images will be published images for the latest OS version at the time a new major/minor version of .NET is released.
    • When new OS versions are released, new images will be added to the latest .NET version and latest LTS .NET version (if they differ).
  • Debian
    • Existing .NET Debian images will continue to receive base image updates, but new .NET images will not be added for future Debian versions.

Pre-release versions of the above distros will be published in the nightly repositories, pending the availability of pre-release base images.

FedRAMP Compliance

For .NET appliance images based on Azure Linux, base image OS upgrades will be delayed until the new version of Azure Linux has FedRAMP approval.

Windows

The official .NET images support Nano Server as well as LTS versions of Windows Server Core. Nano Server is the best Windows SKU to run .NET apps from a performance perspective. In order for Nano Server to perform well and remain lightweight, it doesn't have support for every scenario. In case your scenario isn't supported by Nano Server, you may need to use one of the .NET images based on Windows Server Core.

Windows Server support timelines can be found here: Windows Server release information. .NET images will be published for the following Windows OS versions:

For scenarios where the official .NET images don't meet your needs, you will need to manage your own custom .NET images based on Windows Server Core or Windows Server.

Architectures

.NET images are provided for the following architectures.

  • Linux/Windows x86-64 (amd64)
  • Linux ARMv7 32-bit (arm32v7)
  • Linux ARMv8 64-bit (arm64v8)

Documentation Supported Tags (`documentation/supported-tags.md`)

.NET Container Tags -- Patterns and Policies

This document describes the tagging patterns and policies that are used for the official .NET container images. .NET tags are intended to closely match the tagging patterns used by Official Images on Docker Hub. Please log an issue if you encounter problems using .NET images or applying these tagging patterns.

Complete tag lists:

The terms "fixed version" and "floating version" are used throughout. They are defined in Tag policies.

Single-platform tags

These tags reference an image for a single platform (e.g. "Linux Arm64" or "Windows x64").

`--`

These "fixed version" tags reference an image with a specific Major.Minor.Patch .NET version for a specific operating system and architecture.

Examples:

  • 8.0.11-noble-amd64
  • 8.0.11-noble-arm64v8
  • 8.0.11-nanoserver-ltsc2022
  • 8.0.11-alpine3.21-arm64v8
  • 8.0.11-bookworm-slim-arm32v7

`--`

These "floating version" tags reference an image with a specific Major.Minor (with latest patch) .NET version for a specific operating system and architecture.

Examples:

  • 8.0-noble-arm64v8
  • 8.0-noble-amd64
  • 8.0-nanoserver-ltsc2022
  • 8.0-alpine3.21-arm64v8
  • 8.0-bookworm-slim-arm32v7

Multi-platform tags

These tags reference images for multiple platforms.

They include:

  • For .NET 10, multi-platform tags refer to Ubuntu unless otherwise specified (like 8.0-alpine).
  • For .NET versions prior to 10, multi-platform tags refer to Debian unless otherwise specified.
  • All supported architectures.

[!WARNING]
These multi-platform tags specifically exclude all Windows versions due
to containerd's platform matching algorithm for Windows hosts. See
containerd/containerd#6508
and dotnet/dotnet-docker#4492
for more context.

If you are using Windows, you will need to explicitly specify an OS Version
with a single-platform tag like so:

dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0-nanoserver-ltsc2022
FROM mcr.microsoft.com/dotnet/sdk:8.0-nanoserver-1809
FROM mcr.microsoft.com/dotnet/sdk:8.0-windowsservercore-ltsc2019
FROM mcr.microsoft.com/dotnet/sdk:8.0-windowsservercore-ltsc2022

`-`

These "fixed version" tags reference an image with a specific Major.Minor.Patch .NET version, for a specific operating system, while architecture will be chosen based on the requesting environment.

Examples:

  • 8.0.11-noble
  • 8.0.11-alpine3.21

`-`

These "floating version" tags reference an image with a specific Major.Minor (with latest patch) .NET version, for a specific operating system, while architecture will be chosen based on the requesting environment.

Examples:

  • 8.0-alpine3.21
  • 8.0-noble

`-alpine`

These "floating version" tags reference an image with a specific Major.Minor (with latest patch) .NET version, for the latest Alpine version, while architecture will be chosen based on the requesting environment.

Examples:

  • 8.0-alpine
  • 9.0-alpine

[!NOTE]

  • New versions of Alpine will be published with version-specific tags (e.g. 8.0-alpine3.21).
  • Floating tag (e.g. 8.0-alpine) will be updated with the new Alpine version a month later.
  • Tag changes will be announced so that users know when the tags they want are available.

``

These "fixed version" tags reference an image with a specific Major.Minor.Patch .NET version, while operating system and architecture will be chosen based on the requesting environment.

Examples:

  • 8.0.11
  • 9.0.11

``

These "floating version" tags reference an image with a specific Major.Minor (with latest patch) .NET version, while operating system and architecture will be chosen based on the requesting environment.

Examples:

  • 8.0
  • 9.0

Image Variants

Starting with 8.0, .NET offers several image variants that provide different features for the size-focused OSes, Alpine, Azure Linux, and Ubuntu Chiseled.
You can use these variants by appending the variant name (e.g. extra, chiseled) to the OS name.

Examples:

  • 8.0-noble-chiseled
  • 8.0.11-noble-chiseled-extra
  • 9.0.0-alpine3.21-extra

For more information, see the Image Variants documentation.

`latest`

These "floating version" latest tag references an image with the latest Major.Minor.Patch .NET version, while operating system and architecture will be chosen based on the requesting environment.

[!NOTE]

  • The latest tag references the latest stable release.
  • In the nightly image repo, it may reference the latest preview release.

Tag policies

The following policies are used for the tag patterns we use.

.NET Versions

Each .NET release goes through multiple support phases, with varying support levels.

While a new .NET version is in preview, both the fixed and floating version tags have a special preview suffix.
When a .NET version moves from Preview to Release Candidate, the -preview suffix is dropped from the floating version tags.
This is because .NET Release Candidates are supported for production use under a "Go-Live" license, so the preview label no longer applies.
In this case, fixed versions will use an -rc.<RC Number> suffix since the patch version would otherwise clash with the GA release (e.g. 9.0.0).

Support phase Fixed version tag format Floating version tag format
Preview <Major>.<Minor>.-preview. <Major>.<Minor>-preview
Release Candidate (Go-Live) <Major>.<Minor>.-rc.<ReleaseCandidateVersion> <Major>.<Minor>
Active and Maintenance <Major>.<Minor>. <Major>.<Minor>

See .NET's release policies for more details.

Fixed version tags

"Fixed version" tags reference an image with a specific .NET patch version.

Examples:

  • 8.0.11
  • 8.0.11-alpine3.21
  • 9.0.0-preview.7
  • 9.0.0-rc.1

[!NOTE]

  • Fixed version tags are updated according to the image update policy for the supported life of the image (typically one month).
  • Fixed version tags guarantee that the version of .NET in the image will never change.
  • At times, components of .NET images like PowerShell or MinGit may require updates out of band with .NET releases in order to fix critical bugs or vulnerabilities. If this happens, new images will be created with a -1 suffix appended to the fixed tag so that you can roll back to the previous fixed tag if necessary. The same practice will repeat itself if necessary (with -2 and then -3 tags).

Floating version tags

"Floating version" tags references an image with a specific Major.Minor .NET version, but float on patch updates.

Examples:

  • 8.0
  • 9.0
  • 9.0-alpine3.21
  • 9.0-preview
  • 9.0-preview-noble

[!NOTE]

  • Floating version tags always point to the latest patch version of a given Major.Minor .NET release.
  • Floating version tags are updated according to the image update policy for the supported life of the .NET release or until the OS they are based on reaches EOL, whichever is sooner. We will post an Announcement when we remove or stop supporting images for any reason.

OS tags and base image updates

Version-specific operating system tags reference an image with a specific OS version, but floats on OS patch updates. See Supported Platforms for the list of supported operating systems.

Examples:

  • 8.0-noble
  • 9.0-alpine3.21

[!NOTE]

  • These tags are updated in response to base image updates (like an Ubuntu base image) for the supported life of the .NET release or until the OS they are based on reaches EOL, whichever is sooner.
  • Digest pinning is required to request a specific patch of an operating system (e.g. mcr.microsoft.com/dotnet/runtime@sha256:4d3d5a5131a0621509ab8a75f52955f2d0150972b5c5fb918e2e59d4cb9a9823).
  • If an image is only available for one operating system, then the operating system will be omitted from the tag.
  • For Debian and Ubuntu images, release codenames are used instead of version numbers.

.NET appliance images

.NET produces several appliance images containing useful diagnostic tools and applications:

These appliance images provide value based on the .NET apps they include.
Their base operating system is an implementation detail and should not affect the intended use or behavior of these images.
They use distroless operating systems whenever possible, further reducing the importance of the base image OS.
These images may receive base image OS upgrades in the middle of major or minor version releases, provided this doesn't cause any breaking changes to the image's functionality.
As such, these images may have tags that don't specify an OS version, or in some cases won't even refer to an OS at all.

Windows tags

For Windows, amd64 is the only architecture supported and is excluded from the tag name.

Tag Lifecycle

Each tag will be supported for the lifetime of the .NET and OS version referenced by the tag, unless further restricted according to platform support policy.

When a .NET version or an OS version reaches end of support (sometimes referred to as end-of-life or EOL), its tags will no longer be supported.
Unsupported tags and images can still be pulled, but they will no longer receive updates for any reason.

See "Is your image built from a supported .NET tag?" for details on how to tell if a specific tag is supported.

Policy Changes

In the event that a change is needed to the tagging patterns used, all tags for the previous pattern will continue to be supported for their original lifetime. They will however be removed from the documentation. Announcements will be posted when any tagging policy changes are made.


Documentation Ubuntu Chiseled (`documentation/ubuntu-chiseled.md`)

Ubuntu Chiseled + .NET

What is Ubuntu Chiseled?

.NET's Ubuntu Chiseled images are a type of distroless container image that contain only the minimal set of packages .NET needs, with everything else removed.
These images offer dramatically smaller deployment sizes and attack surface compared to our "full" Ubuntu images that are based on the Ubuntu base images. This is achieved through the following features:

  • Minimal set of packages required to run a .NET application
  • Non-root user by default
  • No package manager
  • No shell
  • 10.0-noble-chiseled
    • docker pull mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled
    • docker pull mcr.microsoft.com/dotnet/runtime:10.0-noble-chiseled
    • docker pull mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled
  • 8.0-noble-chiseled
    • docker pull mcr.microsoft.com/dotnet/aspnet:8.0-noble-chiseled
    • docker pull mcr.microsoft.com/dotnet/runtime:8.0-noble-chiseled
    • docker pull mcr.microsoft.com/dotnet/runtime-deps:8.0-noble-chiseled

We’re not offering a chiseled SDK image as there wasn't a strong need for one, and a chiseled SDK image could be hard to use for some scenarios.
You can continue to use the existing full Ubuntu SDK images to build your apps to run on Chiseled.
If you have a compelling use case for a distroless SDK image, please leave a comment on this issue and we’ll be happy to reconsider.

How do I use Ubuntu Chiseled .NET images?

Please see our sample Dockerfiles for examples on how to use Ubuntu Chiseled .NET images:

If your app's Dockerfile doesn't depend on any shell scripts for setup, Ubuntu Chiseled images could be a drop-in replacement for our full Ubuntu or Debian images.

How do I install additional packages on Chiseled images?

[!IMPORTANT]
Installing additional packages requires the presence of the Chisel manifest, which is currently only available in .NET 10+ images.

Chisel is built on the idea of package slices.
Slices are basically subsets of packages, with their own content and set of dependencies to other internal and external slices.
Chisel relies on a database of slices that are indexed per Ubuntu release.
You can only install packages that are available as slices.

First, acquire chisel and chisel-wrapper.
chisel-wrapper (from rocks-toolbox) is used to generate a dpkg status file documenting which packages are installed, which is used by many vulnerability scanning tools.

dockerfile
FROM mcr.microsoft.com/dotnet/nightly/sdk:10.0-noble AS chisel

# Docker build arg documentation: https://docs.docker.com/build/building/variables/
# Find the latest chisel releases: https://github.com/canonical/chisel/releases
ARG CHISEL_VERSION="v1.X.X"
# Find the latest chisel-wrapper releases: https://github.com/canonical/rocks-toolbox/releases
ARG CHISEL_WRAPPER_VERSION="v1.X.X"

# Install chisel's dependencies
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        file \
    && rm -rf /var/lib/apt/lists/*

# Install chisel and chisel-wrapper
RUN chisel_url=https://github.com/canonical/chisel/releases/download/${CHISEL_VERSION}/chisel_${CHISEL_VERSION}_linux_amd64.tar.gz \
    && curl -fSLOJ ${chisel_url} \
    && curl -fSL ${chisel_url}.sha384 | sha384sum -c - \
    && tar -xzf chisel_${CHISEL_VERSION}_linux_amd64.tar.gz -C /usr/bin/ chisel \
    && curl -fSL --output /usr/bin/chisel-wrapper https://raw.githubusercontent.com/canonical/rocks-toolbox/${CHISEL_WRAPPER_VERSION}/chisel-wrapper \
    && chmod 755 /usr/bin/chisel-wrapper

Then, copy over the filesystem from your desired base image and use chisel and chisel-wrapper to install additional slices.
See canonical/chisel-releases for available slices.

dockerfile
COPY --from=mcr.microsoft.com/dotnet/nightly/runtime-deps:10.0-noble-chiseled / /rootfs/

RUN chisel-wrapper --generate-dpkg-status /rootfs/var/lib/dpkg/status -- \
        --release ubuntu-24.04 --root /rootfs/ \
            libicu74_libs \
            tzdata-legacy_zoneinfo \
            tzdata_zoneinfo

Finally, copy the new root filesystem into a scratch base image for your final (runtime) layer.

dockerfile
FROM scratch AS final
COPY --link --from=chisel /rootfs /
WORKDIR /app
COPY --link --from=chisel /app .
# user as defined in .NET base image
USER app
ENTRYPOINT ["./app"]

Copying the entire filesystem into the final FROM scratch layer results in a perfectly space- and layer-efficient installation of the additional slices.
However, it's important to note that this pattern does not allow any layer sharing with your chosen .NET base image - the first COPY instruction in the final stage is essentially creating a completely new base image layer.
Keep this in mind when creating your Dockerfiles - for example, you may find it beneficial to share layers between multiple .NET images running in the same Kubernetes cluster.


Documentation Vulnerability Reporting (`documentation/vulnerability-reporting.md`)

Container Vulnerability Workflow

This document is intended to help guide you to the appropriate course of action when encountering reported vulnerabilities in the .NET container images.
We rebuild images on a regular basis in response to CVEs and base image updates according to our Image Update Policy.
Before logging an issue, please review this document and follow the recommended steps.

Prerequisites:

  • Dockerfile of the container image being reported upon
    • Name of the .NET image the Dockerfile is based on (e.g. mcr.microsoft.com/dotnet/aspnet:10.0)
  • Architecture of the container image being reported upon (e.g. amd64)
  • Docker
  • ORAS CLI
  • jq, PowerShell, or Windows PowerShell
  • Whether the container image is a Linux or Windows image
  • If you're using Docker Desktop for Windows and investigating a vulnerability in a Linux image, change your settings to target Linux containers.

The flowchart below describes a series of questions to help determine the state of the image being reported upon.
By answering each question, it will eventually direct you to the action to help resolve your vulnerabilities.
The flowchart is meant to be a visual representation of the workflow.
Below the flowchart are the details of each question and the actions to take.
Each question in the details section includes a "Response" header that contains links to help guide you through the workflow based on your responses.
You may begin with the first question at "A. Is your scanner targeting the latest version of your image?"

text
/* Detailed source-code truncated for AI context efficiency. */

Flowchart Details

A. Is your scanner targeting the latest version of your image?

There's no need to spend time analyzing vulnerabilities if the scan results are not based on the latest image.
Each build of an image can potentially bring in newly released package versions which have fixes for the vulnerabilities.

Most image scanning software will output the image digest of the image that is being scanned in its report or logs.
Be sure that that digest represents the latest version of your image.
To determine what the latest digest of your image is run the script below.

macOS/Linux

Requires PowerShell to be installed.

shell
myImage="<insert-my-image-tag>"
curl -sSL https://raw.githubusercontent.com/dotnet/dotnet-docker/main/documentation/scripts/resolve-image-digest.ps1 | pwsh /dev/stdin $myImage

Windows

powershell
$myImage="<insert-my-image-tag>"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://raw.githubusercontent.com/dotnet/dotnet-docker/main/documentation/scripts/resolve-image-digest.ps1'))) $myImage

Resolving multi-arch tags

If the script above returns an error indicating it's unable to resolve a multi-arch tag, you'll need to provide extra parameters to the script.
The following scripts indicate how to do that.

If you're targeting Windows containers, you'll need to determine the Windows build number of the image you're targeting.
To do this, find the Windows version number that is listed on the Windows Docker Hub page.
For example, if you're targeting Windows Server 2022 LTSC, look for the ltsc2022 tag and find its corresponding build number in the OS Version column.
You only need the first three parts of the version number.
In the case of Windows Server 2022, it's 10.0.20348.
You can use a wildcard to indicate the rest of the version number.

macOS/Linux

Requires PowerShell to be installed.

shell
myImage="<insert-my-image-tag>"
os="<linux|windows>" # choose "linux" or "windows"
arch="<amd64|arm|arm64>" # choose the architecture of your image
osVersion="<windows-version>" # Only used for Windows containers (e.g. "10.0.20348.*"). For Linux, leave this blank.
curl -sSL https://raw.githubusercontent.com/dotnet/dotnet-docker/main/documentation/scripts/resolve-image-digest.ps1 | pwsh /dev/stdin $myImage -Os $os -Architecture $arch -OsVersion $osVersion

Windows

powershell
$myImage="<insert-my-image-tag>"
$os="<linux|windows>" # choose "linux" or "windows"
$arch="<amd64|arm|arm64>" # choose the architecture of your image
osVersion="<windows-version>" # Only used for Windows containers (e.g. "10.0.20348.*"). For Linux, leave this blank.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://raw.githubusercontent.com/dotnet/dotnet-docker/main/documentation/scripts/resolve-image-digest.ps1'))) $myImage -Os $os -Architecture $arch -OsVersion $osVersion

Response

B. Scan your latest image

Rerun the scan of your image using your scanning tool. Ensure you get the latest version of the image by running docker pull

C. Is your image built from a supported .NET tag?

When .NET drops support for an image tag, it means it will no longer be updated, even when there is a new base OS image available.
This means that vulnerabilities will be reported for that image over time if it continues to be used.
Our supported tag policy provides detailed information about when these tags are no longer supported.

The simple rule to follow: only the tags shown in our full tag listings are supported:

Image Lifecycle Annotations

When .NET images or tags go out of support, a lifecycle annotation is added to the registry indicating that the image has reached end-of-life.
The following command will output the latest lifecycle artifact associated with a .NET image.

macOS/Linux (sh)

sh
dotnetImage="<insert-dotnet-image-tag>" # example EOL image: mcr.microsoft.com/dotnet/aspnet:6.0
oras discover \
  --format "json" \
  --artifact-type "application/vnd.microsoft.artifact.lifecycle" \
  $dotnetImage \
    | jq '.manifests | sort_by(.annotations."org.opencontainers.image.created") | reverse | first'

PowerShell (any platform)

pwsh
$dotnetImage = "<insert-dotnet-image-tag>" # example EOL image: mcr.microsoft.com/dotnet/aspnet:6.0
$artifacts = oras discover --format "json" --artifact-type "application/vnd.microsoft.artifact.lifecycle" $dotnetImage | ConvertFrom-Json
$artifacts.manifests | Sort-Object { $_.annotations.'org.opencontainers.image.created' } -Descending | Select-Object -First 1

Output

If the image has no lifecycle manifest attached, then there will be no output.
This means the image is supported and a newer image has not been released yet.
However, if the image has a lifecycle manifest attached and it contains an end-of-life date, the image is unsupported.

For example:

json
{
  "reference": "mcr.microsoft.com/dotnet/aspnet@sha256:6e81022bab60ff3ce884cc118115d5394341d5ddf95dede357d2e17ea8920074",
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "digest": "sha256:6e81022bab60ff3ce884cc118115d5394341d5ddf95dede357d2e17ea8920074",
  "size": 788,
  "annotations": {
    "org.opencontainers.image.created": "2025-01-14T05:32:10Z",
    "vnd.microsoft.artifact.lifecycle.end-of-life.date": "2024-11-12"
  },
  "artifactType": "application/vnd.microsoft.artifact.lifecycle"
}

The lifecycle artifact above indicates that the image mcr.microsoft.com/dotnet/aspnet@sha256:6e81022b... reached end of life on 2024-11-12.

Response

D. Update base image tag to supported image

Switching to a supported base image tag requires knowing the specific tag usage:

  • If you're using an out-of-support .NET version, you must upgrade your project and Dockerfiles to target a supported version.
    The .NET Support Policy page contains the latest information on supported .NET versions.
  • If you're using a patch version tag which which has been replaced by a newer patch version, you must switch to the latest patch version.
  • If you're using a version-specific Alpine Linux tag for a version of Alpine that is no longer being maintained via .NET container images, you must move to the latest version. For example, 10.0-alpine3.22 => 10.0-alpine3.23
  • If you're using a tag for a specific Windows version that is no longer supported, you must move to a supported version of Windows.

See our supported tag policy for more information.

E. Is your image built from the latest .NET image?

The Image Update Policy describes exactly when we update .NET images.
A given .NET tag can be updated multiple times over its a lifetime, pointing to a new image each time.
For example, the 10.0 tag will point to a new image each time a new patch version is released.
A patch-version tag (such as 10.0.1) may also be updated due to base image updates or if a security fix is required for a Linux package.
It's important to realize that these updates likely contain security fixes.
Even though you may be using a supported tag, it's possible you're using an older image that has since been replaced by a newer version with that same tag.
Only the latest image for a given tag is supported.
Run one of the scripts below to determine whether your image is built on the latest image for that tag. Provide the image digest of your image and tag of the .NET image (e.g. mcr.microsoft.com/dotnet/aspnet:10.0) as input.

A result of True means that your image is built from the latest .NET image.
A result of False means it is not built on the latest .NET image and must be updated.

macOS/Linux

Requires PowerShell to be installed.

shell
myImage="<insert-my-image-digest>" # format: <registry>/<repo>@sha256:<digest>
dotnetBaseImage="<insert-dotnet-image-tag>" # example: mcr.microsoft.com/dotnet/aspnet:10.0
curl -sSL https://raw.githubusercontent.com/dotnet/dotnet-docker/main/documentation/scripts/check-latest-base.ps1 | pwsh /dev/stdin $myImage $dotnetBaseImage

Windows

powershell
$myImage="<insert-my-image-digest>" # format: <registry>/<repo>@sha256:<digest>
$dotnetBaseImage="<insert-dotnet-image-tag>" # example: mcr.microsoft.com/dotnet/aspnet:10.0
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://raw.githubusercontent.com/dotnet/dotnet-docker/main/documentation/scripts/check-latest-base.ps1'))) $myImage $dotnetBaseImage

Response

F. Rebuild your image to get the latest base

Since your image is out of date with the latest version of the .NET image, rebuilding it will ensure it's up-to-date.
Be sure that your workflow explicitly pulls the .NET image tag rather than using a potentially cached version of it.
For example:

  • CI workflows that use the Azure DevOps Docker task should set the --pull option as an argument:

    yaml
    arguments: "--pull"
  • CI workflows that use the GitHub Actions Docker Build and Push Action should enable the pull option:

    yaml
    pull: true
  • For other build systems, read the documentation to determine how to enable pulling of images. It should map to the --pull option of the docker build command.

G. Is .NET the source of the vulnerability?

To determine whether .NET binaries are the source of the vulnerability, look at the title and description of the vulnerability.
If it contains ".NET", "ASP.NET", or something related to .NET, assume this is a .NET vulnerability.

Response

H. Which OS platform is this for?

See How can I determine whether my image is based on Linux or Windows?.

Response

I. Log an issue at microsoft/Windows-Containers

This vulnerability can be directly addressed by the Windows team.
Log an issue at microsoft/Windows-Containers and provide the vulnerability ID and the image tag you've identified as being affected.

J. Is a fix available for the package from the Linux distro?

These steps will help you determine whether a fix is available for the package and what the actual name of the package is.

The vulnerability scanner or the Linux distro's vulnerability page will provide the name of the vulnerable package.

In some cases, this name doesn't match the name of the package that you would see installed in the image.
For example, CVE-2022-42898 indicates the name of the package is krb5.
But this indicates the name of the source package, not the name of the binary package that would be installed from the distro's package manager.
In the case of the .NET images that are based on Debian, the relevant binary package name is libkrb5-3.
In addition, there may be multiple binary packages associated with a single source package.
And there may be multiple source packages affected by a single vulnerability.

Because of the sheer number of packages that may need to be exhaustively checked and the overall complexity to determine the name of the relevant vulnerable packages, we'll take a more pragmatic approach instead.

The commands below can be used to return a list of upgradable packages that contain the name of the reported vulnerable package.
Again, this name may or may not be the name of the actual package that is installed in the image.
But we're going to use it to narrow down the list of packages to be checked.

Debian/Ubuntu Image

shell
image="<my-image-digest>"
packageName="<vulnerable-package-name>"
docker pull $image
docker run --rm --entrypoint /bin/sh $image -c "apt update > /dev/null && apt list --upgradable | grep $packageName"

Alpine Image

shell
image="<my-image-digest>"
packageName="<vulnerable-package-name>"
docker pull $image
docker run --rm --entrypoint /bin/sh $image -c "apk update > /dev/null && apk list -u | grep $packageName"

Azure Linux Image

shell
image="<my-image-digest>"
packageName="<vulnerable-package-name>"
docker pull $image
docker run --rm --entrypoint /bin/sh $image -c "tdnf check-update | grep $packageName"

For Debian/Ubuntu, the package name can be parsed from the output line by taking everything before the first /. For example, libkrb5-3/stable-security,now 1.18.3-6+deb11u3 amd64 [installed,automatic] would be parsed as libkrb5-3 for the package name. For Alpine, the package name can be parsed from the output line by taking everything before the first - that precedes a digit. For example, krb5-libs-1.20.1-r0 x86_64 {krb5} (MIT) would be parsed as krb5-libs for the package name.

If the output of the commands above does not output any packages, it means there is no package upgrade available.
For the purposes of this workflow, it's not necessary to determine the name of the package that is installed in the image.

However, if the output of the commands above does output packages, it means there is an upgrade available for them and it's now necessary to determine which are relevant to the vulnerability.

In the simple case, there will be a package which exactly matches the name of the vulnerable package you provided.
In that case, that's the package name to use for the rest of the workflow.

If there is no exact match, you'll need to look up the distro's information on the vulnerability to determine the relevant package names and their versions.
Cross-reference that information with the output of the commands above to determine which packages are relevant to the vulnerability.
Use the version numbers provided by the vulnerability documentation to match upon.
There may be multiple packages installed in the image that are relevant to the vulnerability.
You'll want to track them all in the rest of this workflow.
It's possible that packages are listed as upgradable but not actually relevant to the vulnerability.
Those are unrelated packages that happen to contain the same name as the vulnerable package and they can be disregarded.

Response

K. Wait for a fix to be available from the Linux distro

.NET is dependent on the Linux distribution maintainers to produce fixed versions of the packages that are contained in the .NET container images.
We can't take any action if a fixed version of the package isn't available from the package repository of the Linux distro version.
Sometimes fixes aren't ever made available because they are low severity or not applicable in container environments.
Questions on this matter should be directed to the relevant Linux distro.

L. Is the package in the Linux distro base image?

It's important to determine whether the Linux package in question is installed by the layers of the Linux distro base image rather than the layers of the .NET image.
You'll first need to determine the image tag of the Linux distro base image.
Then you can execute the script below to determine whether the package is installed by the Linux distro base image.

macOS/Linux

Requires PowerShell to be installed.

shell
packageName="<insert-package-name>"
imageName="<insert-linux-base-image-name>" # example: amd64/debian:bookworm-slim
curl -sSL https://raw.githubusercontent.com/dotnet/dotnet-docker/main/documentation/scripts/check-package-install.ps1 | pwsh /dev/stdin $packageName $imageName

Windows

powershell
$packageName="<insert-package-name>"
$imageName="<insert-linux-base-image-name>" # example: amd64/debian:bookworm-slim
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://raw.githubusercontent.com/dotnet/dotnet-docker/main/documentation/scripts/check-package-install.ps1'))) $packageName $imageName

Response

M. Upgrade packages or wait for an updated Linux distro base image

.NET is dependent on the Linux distribution maintainers to produce updated versions of the base images that are used by the .NET container images.
Since the vulnerable package is contained in the Linux distro base image, the .NET images won't be updated until a new version of the Linux base image is available that contains the fixed package.
See our image update policy for more details.

In the meantime, if you need to have the vulnerability addressed before that time, you can include a command in your Dockerfile to explicitly upgrade the package.
This will cause the package to be upgraded to the latest version available in the package repository of the Linux distro version.
This will also cause the image to be larger because the package will be installed twice - once in the Linux distro base image and once in your Dockerfile upgrade command.
Because of that, it is not recommended as a long-term solution.

Debian/Ubuntu

dockerfile
RUN apt-get update \
    && apt-get upgrade -y <package-name> \
    && rm -rf /var/lib/apt/lists/*

Alpine

dockerfile
RUN apk update \
    && apk upgrade <package-name>

Azure Linux

dockerfile
RUN tdnf install -y <package-name> \
    && tdnf clean all

N. Is the package installed by .NET?

Run the script below to determine whether the package is installed by one of the .NET image layers.
This script is only accurate if you've first determined that the package is not installed by the Linux distro base image so make sure you've followed that part of the workflow first.

macOS/Linux

Requires PowerShell to be installed.

shell
packageName="<insert-package-name>"
imageName="<insert-dotnet-image-name>" # example: mcr.microsoft.com/dotnet/aspnet:10.0
curl -sSL https://raw.githubusercontent.com/dotnet/dotnet-docker/main/documentation/scripts/check-package-install.ps1 | pwsh /dev/stdin $packageName $imageName

Windows

powershell
$packageName="<insert-package-name>"
$imageName="<insert-linux-base-image-name>" # example: mcr.microsoft.com/dotnet/aspnet:10.0
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://raw.githubusercontent.com/dotnet/dotnet-docker/main/documentation/scripts/check-package-install.ps1'))) $packageName $imageName

Response

O. Rebuild your image to get the latest package

Because the package is not installed by the Linux distro base image or .NET, it must be installed by your Dockerfile.
Open your Dockerfile to determine how the package is being installed.
If no package version is specified, then you should be able to rebuild your image to get the latest version of the package.
Otherwise, you'll need to update your Dockerfile to specify the latest version of the package before rebuilding.

P. Is it Critical severity?

The severity of a vulnerability is indicated by its Common Vulnerability Scoring System (CVSS) base score.
To find this score, search for the CVE at the CVE site.
The score will consist of both a number and a label.
Note whether the label indicates "Critical".

Response

Q. Wait for the next .NET servicing release

Given that the vulnerability does not have a critical severity, it will be addressed in the next .NET servicing release.
These occur on Update Tuesday (the second Tuesday of each month).

R. Log an issue at dotnet/dotnet-docker

This vulnerability can be directly addressed by the .NET team.
Our Image Update Policy defines when we will rebuild for vulnerabilities in .NET images.
If you find that we have missed a vulnerability, please file an issue at dotnet/dotnet-docker and provide the vulnerability ID and the .NET image tag you've identified as being affected.

Appendix

What is an image digest?

An image digest is a unique identifier for the image.
It allows you to retrieve that specific image and know that you're getting the same version as someone else that also used that same image digest.
Unlike image digests, image tags are mutable and cannot be relied upon for ensuring you're getting an exact version of the image every time.
An image tag can be updated multiple times over its lifetime to point to different image digests.

An image digest is a SHA256 value and has the form sha256:<value>.
In order to pull an image by its digest, you need to know its registry, repository name, and digest.
The digest is appended to the repository name, separated by an @ symbol: (e.g.
mcr.microsoft.com/dotnet/samples@sha256:1983d7d0dec846cb92e59fb01eaa3e7f3a65071ca29c7db4075edefcf20db47a).

How can I determine whether my image is based on Linux or Windows?

  1. If you're using Docker Desktop for Windows, change your settings to target Linux containers.

  2. Run the following command:

    shell
    docker pull <insert-my-image-digest>
  3. If the previous command pulled the image successfully, it is a Linux image.
    If you get the error "image operating system "windows" cannot be used on this platform", it is a Windows image.

How can I determine the tag of the Linux distro image that my image is based upon?

You'll first need to get the name of the .NET image (e.g. mcr.microsoft.com/dotnet/aspnet:10.0) from your Dockerfile.
Take the tag portion of that image name and search for that tag on the .NET Runtime Dependencies page within the appropriate "Linux <architecture> Tags" section.
Navigate to the Dockerfile link associated with that tag.
The first FROM line of the Dockerfile will have the name of the Linux distro base image (e.g. amd64/debian:bookworm-slim).

How can I determine the OS version of a Linux image?

Use the appropriate command below to determine the OS version of your image.
If you get an error like the following: unable to start container process: exec: "/bin/sh": stat /bin/sh: no such file or directory: unknown, then you're likely using a distroless image which requires a different command.

shell
docker run --rm <image-digest> cat /etc/os-release

The output will include a NAME field indicating the name of the distro (Debian, Ubuntu, Alpine, or Microsoft Azure Linux).
In the case of Debian and Ubuntu, the output will also include a VERSION_CODENAME field. Make note of this codename because the vulnerability pages on the distro's website refers to that codename instead of a version number.

Distroless Images

Since distroless images have no shell with which to run the commands above, you can use Syft to determine the the OS version using the following command:

macOS/Linux

shell
docker run --rm anchore/syft packages --output json -q <image-digest> | jq -r .distro

Windows

powershell
(docker run --rm anchore/syft packages --output json -q <image-digest> | ConvertFrom-Json).distro

How can I determine what Linux package version is installed?

Use the appropriate command below to determine the package version installed in your image.
If you get an error like the following: unable to start container process: exec: "/bin/sh": stat /bin/sh: no such file or directory: unknown, then you're likely using a distroless image which requires a different command.

Debian/Ubuntu Images

shell
docker run --rm --entrypoint /bin/sh <my-image-digest> apt list <package-name>

Alpine Images

shell
docker run --rm --entrypoint /bin/sh <my-image-digest> apk list <package-name>

Azure Linux Images

shell
docker run --rm --entrypoint /bin/sh <my-image-digest> tdnf list installed <package-name>

Distroless Images

Since distroless images have no shell with which to run the commands above, you can use Syft to determine the package version using the following command:

macOS/Linux

shell
docker run --rm anchore/syft packages --output json -q <my-image-digest> | jq -rc ".artifacts[] | select(.name == \"<package-name>\") | .version"

Windows

powershell
((docker run --rm anchore/syft packages --output json -q <my-image-digest> | ConvertFrom-Json).artifacts | Where-Object { $_.name -eq "<package-name>" }).version

How can I view the Linux distro's information about a vulnerability?

Each Linux distribution maintains there own version of software components, made available in their package repositories.
Each version of the Linux distro also has its own package repository.
So it's possible that a fixed package may be available in one Linux distro version but not another.
It's necessary to check with the Linux distro security site to determine whether a fix is even available for the particular vulnerability.

Your image vulnerability scanner will output an identifer of the vulnerability. This will typically be a CVE number. (Debian also has its own Debian Security Advisory (DSA) numbers).

Search for the vulnerability ID on the relevant Linux distro's security site.
If you don't know which Linux distro is relevant for your image, look up the image tag and it will tell you its OS version in the tag listing table.
The following are links to the security sites for the Linux distros that we support including the URL format to get information on a specific CVE:

Within those pages the distro will indicate in which release the vulnerability was fixed or not fixed. Use the distro version/codename to determine which release is relevant.

It's important that you determine whether a fix is available for the specific version/release of the Linux distro.
For example, a package may have a vulnerability fixed for Ubuntu Noble but not yet available for Ubuntu Jammy.
While you could configure the Ubuntu Jammy container to install that package from the Ubuntu Noble package repository, that doesn't mean the package will actually be compatible.
Doing so is not a supported configuration from .NET's perspective and we will not take such an action with our images.


Documentation Scenarios Installing Dotnet (`documentation/scenarios/installing-dotnet.md`)

Installing .NET in a Dockerfile

There can be times where you need .NET installed on a base image that is unavailable amongst the set of official .NET Docker images, such as a different Linux distro version or a Windows Server Core image. In that case, you'll need to author your own Dockerfile which installs .NET. The snippets below describe how to do this.

There are two scenarios you should consider depending on how your Docker image is to be consumed: base images and application images. But before getting to that, let's first consider whether you actually need to go down this road.

Determine whether a different base image is needed

Before doing all the work of authoring and maintaining a Dockerfile that installs .NET, it's worthwhile to stop and thoroughly analyze whether you actually do need a different base image than those provided as part of the set of official .NET Docker images.

If there's a platform that you require that is available in its own Docker image, ask yourself whether it would be better to use that image and add .NET to it or would it be better to use the .NET image as the base and add the platform to it.

In some cases, you can work around the need for the .NET runtime by publishing your application as a self-contained app, in which case all of your app's dependencies are packaged with the app. This reduces the dependencies that need to be installed separately on the base image. Example Dockerfiles that demonstrate publishing a self-contained app are available in the ConsoleAppSelfContained sample.

Image Purposes

Base Images

If you're building a base image that is intended to be publicly consumed, you should strongly consider following the Docker guidelines for official images.
Those guidelines will help you create a high quality and maintainable Dockerfile that is easy for users to understand.

For general base images, it is recommended that you install .NET by binary archive (Linux/Windows) or package manager (Linux only). Both of those options can be used in a way that conforms to the Docker guidelines for official images.

Application Images

If you're building an image for a custom application that is not to be intended to be publicly consumed as a platform for other applications, you can get by with a simpler Dockerfile implementation compared to base images if you choose. Because the image is only intended for your own organization's purposes, the need for transparency in the Dockerfile is lessened. Convenience can trump clarity in this case.

For custom application images, it is recommended that you install .NET by package manager (Linux only) or dotnet-install script (Linux/Windows).

Installing .NET

Prerequisites

When installing .NET from a binary archive or the dotnet-install script, you need to make sure .NET's runtime dependencies are present in order for the installation to function properly.
You can find the full list of .NET dependencies for each Linux distro and .NET version here, including example commands for installing the packages.
You can also see the official .NET Dockerfiles for more package installation examples.

When installing from a package manager, the package manager will handle resolving dependencies for you.

Installing from a Binary Archive

When authoring your Dockerfiles, you can look to the official .NET Dockerfiles as a template for the install steps. There are several variations depending on the .NET version, OS type, and architecture being used.

Installing .NET Binaries

In the spirit of clarity, the Dockerfiles for the official .NET Docker images do not use a general purpose script for installing .NET. Rather, they explicitly provide each step of the installation process and reference the exact URL of the binary archive.

Example (Linux):

text
/* Detailed source-code truncated for AI context efficiency. */

Example (Windows):

text
/* Detailed source-code truncated for AI context efficiency. */

Servicing Maintenance

By having the version of .NET you're installing explicitly defined in the Dockerfile, as should be done for clarity reasons, it means the Dockerfile must be regularly maintained to account for servicing releases of .NET. There are two parts of the install steps that will need to updated in order to reference a new release:

  • .NET download URL (e.g. ENV DOTNET_URL=https://download.visualstudio.microsoft.com/...)
  • SHA value (e.g. dotnet_sha512='d4d67df5ff5f6dde0d865a6e87559955bd57429df396cf7d05fe77f09e6220c67dc5e66439b1801ca4d301a62f81f666122bf4b623b31a46b861677dcafc62a4')

You can track these values by making use of the information contained in the releases.json of the relevant release. For example, the releases.json for 10.0 contains all the metadata for the 10.0 releases including download links of the binary archives as well as their hash values. The release information is described on the main release notes page.

Installing from a Linux Package Manager

For Linux, you may prefer to use your Linux distro's package manager to install .NET rather than directly from a binary archive.
See the Install .NET on Linux guidance on how to install .NET from the various distro package managers.

Using a package manager allows for easier maintenance since you only have to reference the major/minor version of the release and you'll get servicing releases "for free". This is true as long as you are mindful of Docker's caching functionality. You may need to build with the --no-cache option to force the build to re-execute the commands that install .NET in order to get an updated servicing release. Alternatively, you can specify the full version (major/minor/build) and increment that with each service release.

Another benefit of installing via a package manager is that all of .NET's dependencies are installed by the package manager for you. You don't have to manually list the prerequisites to be installed like you do when using binary archives or the dotnet-install script.

Native Distro Packages vs. `packages.microsoft.com`

.NET is available in the default package manager feeds for some Linux distros as well as from the Microsoft package repository (packages.microsoft.com).
However, you should only use one or the other to install .NET.

Starting with .NET 9, we have stopped publishing .NET packages on packages.microsoft.com for distros that publish their own .NET packages.
For more details please see .NET 9 Package Publishing announcement.

.NET is supported by Microsoft when downloaded from a Microsoft source. Best effort support is offered from Microsoft when downloaded from elsewhere. You can open issues at dotnet/core if you run into problems.

Native Package Installation Example

Below is an example Dockerfile that installs .NET on Ubuntu directly from the default Ubuntu package feeds.
This distribution of .NET is built and supported by the Ubuntu package maintainers.
Package installation commands for other distros are documented in Install .NET on Linux.

dockerfile
FROM ubuntu:noble
RUN apt-get update \
    && apt-get install -y dotnet-sdk-8.0 \
    && rm -rf /var/lib/apt/lists/*

[!IMPORTANT]
.NET packages in native distro feeds are typically limited to the .1xx SDK band, while packages.microsoft.com packages are not. Users that want to use the latest feature band builds (on distros without packages.microsoft.com packages available) must install those builds manually via binary archive or dotnet-install script.

Microsoft Package Feeds Installation Example

Instructions for setting up the Microsoft package feed vary between Linux distros.
Please refer to Install .NET on Linux for detailed instructions for each supported distro.

Keep in mind the Dockerfile best practices when porting the installation commands to your Dockerfile - in particular, installing or using sudo in your Dockerfile is not neccesary since the default user in most base container images is root.

Here is an example Dockerfile using Debian:

dockerfile
FROM debian:bookworm-slim
RUN apt-get update \
    # Install prerequisites
    && apt-get install -y --no-install-recommends \
       wget \
       ca-certificates \
    \
    # Install Microsoft package feed
    && wget -q https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb -O packages-microsoft-prod.deb \
    && dpkg -i packages-microsoft-prod.deb \
    && rm packages-microsoft-prod.deb \
    \
    # Install .NET
    && apt-get update \
    && apt-get install -y --no-install-recommends \
        dotnet-runtime-8.0 \
    \
    # Cleanup
    && rm -rf /var/lib/apt/lists/*

Installing from dotnet-install script

A set of installation scripts are provided to conveniently install .NET on Linux with Bash or Windows with PowerShell. These scripts can be thought of as a happy medium between the two previously mentioned approaches (binary archive link and package manager). They fill a gap on systems where the desired .NET release is not available through a package manager and you don't want to deal with the cost of maintaining a direct link to a binary package. With the installation script, you have flexibility in specifying which version gets installed. You can install a specific version such as 10.0.0, the latest of a release channel such as the latest 10.0 patch, etc.

In addition to installing .NET, you'll also need to ensure that the prerequisites are installed. The .NET Dockerfiles also demonstrate how that can be done.

Example (Linux):

dockerfile
FROM ubuntu:noble

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        ca-certificates \
        curl \
        \
        # .NET dependencies
        libc6 \
        libgcc-s1 \
        libicu74 \
        libssl3t64 \
        libstdc++6 \
        tzdata \
    && rm -rf /var/lib/apt/lists/*

RUN curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin -Channel 10.0 -Runtime dotnet -InstallDir /usr/share/dotnet \
    && ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet

Example (Windows):

dockerfile
# escape=`

FROM mcr.microsoft.com/windows/servercore:ltsc2025
RUN powershell -Command `
        $ErrorActionPreference = 'Stop'; `
        $ProgressPreference = 'SilentlyContinue'; `
        Invoke-WebRequest `
            -UseBasicParsing `
            -Uri https://dot.net/v1/dotnet-install.ps1 `
            -OutFile dotnet-install.ps1; `
        ./dotnet-install.ps1 `
            -InstallDir '/Program Files/dotnet' `
            -Channel 10.0 `
            -Runtime dotnet; `
        Remove-Item -Force dotnet-install.ps1 `
    && setx /M PATH "%PATH%;C:\Program Files\dotnet"

Documentation Scenarios Nuget Credentials (`documentation/scenarios/nuget-credentials.md`)

Managing NuGet Credentials in Docker Scenarios

There are often cases when your Dockerfile needs to build a .NET project that make use of a NuGet package located in a private feed. How do you properly provide your Docker build access to that private feed in a secure manner? The options below provide recommended approaches you can take.

There are two aspects to be aware of when it comes to protecting credentials:

  1. Exposing credentials in an image that is published to a registry. This is the most obvious scenario. You don't want consumers of your image to have access to data they should not have. The options listed below demonstrate how to prevent this from happening.
  2. Exposing secrets locally on the Docker host machine. This isn't necessarily an issue; it depends on your build processes. Just be aware that while your published image may not be exposing sensitive data, there can be sensitive data stored on the Docker host machine that you may want to clean up depending on how the machine is managed in your build workflow. For example, when using Azure DevOps pipelines with a hosted agent, there's no need to clean up data because the virtual machine of the build agent is automatically deleted at the end of the build. But if you manage your own agent, you're in charge of cleaning any leftover state should it be necessary. Because some environments log command lines, it is a security best-practice to avoid putting secrets on the command line. To avoid this with docker build, store the secret in an enviornment variable with the name you want the build argument to have. Then, on the docker build command line, refer to just the name of the environment variable without the = or the value.

Use a multi-stage build to protect `nuget.config` passed by build context

By using multi-stage builds, you can use the build context to pass a set of files to the Docker build that are used just for building the application and avoid having all of those files end up in the final image. If your nuget.config file contains credentials and is already stored on the Docker host machine, then you may want to consider passing it via the build context for the Docker build (also see Passing secrets by file with Docker BuildKit for a related pattern). Be careful when storing credentials on disk. Make sure that the machine and file are properly secured.

Even though using a multi-stage build is a good technique to help avoid exposing credentials in the final image, it should be known that the intermediate layers produced by the build can still expose those secrets locally on the Docker host machine.

Here's an example nuget.config file containing the credentials:

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <packageSources>
    <add key="public" value="https://api.nuget.org/v3/index.json" />
    <add key="customfeed" value="https://mycustomfeedurl"  />
  </packageSources>

  <packageSourceCredentials>
    <customfeed>
      <add key="Username" value="<username>" />
      <add key="ClearTextPassword" value="<password>" />
    </customfeed>
  </packageSourceCredentials>
</configuration>

In the Dockerfile, the nuget.config file is copied via the build context and stored in the file system only for the build stage. In the runtime stage, only the binaries of the application are copied, not the nuget.config file so the credentials are not exposed in the final image.

dockerfile
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /source

# Copy project file and restore as distinct layers
COPY --link *.csproj .
COPY --link ./nuget.config .
RUN dotnet restore

# Copy source code and publish app
COPY --link . .
RUN dotnet publish --no-restore -o /app

# Runtime stage
FROM mcr.microsoft.com/dotnet/runtime:9.0
WORKDIR /app
COPY --link --from=build /app .
USER $APP_UID
ENTRYPOINT ["./ConsoleApp"]

Using environment variables in `nuget.config`

If you do not store your credentials in the nuget.config file and instead retrieve them in some manner through a build process, you can use this environment variable with multi-stage build pattern. This pattern involves using the ARG instruction to pass in the NuGet feed credentials to a docker build command. Care must be taken here to ensure that the ARG instructions are only used for intermediate stages and never the final stage to avoid the leaking the credentials in the final image.

Even though using a multi-stage build is a good technique to help avoid exposing credentials in the final image, it should be known that the intermediate layers produced by the build can still expose those secrets locally on the Docker host machine.

For this pattern, we're going to rely on a nuget.config file that makes use of NuGet's support for environment variables:

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <packageSources>
    <add key="public" value="https://api.nuget.org/v3/index.json" />
    <add key="customfeed" value="https://mycustomfeedurl"  />
  </packageSources>

  <packageSourceCredentials>
    <customfeed>
      <add key="Username" value="%Nuget_CustomFeedUserName%" />
      <add key="ClearTextPassword" value="%Nuget_CustomFeedPassword%" />
    </customfeed>
  </packageSourceCredentials>
</configuration>

When making use of the ARG instruction, the name of the argument is made available as an environment variable during the scope of the docker build. This aligns nicely with our use of environment variables in the nuget.config file.

In the Dockerfile below, there are two stages: build and runtime. The build stage is responsible for building the application project. It defines two ARG values which match the names of the environment variables used in the the nuget.config file. But the build stage is not the final stage of the Dockerfile so the --build-arg values that are passed to the docker build command do not get exposed in the resulting image. All the final stage is really doing is copying the built output of the application from the build stage since that's all that's needed to run the application.

dockerfile
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build

ARG Nuget_CustomFeedUserName
ARG Nuget_CustomFeedPassword

WORKDIR /source

# Copy project file and restore as distinct layers
COPY --link *.csproj .
COPY --link ./nuget.config .
RUN dotnet restore

# Copy source code and publish app
COPY --link . .
RUN dotnet publish --no-restore -o /app

# Runtime stage
FROM mcr.microsoft.com/dotnet/runtime:9.0
WORKDIR /app
COPY --link --from=build /app .
USER $APP_UID
ENTRYPOINT ["./ConsoleApp"]

Before running docker build, first populate the Nuget_CustomFeedUserName and Nuget_CustomFeedPassword environment variables with appropriate secrets. Then, this Dockerfile would be built using this command:

bash
docker build --build-arg Nuget_CustomFeedUserName --build-arg Nuget_CustomFeedPassword .

Passing the username and password values to the docker build command in this manner can be useful in automated scenarios when those values are stored as environment variables on the Docker host machine or can be retrieved from an external secrets storage location and passed to the docker build command.

Using the Azure Artifact Credential Provider

NuGet has support for custom credential providers that provide a mechanism to authenticate to feeds both interactively and non-interactively. One such implementation of this is Azure Artifact Credential Provider which provides a mechanism to acquire credentials for restoring NuGet packages from Azure Artifacts feeds. In unattended build agent scenarios, like building Docker images, a VSS_NUGET_EXTERNAL_FEED_ENDPOINTS environment variable is used to supply an access token.

If you're using Azure Artifacts for your private NuGet feeds, this scenario is for you since it provides a compatible experience between interactive developer machines and automated scenarios like Docker image builds. This allows you to use the same nuget.config file in either scenario. In this pattern, we'll use the environment variables and multi-stage build along with Credential Provider to pass the credentials for a private NuGet feed.

Even though using a multi-stage build is a good technique to help avoid exposing credentials in the final image, it should be known that the intermediate layers produced by the build can still expose those secrets locally on the Docker host machine.

The VSS_NUGET_EXTERNAL_FEED_ENDPOINTS environment variable is a well-known variable that is meant to be an extension of the nuget.config file, allowing both to be used together where VSS_NUGET_EXTERNAL_FEED_ENDPOINTS simply supplies the access token. The nuget.config file used here does not define any packageSourceCredentials element for customfeed:

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <packageSources>
    <add key="public" value="https://api.nuget.org/v3/index.json" />
    <add key="customfeed" value="https://fabrikam.pkgs.visualstudio.com/_packaging/MyGreatFeed/nuget/v3/index.json"  />
  </packageSources>
</configuration>

Instead, the credentials for customfeed are defined in the Dockerfile by making use of an ARG for the access token:

Linux

dockerfile
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG FEED_ACCESSTOKEN
WORKDIR /source

RUN curl -L https://raw.githubusercontent.com/Microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh  | sh

# Copy project file and restore as distinct layers
COPY --link *.csproj .
COPY --link ./nuget.config .
ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS="{\"endpointCredentials\": [{\"endpoint\":\"https://fabrikam.pkgs.visualstudio.com/_packaging/MyGreatFeed/nuget/v3/index.json\", \"username\":\"docker\", \"password\":\"${FEED_ACCESSTOKEN}\"}]}"
RUN dotnet restore

# Copy source code and publish app
COPY --link . .
RUN dotnet publish --no-restore -o /app

# Runtime stage
FROM mcr.microsoft.com/dotnet/runtime:9.0
WORKDIR /app
COPY --link --from=build /app .
USER $APP_UID
ENTRYPOINT ["./ConsoleApp"]

Windows

dockerfile
FROM mcr.microsoft.com/dotnet/sdk:9.0-nanoserver-ltsc2022 AS build
WORKDIR /source

SHELL ["pwsh", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]

# Install the cred provider
# By default, Nano Server runs as ContainerUser. In order to install the cred provider as ContainerUser, an explicit temp directory must be created
WORKDIR /temp
ENV TMP=C:\temp
RUN Invoke-WebRequest https://aka.ms/install-artifacts-credprovider.ps1 -OutFile installcredprovider.ps1; `
    .\installcredprovider.ps1; `
    del installcredprovider.ps1

# Copy project file and restore as distinct layers
COPY --link *.csproj .
COPY --link nuget.config .
ARG FEED_ACCESSTOKEN
ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS="{`"endpointCredentials`": [{`"endpoint`":`"https://fabrikam.pkgs.visualstudio.com/_packaging/MyGreatFeed/nuget/v3/index.json`", `"username`":`"docker`", `"password`":`"${FEED_ACCESSTOKEN}`"}]}"
RUN dotnet restore

# Copy source code and publish app
COPY --link . .
RUN dotnet publish --no-restore -o /app

# Runtime stage
FROM mcr.microsoft.com/dotnet/runtime:9.0-nanoserver-ltsc2022
WORKDIR /app
COPY --link --from=build /app .
ENTRYPOINT ["ConsoleApp"]

[!NOTE]
A script is called to install the Credential Provider. When dotnet restore is run, the Credential Provider is invoked to resolve the credentials and it retrieves them from the VSS_NUGET_EXTERNAL_FEED_ENDPOINTS environment variable.

Before running docker build, first populate the FEED_ACCESSTOKEN environment variable with a personal access token. Then, this Dockerfile would be built using this command:

bash
docker build --build-arg FEED_ACCESSTOKEN .

Passing the access token to the docker build command in this manner can be useful in automated scenarios when that value is stored as an environment variable on the Docker host machine or can be retrieved from an external secrets storage location and passed to the docker build command.

Azure Pipelines

In Azure Pipelines, the NuGetAuthenticate task can be used to obtain the VSS access token required for the feed. It creates two environment variables (VSS_NUGET_URI_PREFIXES and VSS_NUGET_ACCESSTOKEN) that can be used instead of a PAT (Personal Access Token).

yaml
steps:
    - task: NuGetAuthenticate@1
      displayName: 'Authenticate to NuGet'

    - task: Docker@2
      displayName: 'build container'
      inputs:
        command: 'build'
        dockerfile: '**/Dockerfile'
        arguments: '--build-arg FEED_ACCESSTOKEN=$(VSS_NUGET_ACCESSTOKEN)'

Credential Provider Troubleshooting

If you are having authentication issues, try adding a command to the Dockerfile to print out the value of the VSS_NUGET_EXTERNAL_FEED_ENDPOINTS environment variable. This will help you to ensure it is set and formatted correctly.

Example (Linux):

dockerfile
ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS="{`"endpointCredentials`": [{`"endpoint`":`"https://fabrikam.pkgs.visualstudio.com/_packaging/MyGreatFeed/nuget/v3/index.json`", `"username`":`"docker`", `"password`":`"${FEED_ACCESSTOKEN}`"}]}"
RUN echo $VSS_NUGET_EXTERNAL_FEED_ENDPOINTS

Example (Windows PowerShell):

dockerfile
ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS="{`"endpointCredentials`": [{`"endpoint`":`"https://fabrikam.pkgs.visualstudio.com/_packaging/MyGreatFeed/nuget/v3/index.json`", `"username`":`"docker`", `"password`":`"${FEED_ACCESSTOKEN}`"}]}"
RUN echo $Env:VSS_NUGET_EXTERNAL_FEED_ENDPOINTS

Example (Windows Cmd):

dockerfile
ENV VSS_NUGET_EXTERNAL_FEED_ENDPOINTS="{`"endpointCredentials`": [{`"endpoint`":`"https://fabrikam.pkgs.visualstudio.com/_packaging/MyGreatFeed/nuget/v3/index.json`", `"username`":`"docker`", `"password`":`"${FEED_ACCESSTOKEN}`"}]}"
RUN echo %VSS_NUGET_EXTERNAL_FEED_ENDPOINTS%

Passing secrets by file with BuildKit

Docker's BuildKit integration provides some enhanced functionality for passing secret information when running docker build. This avoids the use of ARG values which could potentially be exposed in the resulting image unless you're disciplined in defining your multi-stage build. But it also requires that the secret data be stored in a file on the Docker host machine during the docker build. If your nuget.config file contains credentials and is already stored on the Docker host machine, then this may be a good option for you. Be careful when storing credentials on disk. Make sure that the machine and file are properly secured.

Let's use the following sample nuget.config file which is stored on the Docker host machine:

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <packageSources>
    <add key="public" value="https://api.nuget.org/v3/index.json" />
    <add key="customfeed" value="https://mycustomfeedurl"  />
  </packageSources>

  <packageSourceCredentials>
    <customfeed>
      <add key="Username" value="<username>" />
      <add key="ClearTextPassword" value="<password>" />
    </customfeed>
  </packageSourceCredentials>
</configuration>

The Dockerfile references the nugetconfig secret with the --mount=type=secret syntax and access it at its default path of /run/secrets/nugetconfig:

dockerfile
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /source

# Copy project file and restore as distinct layers
COPY --link *.csproj .
RUN --mount=type=secret,id=nugetconfig \
    dotnet restore --configfile /run/secrets/nugetconfig

# Copy source code and publish app
COPY --link . .
RUN dotnet publish --no-restore -o /app

# Runtime stage
FROM mcr.microsoft.com/dotnet/runtime:9.0
WORKDIR /app
COPY --link --from=build /app .
USER $APP_UID
ENTRYPOINT ["./ConsoleApp"]

This Dockerfile would be built using this command:

bash
docker buildx build --secret id=nugetconfig,src=nuget.config .

Other Options

There are a number of techniques that can be used to protect secrets in a Docker build. Doing a web search yields a good quantity of material out there. The options listed above are considered to be simple and straightforward but depending on your environment and tolerance for complexity, you may want to consider other options. You're encouraged to do your research and find an option that works best for you.

General Troubleshooting

  • The sample Dockerfiles provided above make the assumption that the application's projects are all based on .NET. If you're attempting to build a project that is based on a .NET Framework project that makes use of a packages.config file, you'll need to run nuget restore instead of dotnet restore for that project or solution.

  • Often times, restoration of NuGet packages is the first point in a Dockerfile that attempts to make a network call. Firewalls and proxies can impact network connectivity. If you're encountering connection issues when attempting to restore NuGet packages, make sure that the container actually has network access by adding a command like RUN ping www.microsoft.com.


Documentation Scenarios Using System Drawing Common (`documentation/scenarios/using-system-drawing-common.md`)

Using the System.Drawing.Common Package in a Docker Container

The required dependencies of the System.Drawing.Common NuGet package are only available in Windows Server Core and full Server containers. Official .NET images for Windows Server Core are available with tags documented on the .NET container repos. Example tag: mcr.microsoft.com/dotnet/aspnet:8.0-windowsservercore-ltsc2022.


--- METRICS ---