# Technical Documentation: dotnet/dotnet-docker

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/dotnet/dotnet-docker)

### Overview (`README.md`)

# .NET

## Featured Repos

* [dotnet/sdk](https://github.com/dotnet/dotnet-docker/blob/main/README.sdk.md): .NET SDK
* [dotnet/aspnet](https://github.com/dotnet/dotnet-docker/blob/main/README.aspnet.md): ASP.NET Core Runtime
* [dotnet/runtime](https://github.com/dotnet/dotnet-docker/blob/main/README.runtime.md): .NET Runtime
* [dotnet/runtime-deps](https://github.com/dotnet/dotnet-docker/blob/main/README.runtime-deps.md): .NET Runtime Dependencies
* [dotnet/monitor](https://github.com/dotnet/dotnet-docker/blob/main/README.monitor.md): .NET Monitor Tool
* [dotnet/samples](https://github.com/dotnet/dotnet-docker/blob/main/README.samples.md): .NET Samples

## About

[.NET](https://docs.microsoft.com/dotnet/core/) is a general purpose development platform maintained by Microsoft and the .NET community on [GitHub](https://github.com/dotnet/core). 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](https://learn.microsoft.com/dotnet/csharp/async), 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#](https://docs.microsoft.com/dotnet/csharp/) 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#](https://docs.microsoft.com/dotnet/fsharp/) is a cross-platform, open-source, functional programming language for .NET. It also includes object-oriented and imperative programming.

[.NET](https://github.com/dotnet/core) is open source (MIT and Apache 2 licenses) and was contributed to the [.NET Foundation](http://dotnetfoundation.org) 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](https://github.com/dotnet/core/blob/main/CONTRIBUTING.md), 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](https://docs.microsoft.com/dotnet/core/)

Watch [discussions](https://github.com/dotnet/dotnet-docker/discussions/categories/announcements) for Docker-related .NET announcements.

## Usage

The [.NET Docker samples](https://github.com/dotnet/dotnet-docker/blob/main/samples/README.md) show various ways to use .NET and Docker together. See [Introduction to .NET and Docker](https://learn.microsoft.com/dotnet/core/docker/introduction) to learn more.

### Container sample: Run a simple application

You can quickly run a container with a pre-built [.NET Docker image](https://github.com/dotnet/dotnet-docker/blob/main/README.samples.md), based on the [.NET console sample](https://github.com/dotnet/dotnet-docker/blob/main/samples/ConsoleApp/README.md).

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](https://github.com/dotnet/dotnet-docker/blob/main/README.samples.md), based on the [ASP.NET Core sample](https://github.com/dotnet/dotnet-docker/blob/main/samples/AspNetCoreRazorApp/README.md).

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](https://github.com/dotnet/dotnet-docker/blob/6da64f31944bb16ecde5495b6a53fc170fbe100d/src/runtime-deps/8.0/bookworm-slim/amd64/Dockerfile#L7), starting with .NET 8. The [`-p` argument](https://docs.docker.com/engine/reference/commandline/run/#publish) 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](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/endpoints).

See [Hosting ASP.NET Core Images with Docker over HTTPS](https://github.com/dotnet/dotnet-docker/blob/main/samples/host-aspnetcore-https.md) 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](https://github.com/dotnet/dotnet-docker/blob/main/documentation/image-variants.md) contains a summary of the image variants and their use-cases.

### Distroless images

.NET [distroless container images](https://github.com/dotnet/dotnet-docker/blob/main/documentation/distroless.md) 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](https://github.com/dotnet/dotnet-docker/blob/main/documentation/azurelinux.md) and [Ubuntu (Chiseled)](https://github.com/dotnet/dotnet-docker/blob/main/documentation/ubuntu-chiseled.md).

## Related Repositories

.NET:

* [dotnet/nightly/sdk](https://github.com/dotnet/dotnet-docker/blob/nightly/README.sdk.md): .NET SDK (Preview)
* [dotnet/nightly/aspnet](https://github.com/dotnet/dotnet-docker/blob/nightly/README.aspnet.md): ASP.NET Core Runtime (Preview)
* [dotnet/nightly/runtime](https://github.com/dotnet/dotnet-docker/blob/nightly/README.runtime.md): .NET Runtime (Preview)
* [dotnet/nightly/runtime-deps](https://github.com/dotnet/dotnet-docker/blob/nightly/README.runtime-deps.md): .NET Runtime Dependencies (Preview)
* [dotnet/nightly/monitor](https://github.com/dotnet/dotnet-docker/blob/nightly/README.monitor.md): .NET Monitor Tool (Preview)

.NET Framework:

* [dotnet/framework](https://github.com/microsoft/dotnet-framework-docker/blob/main/README.md): .NET Framework, ASP.NET and WCF
* [dotnet/framework/samples](https://github.com/microsoft/dotnet-framework-docker/blob/main/README.samples.md): .NET Framework, ASP.NET and WCF Samples

## Support

### Lifecycle

* [Microsoft Support for .NET](https://github.com/dotnet/core/blob/main/support.md)
* [Supported Container Platforms Policy](https://github.com/dotnet/dotnet-docker/blob/main/documentation/supported-platforms.md)
* [Supported Tags Policy](https://github.com/dotnet/dotnet-docker/blob/main/documentation/supported-tags.md)

### 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](https://github.com/dotnet/dotnet-docker/blob/main/documentation/supported-tags.md#fixed-version-tags).

#### 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](https://nvd.nist.gov/vuln-metrics/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](https://github.com/dotnet/dotnet-docker/blob/main/SECURITY.md) and [Container Vulnerability Workflow](https://github.com/dotnet/dotnet-docker/blob/main/documentation/vulnerability-reporting.md) for more detail about what to do when a CVE is encountered in a .NET image.

### Feedback

* [File an issue](https://github.com/dotnet/dotnet-docker/issues/new/choose)
* [Contact Microsoft Support](https://support.microsoft.com/contactus/)

## License

* Legal Notice: [Container License Information](https://aka.ms/mcr/osslegalnotice)
* [.NET license](https://github.com/dotnet/dotnet-docker/blob/main/LICENSE)
* [Discover licensing for Linux image contents](https://github.com/dotnet/dotnet-docker/blob/main/documentation/image-artifact-details.md)
* [Windows base image license](https://learn.microsoft.com/virtualization/windowscontainers/images-eula) (only applies to Windows containers)
* [Pricing and licensing for Windows Server](https://www.microsoft.com/windows-server/pricing)

---

### 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](kubernetes/README.md) directory.

> [!IMPORTANT]
>
> [.NET 10 containers breaking changes](https://learn.microsoft.com/dotnet/core/compatibility/10#containers)
>
> [.NET 9 containers breaking changes](https://learn.microsoft.com/dotnet/core/compatibility/9.0#containers)
>
> [.NET 8 containers breaking changes](https://learn.microsoft.com/dotnet/core/compatibility/8.0#containers)

## Building images

* [Build a .NET container image](ConsoleApp/README.md)
* [Build an ASP.NET Core container image](AspNetCoreRazorApp/README.md)
* [Build a single file app](ConsoleAppSelfContained/README.md)
* [Build a native AOT app](AspNetCoreNativeAot/README.md)
* [Building a globalization and time zone aware (or unaware) image](ConsoleAppWithGlobalization/README.md)
* [Build for a platform](build-for-a-platform.md)

## Development guidance

* [Selecting .NET image tags](../documentation/supported-tags.md)
* [Enable (or disable) globalization](enable-globalization.md)
* [Build and test a multi-project solution](MultiProjectApp/README.md)
* [Run test in a container](run-tests-in-sdk-container.md)
* [Build in an SDK container](build-in-sdk-container.md)
* [Run applications in an SDK container](run-in-sdk-container.md)
* [Run ASP.NET Core Applications in development with container and HTTPS](run-aspnetcore-https-development.md)
* [Discover licensing for Linux image contents](https://github.com/dotnet/dotnet-docker/blob/main/documentation/image-artifact-details.md)

## Hosting guidance

* [Host ASP.NET Core Images with container and HTTPS](host-aspnetcore-https.md)
* [Push container images to Azure Container Registry](push-image-to-acr.md)
* [Push container images to Docker Hub](push-image-to-dockerhub.md)
* [Deploy ASP.NET Core Applications to Azure Container Instances](deploy-container-to-aci.md)

## Other documentation

* [.NET 8 Container Workshop](https://github.com/richlander/container-workshop)
* [Introduction to .NET and Docker](https://learn.microsoft.com/dotnet/core/docker/)
* [Announcing built-in container support for the .NET SDK](https://devblogs.microsoft.com/dotnet/announcing-builtin-container-support-for-the-dotnet-sdk/)
* [Staying safe in containers](https://devblogs.microsoft.com/dotnet/staying-safe-with-dotnet-containers/)
* [Improving multi-platform container support](https://devblogs.microsoft.com/dotnet/improving-multiplatform-container-support/)
* [Container blog posts](https://devblogs.microsoft.com/dotnet/category/containers/)

## 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

* [dotnet](https://github.com/dotnet/dotnet-docker/blob/main/README.md): .NET
* [dotnet/framework](https://github.com/microsoft/dotnet-framework-docker/blob/main/README.md): .NET Framework, ASP.NET and WCF

---

### 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

* [Managing NuGet Credentials in Docker Scenarios](nuget-credentials.md)
* [Installing .NET in a Dockerfile](installing-dotnet.md)
* [Using the System.Drawing.Common Package in a Docker Container](using-system-drawing-common.md)

## 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](../../CONTRIBUTING.md).

---

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

# Don't touch this folder

                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](DEV-GUIDE.md).

---

### 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](../update-dependencies/README.md) and the
[.NET Docker test project](../../tests/Microsoft.DotNet.Docker.Tests/).

---

### 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](../../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](https://github.com/dotnet/docker-tools/issues/1658)).

## 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`](./BuildRepo.cs) enum.
- Update the [GetBuildRepo()](./BuildExtensions.cs) extension method to support
  the new enum value.
- Implement a new [`IBuildUpdaterService`](./IBuildUpdaterService.cs), using
  existing implementations as a reference.
- Register a new keyed singleton in [Program.cs](./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).

### External projects

1. Add your component's version to
   [manifest.versions.json](../../manifest.versions.json) with the format:
   `<product-key>|<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.
1. Create a new class that implements
   [IDependencyVersionSource](./IDependencyVersionSource.cs). You can use
   whatever method you need to fetch version updates. Example implementation:
   [ChiselVersionSource](./ChiselVersionSource.cs) (determines version using
   GitHub Releases).
1. Register your IDependencyVersionSource implementation in
   [Program.cs](./Program.cs) using **the same product key** from step 1.
1. 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> <product-key>`
1. (Optional) Add a job to the
   [update-dependencies pipeline](../pipelines/pipelines/update-dependencies.yml)
   to enable automatic update pull requests.

---

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

# Release Json Report API

This app demonstrates publishing an app as [native AOT](https://learn.microsoft.com/dotnet/core/deploying/native-aot/) in containers.

A similar console app sample supports [single file deployment](../ConsoleAppSelfContained/README.md) (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](https://github.com/dotnet/core/blob/main/release-notes/releases-index.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.

- [Ubuntu Chiseled](Dockerfile)
- [Alpine Linux](Dockerfile.alpine)
- [Azure Linux Distroless](Dockerfile.azurelinux-distroless)
- [Ubuntu Chiseled with Globalization support](Dockerfile.icu)
- [Alpine Linux with Globalization support](Dockerfile.alpine-icu)
- [Azure Linux Distroless with Globalization support](Dockerfile.azurelinux-distroless-icu)

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.

- [Build on AMD64 targeting ARM64](Dockerfile.ubuntu-cross-x64-arm64)
- [Build on ARM64 targeting AMD64](Dockerfile.ubuntu-cross-arm64-x64)

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](https://github.com/dotnet/runtime/blob/main/src/coreclr/nativeaot/docs/containers.md) 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](../build-for-a-platform.md) 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](../README.md) for more samples.

## Run the sample image

You can start by launching a sample from our [container registry](https://mcr.microsoft.com/) 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](https://github.com/dotnet/dotnet-docker/blob/6da64f31944bb16ecde5495b6a53fc170fbe100d/src/runtime-deps/8.0/bookworm-slim/amd64/Dockerfile#L7). The [`-p` argument](https://docs.docker.com/engine/reference/commandline/run/#publish) 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](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/endpoints).
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](https://github.com/dotnet/dotnet-docker/blob/main/src/runtime-deps/11.0/resolute/amd64/Dockerfile#L7)
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](../host-aspnetcore-https.md#hosting-aspnet-core-images-with-docker-over-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](../run-aspnetcore-https-development.md).

## 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](https://devblogs.microsoft.com/dotnet/dotnet-6-is-now-in-ubuntu-2204/#net-in-chiseled-ubuntu-containers) image using [Dockerfile.chiseled](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](#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](https://docs.docker.com/build/building/multi-platform/#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](../build-for-a-platform.md#net-and-qemu) for more information.

## Build image with the SDK

The easiest way to [build .NET images is using the SDK](https://learn.microsoft.com/dotnet/core/containers/overview).

```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](https://learn.microsoft.com/visualstudio/msbuild/property-element-msbuild).
For a full list of supported properties, see the [.NET SDK publishing reference](https://learn.microsoft.com/dotnet/core/containers/publish-configuration).

## Supported Linux distros

The .NET Team publishes images for [multiple distros](../../documentation/supported-platforms.md).

Sample Dockerfiles are provided for:

- [Alpine](Dockerfile.alpine)
- [Alpine with Composite ready-to-run image](Dockerfile.alpine-composite)
- [Alpine with ICU installed](Dockerfile.alpine-icu)
- [Azure Linux](Dockerfile.azurelinux)
- [Azure Linux Distroless](Dockerfile.azurelinux-distroless)
- [Ubuntu](Dockerfile.ubuntu)
- [Ubuntu Chiseled](Dockerfile.chiseled)
- [Ubuntu Chiseled with Composite ready-to-run image](Dockerfile.chiseled-composite)

## Supported Windows versions

The .NET Team publishes images for [multiple Windows versions](../../documentation/supported-platforms.md). You must have [Windows containers enabled](https://docs.docker.com/docker-for-windows/#switch-between-windows-and-linux-containers) to use these images.

Samples are provided for

- [Nano Server](Dockerfile.nanoserver)
- [Windows Server Core](Dockerfile.windowsservercore)
- [Windows Server Core with IIS](Dockerfile.windowsservercore-iis) (Note: the IIS sample listens on port `80`, not `8080`)

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](../README.md) for more samples.

## Run the sample image

You can start by launching a sample from our [container registry](https://mcr.microsoft.com/artifact/mar/dotnet/samples/about).

```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](../../documentation/ubuntu-chiseled.md) image using [Dockerfile.chiseled](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](../../documentation/supported-platforms.md).

Sample Dockerfiles are provided for:

- [Alpine](Dockerfile.alpine)
- [Alpine with ICU installed](Dockerfile.alpine-icu)
- [Azure Linux](Dockerfile.azurelinux)
- [Azure Linux Distroless](Dockerfile.azurelinux-distroless)
- [Ubuntu](Dockerfile.ubuntu)
- [Ubuntu Chiseled](Dockerfile.chiseled)

## Supported Windows versions

The .NET Team publishes images for [multiple Windows versions](../../documentation/supported-platforms.md). You must have [Windows containers enabled](https://docs.docker.com/docker-for-windows/#switch-between-windows-and-linux-containers) to use these images.

Samples are provided for

- [Nano Server](Dockerfile.nanoserver)
- [Windows Server Core](Dockerfile.windowsservercore)

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](https://learn.microsoft.com/dotnet/core/deploying/single-file/) deployment in containers.

A similar [web API sample](../AspNetCoreNativeAot/README.md) 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:

<details>
<summary>Command output</summary>

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

</details>

## 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](https://github.com/dotnet/core/blob/main/release-notes/releases-index.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:

- [Alpine](Dockerfile.alpine)
- [Azure Linux Distroless](Dockerfile.azurelinux-distroless)
- [Ubuntu](Dockerfile)
- [Ubuntu Chiseled](Dockerfile.chiseled)

---

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

# Globalization Test App

This [app](Program.cs) 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](Dockerfile) is intended to aid testing and isn't itself an example of a production Dockerfile.

See [.NET Docker Samples](../README.md) for more samples.

## Configuration

The behavior of these APIs is affected by:

- Presence of `icu` and `tzdata` packages.
- The value of [globalization invariant mode](https://aka.ms/GlobalizationInvariantMode).
- The value (or absence) of the `TZ` environment variable.
- The value (or absence) of `/etc/timezone`

The recommended way to configure [tzdata](https://en.wikipedia.org/wiki/Tz_database) and [timezones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) 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](https://en.wikipedia.org/wiki/Coordinated_Universal_Time). 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 Å Å; 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](MultiProjectApp/Program.cs) "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](../README.md).

The instructions assume that you have cloned this repo, have [Docker](https://www.docker.com/products/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](./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](../run-tests-in-sdk-container.md), 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](../establishing-docker-environment.md) 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](../run-tests-in-sdk-container.md) is one example that covers running this sample's tests using the separate executable test stage in the [`MultiProjectApp` Dockerfile](./Dockerfile).

## More Samples

* [.NET Docker Samples](../README.md)
* [.NET Framework Docker Samples](https://github.com/microsoft/dotnet-framework-docker/blob/main/samples/README.md)

---

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

# Using .NET with Kubernetes

[Kubernetes](https://kubernetes.io/) 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](../README.md) 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](./environment.md).

## 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](hello-dotnet/README.md) for information.

## Examples

The following examples demonstrate various Kubernetes and .NET capabilities.

- [hello-dotnet](hello-dotnet/README.md)
- [Manual deployment](manual-deployment/README.md)
- [Resource limits](resource-limits/README.md)
- [Non-root user](non-root/README.md)
- [Replicas and health checks](replicas-and-health/README.md)
- [dotnet-monitor](dotnet-monitor/README.md)
- [Graceful shutdown](graceful-shutdown/README.md)

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](../../documentation/scenarios/README.md).

---

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

# Monitor your app with `dotnet-monitor` in Kubernetes

[`dotnet-monitor`](https://github.com/dotnet/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](https://kubernetes.io/) deployment.

## Use `dotnet-monitor`

Apply [dotnet-monitor.yaml](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](https://github.com/dotnet/dotnet-monitor/blob/main/documentation/api/README.md) 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](https://prometheus.io/docs/instrumenting/exposition_formats/). 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](https://prometheus.io/) is a popular monitoring solution. `dotnet-monitor` exports metrics data in [Prometheus exposition format](https://prometheus.io/docs/instrumenting/exposition_formats/) via its `metrics` endpoint. That makes it straightforward to connect the two systems.

Apply [prometheus-app.yaml](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.

<img width="1191" alt="image" src="https://user-images.githubusercontent.com/2608468/231349237-69bd3b08-57fd-4d87-9e16-1fdaf6087b34.png">

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](graceful-shutdown.md).

The [following pattern](https://github.com/dotnet/dotnet-docker/blob/main/samples/AspNetCoreRazorApp/AspNetCoreRazorApp/Program.cs#L30-L55) 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](https://kubernetes.io/) 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`](https://minikube.sigs.k8s.io/docs/handbook/accessing/#example-of-loadbalancer).

```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](../environment.md)

```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](https://learn.microsoft.com/aspnet/core/security/enforcing-ssl) and [What is Application Gateway Ingress Controller?](https://learn.microsoft.com/azure/application-gateway/ingress-controller-overview) describe options for exposing secure HTTPS endpoints.

---

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

# Manual deployment

[kubectl](https://kubernetes.io/docs/reference/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](https://kubernetes.io/) provides a way to validate that a [non-root user](https://devblogs.microsoft.com/dotnet/securing-containers-with-rootless/) is used.

Apply [non-root.yaml](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`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#securitycontext-v1-core) holds security configuration that will be applied to a container, in a Pod or [Deployment manifest](non-root.yaml). 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](https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted).

This [`securityContext`](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/) 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](https://github.com/dotnet/dotnet-docker/blob/7bca20cb06e1f912fc2e7fa8ce04dda606277537/samples/aspnetapp/Dockerfile#L21) 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](https://kubernetes.io/) enables creating multiple [replicas](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/) and registering [health checks](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/).

There are multiple forms of health checks. The sample demonstrates a `liveness` check. The following warning is copied from [Kubernetes docs](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/).

> 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](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](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-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](https://kubernetes.io/) enables [limiting the CPU and/or memory resources](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) that can be used by a container.

Apply [resource-limits.yaml](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:

```
/* 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](https://github.com/open-telemetry/opentelemetry-proto) 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](https://github.com/microsoft/AzureLinux) is an MIT-licensed, RPM-based Linux distribution published by Microsoft.
Azure Linux .NET images are [publicly supported](https://github.com/dotnet/dotnet-docker/discussions/5022) by Microsoft.

## Featured Tags

.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](./distroless.md) for more info.

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

* [`mcr.microsoft.com/dotnet/runtime`](../README.runtime.md)
* [`mcr.microsoft.com/dotnet/aspnet`](../README.aspnet.md)
* [`mcr.microsoft.com/dotnet/runtime-deps`](../README.runtime-deps.md) (for self-contained or AOT apps)

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.

```
/* 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](https://learn.microsoft.com/visualstudio/containers/container-build#debugging).

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](https://docs.microsoft.com/visualstudio/containers/container-msbuild-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](./ubuntu-chiseled.md) and [Azure Linux](./azurelinux.md).

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

- [`mcr.microsoft.com/dotnet/runtime`](../README.runtime.md)
- [`mcr.microsoft.com/dotnet/aspnet`](../README.aspnet.md)
- [`mcr.microsoft.com/dotnet/runtime-deps`](../README.runtime-deps.md) (for self-contained or AOT apps)

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](https://github.com/dotnet/dotnet-docker/issues/4942) 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"](https://github.com/dotnet/dotnet-docker/discussions/4821) 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](https://docs.docker.com/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](https://docs.docker.com/engine/reference/builder/#run) 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](https://github.com/dotnet/dotnet-docker/issues/371)). Use cases that cannot tolerate the globalization invariant mode can reset the `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT` environment variable.
    1. 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
    1. Package as a .NET global tool
    1. Ship within their own Docker image
        1. Support standalone execution and multi-stage builds
        1. Support running within a side car image

1. Components installed within the images are required to have the same or longer support lifecycle as [.NET](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). 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.

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

1. 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.
    1. If a new component is added, it should be available across all supported OS types and architectures.

1. The matrix of [supported container operating systems and versions](supported-platforms.md) will evolve on a continuous basis.

## Image Tagging

See the [supported tags](supported-tags.md) 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.

1. 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](https://github.com/dotnet/dotnet-docker/blob/main/README.md#image-update-policy) section of the README contains the exact guidelines for when images will be updated.

1. Images will never be deleted from the [official .NET Docker repositories on the Microsoft Artifact Registry](https://mcr.microsoft.com/catalog?search=dotnet). This does not apply to the [nightly repositories](https://github.com/dotnet/dotnet-docker/blob/nightly/README.md).

1. The [Dockerfiles](https://github.com/search?q=repo%3Adotnet%2Fdotnet-docker+path%3A**%2FDockerfile&type=code) 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.

1. 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](https://aka.ms/mcr/osslinuxmetadata).  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](#distroless-images-and-appliance-images) 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](https://github.com/dotnet/dotnet-docker/blob/d90e7bd1d10c8781f0008f5ab1327ca3481e78de/src/runtime/8.0/bookworm-slim/amd64/Dockerfile#L6-L13), [aspnet](https://github.com/dotnet/dotnet-docker/blob/d90e7bd1d10c8781f0008f5ab1327ca3481e78de/src/aspnet/8.0/bookworm-slim/amd64/Dockerfile#L6-L12), and [sdk](https://github.com/dotnet/dotnet-docker/blob/d90e7bd1d10c8781f0008f5ab1327ca3481e78de/src/sdk/8.0/bookworm-slim/amd64/Dockerfile#L26-L48) Dockerfiles.

### .NET Runtime and ASP.NET Core Images

The [runtime](../README.runtime.md) and [ASP.NET Core images](../README.aspnet.md) 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](../README.sdk.md) includes the .NET SDK, which includes various .NET components, with associated licenses and third party notice files.

```
/* 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](./distroless.md) since distroless images don't contain a shell by default.
Instead, this repo contains a [Dockerfile](./scripts/Dockerfile.distroless-wrapper) 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](https://github.com/dotnet/dotnet-docker/blob/main/documentation/image-artifact-details.md) 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](./distroless.md) 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](https://notaryproject.dev/) signatures.

## Inspect signature artifacts

Signatures are stored as [OCI referrer artifacts](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers)
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:

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

## Verify signatures using Notation CLI

### Prerequisites

- [Notation CLI]. This guide was written using version `1.3.2`.
- (Optional) [ORAS CLI] or [Docker CLI], for resolving image digests.

### Download the root CA certificates

.NET container image signatures use the following certificates:

- Signature Root Certificate: [Microsoft Supply Chain RSA Root CA 2022](https://www.microsoft.com/pkiops/certs/Microsoft%20Supply%20Chain%20RSA%20Root%20CA%202022.crt)
- Timestamping Authority (TSA) Root Certificate: [Microsoft Root Certificate Authority 2010](http://www.microsoft.com/pki/certs/MicRooCerAut_2010-06-23.crt)

### Set up Trust Store and Trust Policy

See the Notary Project's [Trust Store and Trust Policy Specification](https://github.com/notaryproject/specifications/blob/d4b62b949228911bd5e62da4f5f8dd38682d293a/specs/trust-store-trust-policy.md).

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](https://github.com/notaryproject/specifications/blob/d4b62b949228911bd5e62da4f5f8dd38682d293a/specs/trust-store-trust-policy.md#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](supported-tags.md)
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>
```

## Useful links

- [Notary Project Overview](https://notaryproject.dev/docs/notary-project-overview/)
- [Notary Project Specifications](https://github.com/notaryproject/specifications)

[Docker CLI]: https://docs.docker.com/reference/cli/docker/
[Notation CLI]: https://github.com/notaryproject/notation
[ORAS CLI]: https://oras.land/docs/installation

---

### 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](./ubuntu-chiseled.md) .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](https://learn.microsoft.com/dotnet/core/runtime-config/globalization).
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](https://ubuntu.com/engage/chiselled-ubuntu-images-for-containers) and [Azure Linux](./azurelinux.md) 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](./distroless.md).

## `extra` (.NET 8+)

For apps that depend on globalization functionality, the `extra` image variant is offered for [Ubuntu Chiseled](./ubuntu-chiseled.md) and [Azure Linux](./azurelinux.md) 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](https://learn.microsoft.com/dotnet/core/deploying/ready-to-run).
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](https://github.com/dotnet/runtime/blob/main/docs/design/features/readytorun-composite-format-design.md).

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

SDK images with the `-aot` suffix include [additional libraries](https://learn.microsoft.com/dotnet/core/deploying/native-aot#prerequisites) required for native AOT compilation that aren't present in the default `mcr.microsoft.com/dotnet/sdk` images.

[Native AOT](https://learn.microsoft.com/dotnet/core/deploying/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"](https://learn.microsoft.com/dotnet/core/deploying/native-aot#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](../samples/AspNetCoreNativeAot).

| 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](https://github.com/dotnet/dotnet-docker/discussions/categories/announcements)
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](./distroless.md)
  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](https://github.com/dotnet/dotnet-docker/blob/main/documentation/image-variants.md)
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"](https://learn.microsoft.com/dotnet/core/deploying)
for more details on all of the supported options for publishing .NET apps.

[Ubuntu Chiseled]:                                 https://github.com/dotnet/dotnet-docker/blob/main/documentation/ubuntu-chiseled.md
[Self-contained]:                                  https://learn.microsoft.com/dotnet/core/deploying/#publish-self-contained
[Trimming]:                                        https://learn.microsoft.com/dotnet/core/deploying/trimming/trim-self-contained
[Native AOT]:                                      https://learn.microsoft.com/dotnet/core/deploying/native-aot/
[Framework-dependent]:                             https://learn.microsoft.com/dotnet/core/deploying/#publish-framework-dependent
[`aspnet:10.0`]:                            https://github.com/dotnet/dotnet-docker/blob/main/src/aspnet/10.0/noble
[`aspnet:10.0-noble-chiseled`]:             https://github.com/dotnet/dotnet-docker/blob/main/src/aspnet/10.0/noble-chiseled
[`aspnet:10.0-noble-chiseled-extra`]:       https://github.com/dotnet/dotnet-docker/blob/main/src/aspnet/10.0/noble-chiseled-extra
[`runtime-deps:10.0`]:                      https://github.com/dotnet/dotnet-docker/blob/main/src/runtime-deps/10.0/noble
[`runtime-deps:10.0-noble-chiseled`]:       https://github.com/dotnet/dotnet-docker/blob/main/src/runtime-deps/10.0/noble-chiseled
[`runtime-deps:10.0-noble-chiseled-extra`]: https://github.com/dotnet/dotnet-docker/blob/main/src/runtime-deps/10.0/noble-chiseled-extra
[`aspnet:10.0-alpine`]:                     https://github.com/dotnet/dotnet-docker/blob/main/src/aspnet/10.0/alpine3.23
[`aspnet:10.0-alpine-extra`]:               https://github.com/dotnet/dotnet-docker/blob/main/src/aspnet/10.0/alpine3.23-extra
[`runtime-deps:10.0-alpine`]:               https://github.com/dotnet/dotnet-docker/blob/main/src/runtime-deps/10.0/alpine3.23
[`runtime-deps:10.0-alpine-extra`]:         https://github.com/dotnet/dotnet-docker/blob/main/src/runtime-deps/10.0/alpine3.23-extra

---

### 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](https://github.com/dotnet/core/blob/main/os-lifecycle-policy.md). 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](https://github.com/dotnet/dotnet-docker/discussions). Alternatively, you can [author your own .NET images](scenarios/installing-dotnet.md).

- 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](https://github.com/dotnet/dotnet-docker/blob/nightly/README.md) first, and are added to the officially supported repos afterwards.
- All new OS releases will be accompanied by an [announcement](https://github.com/dotnet/dotnet-docker/discussions/categories/announcements).
- Unless otherwise specified (see Alpine Linux below), we stop publishing updates to images when the .NET version in the image reaches [end of support](https://dotnet.microsoft.com/platform/support/policy/dotnet-core) 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](https://github.com/dotnet/core/issues/9638).

### 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](https://github.com/dotnet/dotnet-docker/blob/nightly/README.md), pending the availability of pre-release base images.

#### FedRAMP Compliance

For [.NET appliance images](./supported-tags.md#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](https://learn.microsoft.com/windows/release-health/windows-server-release-info). .NET images will be published for the following Windows OS versions:

- Nano Server - all versions in the [Mainstream Support](https://learn.microsoft.com/lifecycle/policies/fixed#mainstream-support) phase.
- Windows Server Core - all versions in the [Mainstream Support](https://learn.microsoft.com/lifecycle/policies/fixed#mainstream-support) phase.

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](https://mcr.microsoft.com/artifact/mar/windows/servercore/about) or [Windows Server](https://mcr.microsoft.com/artifact/mar/windows/server/about).

## 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](https://hub.docker.com/search?q=&type=image&image_filter=official). Please [log an issue](https://github.com/dotnet/dotnet-docker/issues/new/choose) if you encounter problems using .NET images or applying these tagging patterns.

Complete tag lists:

- [runtime-deps](../README.runtime-deps.md#full-tag-listing)
- [runtime](../README.runtime.md#full-tag-listing)
- [aspnet](../README.aspnet.md#full-tag-listing)
- [sdk](../README.sdk.md#full-tag-listing)
- [monitor](../README.monitor.md#full-tag-listing)
- [monitor-base](../README.monitor-base.md#full-tag-listing)
- [aspire-dashboard](../README.aspire-dashboard.md#full-tag-listing)
- [samples](../README.samples.md#full-tag-listing)

The terms "fixed version" and "floating version" are used throughout. They are defined in [Tag policies](#tag-policies).

## Single-platform tags

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

### `<Major.Minor.Patch .NET Version>-<OS>-<Architecture>`

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`

### `<Major.Minor .NET Version>-<OS>-<Architecture>`

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](https://docs.docker.com/build/building/multi-platform/).

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](supported-platforms.md#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](https://github.com/containerd/containerd/issues/6508)
> and [dotnet/dotnet-docker#4492](https://github.com/dotnet/dotnet-docker/issues/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
> ```

### `<Major.Minor.Patch .NET Version>-<OS version>`

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`

### `<Major.Minor .NET Version>-<OS version>`

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`

### `<Major.Minor .NET Version>-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](https://github.com/dotnet/dotnet-docker/discussions/categories/announcements) so that users know when the tags they want are available.

### `<Major.Minor.Patch .NET Version>`

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`

### `<Major.Minor .NET Version>`

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](./image-variants.md).

### `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](https://github.com/dotnet/core/blob/main/release-policies.md#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>.<Patch>-preview.<PreviewVersion>` | `<Major>.<Minor>-preview` |
| **Release Candidate (Go-Live)** | `<Major>.<Minor>.<Patch>-rc.<ReleaseCandidateVersion>` | `<Major>.<Minor>` |
| **Active and Maintenance** | `<Major>.<Minor>.<Patch>` | `<Major>.<Minor>` |

See [.NET's release policies](https://github.com/dotnet/core/blob/main/release-policies.md) 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](../README.md#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](../README.md#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](https://github.com/dotnet/dotnet-docker/discussions/categories/announcements) 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](supported-platforms.md#operating-systems) 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](https://en.wikipedia.org/wiki/Debian_version_history) and [Ubuntu](https://en.wikipedia.org/wiki/Ubuntu_version_history) images, release codenames are used instead of version numbers.

### .NET appliance images

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

- [.NET Monitor](../README.monitor.md#full-tag-listing)
- [.NET Monitor Base](../README.monitor-base.md#full-tag-listing)
- [Aspire Dashboard](../README.aspire-dashboard.md#full-tag-listing)

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](./distroless.md) 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](supported-platforms.md).

When a .NET version or an OS version reaches [end of support](https://dotnet.microsoft.com/platform/support/policy/dotnet-core) (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?"](vulnerability-reporting.md#c-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](https://github.com/dotnet/dotnet-docker/discussions/categories/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](./distroless.md) 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

## Featured Tags

* `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](https://github.com/dotnet/dotnet-docker/issues/4942) 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:

* [ConsoleApp](../samples/ConsoleApp/Dockerfile.chiseled)
* [ConsoleAppSelfContained](../samples/ConsoleAppSelfContained/Dockerfile.chiseled)
* [AspNetCoreRazorApp](../samples/AspNetCoreRazorApp/Dockerfile.chiseled)
* [AspNetCoreNativeAot](../samples/AspNetCoreNativeAot/Dockerfile) (and [icu version](../samples/AspNetCoreNativeAot/Dockerfile.icu))

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](https://github.com/dotnet/dotnet-docker/issues/6135), which is currently only available in **.NET 10+** images.

[Chisel](https://github.com/canonical/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](https://github.com/canonical/chisel-releases) 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](https://github.com/canonical/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](https://github.com/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](../README.md#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](https://docs.docker.com/desktop)
* [ORAS CLI](https://oras.land/)
* [`jq`](https://jqlang.org/), [PowerShell](https://github.com/PowerShell/PowerShell), or Windows PowerShell
* Whether the container image is a [Linux or Windows image](#how-can-i-determine-whether-my-image-is-based-on-linux-or-windows)
  * For Linux images, it's also important to [know the Linux distro and version](#how-can-i-determine-the-os-version-of-a-linux-image) (e.g. `Debian Bookworm`, `Alpine 3.22`, `Ubuntu 24.04 (Noble)`, `Azure Linux 3.0`)
* If you're using Docker Desktop for Windows and investigating a vulnerability in a Linux image, [change your settings](https://docs.docker.com/desktop/settings/windows/) 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?](#a-is-your-scanner-targeting-the-latest-version-of-your-image)"

```
/* 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](#what-is-an-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](https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-linux).

```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](https://mcr.microsoft.com/artifact/mar/windows/nanoserver/about).
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](https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-linux).

```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

* `Yes`: go to "[B. Scan your latest image](#b-scan-your-latest-image)"
* `No`: go to "[C. Is your image built from a supported .NET tag?](#c-is-your-image-built-from-a-supported-net-tag)"

### 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 <image-tag>`

### 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](supported-tags.md) 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:

* [runtime-deps](../README.runtime-deps.md#full-tag-listing)
* [runtime](../README.runtime.md#full-tag-listing)
* [aspnet](../README.aspnet.md#full-tag-listing)
* [sdk](../README.sdk.md#full-tag-listing)
* [monitor](../README.monitor.md#full-tag-listing)
* [monitor-base](../README.monitor-base.md#full-tag-listing)
* [aspire-dashboard](../README.aspire-dashboard.md#full-tag-listing)
* [samples](../README.samples.md#full-tag-listing)
* [Microsoft Artifact Registry](https://mcr.microsoft.com/catalog?search=dotnet/)

#### 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

* `Yes`: go to "[E. Is your image built from the latest .NET image?](#e-is-your-image-built-from-the-latest-net-image)"
* `No`: go to "[D. Update base image tag to supported image](#d-update-base-image-tag-to-supported-image)"

### D. Update base image tag to supported image

Switching to a [supported base image tag](supported-tags.md) 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](https://dotnet.microsoft.com/platform/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](supported-tags.md) for more information.

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

The [Image Update Policy](/README.md#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](#what-is-an-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](https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-linux).

```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

* `True`/`Yes`: go to "[G. Is .NET the source of the vulnerability?](#g-is-net-the-source-of-the-vulnerability)"
* `False`/`No`: go to "[F. Rebuild your image to get the latest base](#f-rebuild-your-image-to-get-the-latest-base)"

### 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](https://learn.microsoft.com/azure/devops/pipelines/tasks/reference/docker-v2) should set the `--pull` option as an argument:

  ```yaml
  arguments: "--pull"
  ```

* CI workflows that use the GitHub Actions [Docker Build and Push Action](https://github.com/marketplace/actions/build-and-push-docker-images) 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](https://docs.docker.com/engine/reference/commandline/build/).

### 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

* `Yes`: go to "[R. Log an issue at dotnet/dotnet-docker](#r-log-an-issue-at-dotnetdotnet-docker)"
* `No`: go to "[H. Which OS platform is this for?](#h-which-os-platform-is-this-for)"

### H. Which OS platform is this for?

See [How can I determine whether my image is based on Linux or Windows?](#how-can-i-determine-whether-my-image-is-based-on-linux-or-windows).

#### Response

* `Windows`: go to "[I. Log an issue at microsoft/Windows-Containers](#i-log-an-issue-at-microsoftwindows-containers)"
* `Linux`: go to "[J. Is a fix available for the package from the Linux distro?](#j-is-a-fix-available-for-the-package-from-the-linux-distro)"

### 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](https://github.com/microsoft/Windows-Containers/issues/new/choose) 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](#how-can-i-view-the-linux-distros-information-about-a-vulnerability) 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](https://security-tracker.debian.org/tracker/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](#how-can-i-view-the-linux-distros-information-about-a-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

* `Yes`: go to "[L. Is the package in the Linux distro base image?](#l-is-the-package-in-the-linux-distro-base-image)"
* `No`: go to "[K. Wait for a fix to be available from the Linux distro](#k-wait-for-a-fix-to-be-available-from-the-linux-distro)"

### 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](#how-can-i-determine-the-tag-of-the-linux-distro-image-that-my-image-is-based-upon).
Then you can execute the script below to determine whether the package is installed by the Linux distro base image.

#### macOS/Linux

Requires [PowerShell](https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-linux) 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

* `Yes`: go to "[M. Upgrade packages or wait for an updated Linux distro base image](#m-upgrade-packages-or-wait-for-an-updated-linux-distro-base-image)"
* `No`: go to "[N. Is the package installed by .NET?](#n-is-the-package-installed-by-net)"

### 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](../README.md#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](#l-is-the-package-in-the-linux-distro-base-image) so make sure you've followed that part of the workflow first.

#### macOS/Linux

Requires [PowerShell to be installed](https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-linux).

```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

* `Yes`: go to "[P. Is it Critical severity?](#p-is-it-critical-severity)"
* `No`: go to "[O. Rebuild your image to get the latest package](#o-rebuild-your-image-to-get-the-latest-package)"

### 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](https://www.cve.org/SiteSearch).
The score will consist of both a number and a label.
Note whether the label indicates "Critical".

#### Response

* `Yes`: go to "[R. Log an issue at dotnet/dotnet-docker](#r-log-an-issue-at-dotnetdotnet-docker)"
* `No`: go to "[Q. Wait for the next .NET servicing release](#q-wait-for-the-next-net-servicing-release)"

### 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](https://github.com/dotnet/dotnet-docker/blob/main/README.md#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](https://github.com/dotnet/dotnet-docker/issues/new/choose) 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](https://docs.docker.com/desktop/settings/windows/) to target Linux containers.

1. Run the following command:

    ```shell
    docker pull <insert-my-image-digest>
    ```

1. 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](../README.runtime-deps.md) within the appropriate "Linux &lt;architecture&gt; 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](https://github.com/anchore/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](https://github.com/anchore/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](../README.md) 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:

* Debian: <https://security-tracker.debian.org/tracker>
  * `https://security-tracker.debian.org/tracker/CVE-<NUMBER>`
* Ubuntu: <https://ubuntu.com/security/cves>
  * `https://ubuntu.com/security/CVE-<NUMBER>`
* Alpine: <https://security.alpinelinux.org>
  * `https://security.alpinelinux.org/vuln/CVE-<NUMBER>`
* Azure Linux: <https://github.com/microsoft/AzureLinuxVulnerabilityData>
  * Azure Linux provides vulnerability data in the [OVAL](https://oval.mitre.org/language/about/overview.html) format.

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](../../README.md), 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](#base-images) and [application images](#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](../../README.md).

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](https://learn.microsoft.com/dotnet/core/deploying/), 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](../../samples/ConsoleAppSelfContained/README.md).

## 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](https://github.com/docker-library/official-images) 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](#installing-from-a-binary-archive) (Linux/Windows) or [package manager](#installing-from-a-linux-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](#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](#installing-from-a-linux-package-manager) (Linux only) or [dotnet-install script](#installing-from-dotnet-install-script) (Linux/Windows).

## Installing .NET

### Prerequisites

When installing .NET [from a binary archive](#installing-from-a-binary-archive) or the [dotnet-install script](#installing-from-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](https://github.com/dotnet/core/blob/main/linux.md#dependencies), including example commands for installing the packages.
You can also see the official [.NET Dockerfiles](https://github.com/dotnet/dotnet-docker/tree/main/src) for more package installation examples.

When [installing from a package manager](#installing-from-a-linux-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](https://github.com/dotnet/dotnet-docker) 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](https://github.com/docker-library/official-images#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):

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

Example (Windows):

```
/* 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`](https://builds.dotnet.microsoft.com/dotnet/release-metadata/10.0/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](https://github.com/dotnet/core/blob/main/release-notes/README.md) 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](https://learn.microsoft.com/dotnet/core/install/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](#installing-from-a-binary-archive) or the [dotnet-install script](#installing-from-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](https://github.com/dotnet/core/discussions/9556).

.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](https://github.com/dotnet/core/issues/new) 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](https://learn.microsoft.com/dotnet/core/install/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](#installing-from-a-binary-archive) or [dotnet-install script](#installing-from-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](https://learn.microsoft.com/dotnet/core/install/linux#packages) for detailed instructions for each supported distro.

Keep in mind the [Dockerfile best practices](https://docs.docker.com/build/building/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](https://learn.microsoft.com/dotnet/core/tools/dotnet-install-script) 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](https://github.com/dotnet/core/blob/main/linux.md#dependencies) are installed. The [.NET Dockerfiles](https://github.com/dotnet/dotnet-docker) 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](https://docs.docker.com/engine/reference/commandline/build/#set-build-time-variables---build-arg) 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](#passing-secrets-by-file-with-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](https://docs.docker.com/engine/reference/builder/#arg) 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](https://learn.microsoft.com/nuget/reference/nuget-config-file#using-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](https://learn.microsoft.com/nuget/reference/extensibility/nuget-cross-platform-authentication-plugin) that provide a mechanism to authenticate to feeds both interactively and non-interactively. One such implementation of this is [Azure Artifact Credential Provider](https://github.com/Microsoft/artifacts-credprovider) 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](https://github.com/Microsoft/artifacts-credprovider#environment-variables) 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](https://learn.microsoft.com/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate). 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](https://learn.microsoft.com/azure/devops/pipelines/tasks/reference/nuget-authenticate-v1?view=azure-pipelines) 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](https://github.com/moby/buildkit) integration provides some enhanced functionality for passing [secret information](https://docs.docker.com/develop/develop-images/build_enhancements/#new-docker-build-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](https://learn.microsoft.com/dotnet/core/compatibility/core-libraries/6.0/system-drawing-common-windows-only). Official .NET images for Windows Server Core are available with tags documented on the [.NET container repos](../../README.md). Example tag: `mcr.microsoft.com/dotnet/aspnet:8.0-windowsservercore-ltsc2022`.

---

--- METRICS ---
- Files Extracted: 39
- Estimated Token Budget: ~49294 tokens
- Canonical Reference: https://codewiki.google/github.com/dotnet/dotnet-docker
