6 Things To Know When Dockerizing Microsoft .NET Apps in Production
A checklist to run Microsoft .NET applications in Docker containers in a production environment

Many people enjoyed my articles about running Node.js or Python applications in a production environment with Docker.
Next to Node.js and Python, I also work with Microsoft.NET. Now, most things I wrote for Python and Node.js are also valid for .NET. But there are differences. This article describes the critical things when running .NET applications in a Docker container.
You can find all examples in this GitHub repository.
1. Choose the Right Base Image
It’s crucial to use the best image for running your application. You should choose the smallest image that has all the features your application needs. A small image travels fast across the network from the Docker registry to your Docker hosts. So your container starts quicker.
Instead of developing an image, you should always use the official .NET Docker images. These are designed and optimized by Microsoft. There are two types, one for development and another for running your application. We need the latter.
If you look at Docker hub, there are still many possible images. The choice comes down to Windows or Linux? If possible, select Linux because these images tend to be smaller.
I really like Alpine images. Alpine is a minimal Linux distribution with a hardened kernel. Below, I pulled some of the Linux images.

The 5.0 tag is the default image and the same as 5.0-buster-slim-amd64 which is based on Debian 10. The 5.0-focal tag is based on Ubuntu 20.04, and the 5.0-alpine-amd64 image is based on Alpine 3.13.
You see that the Alpine version is the smallest.
2. Use a Non-Root Container Image
By default, your app runs as root inside a container. Root in a container is not the same as root on the host. Docker restricts users in containers. But to decrease the security-attack surface, you’d want to run the container as an unprivileged user.
Non-root containers add an extra layer of security and are recommended for production environments. But, because they run as a non-root user, privileged tasks are not possible.
Some official images already create a non-root user, but most of the time you have to add it. Create a user in your Dockerfile with a known UID and GID, and run your process as this user. See the example below.
On the first line, I use the mcr.microsoft.com/dotnet/runtime:5.0-alpine-amd64
as the base image. Next, the statement on line 5 adds the app user and group. It sets the group and user id to 1000.
The instruction on line 8 creates the directory for the app. I set the new owner of the directory to the user app.
Usually, the WORKDIR
instruction sets the working directory and creates the folder. But, there’s no way to set the directory owner. So I manually create the folder and set the owner.
I set the user to the app user on line 12. The USER
instruction sets the username (or UID) when running the image. On the next line, I copy the binaries of my application to the working folder.
On the last line, the application starts using the CMD
instruction. This provides the defaults for executing the container.
3. Starting and Stopping Your .NET App
It’s crucial to start and stop your .NET app inside the Docker container in the correct way. In the Dockerfile shown before, I start the app using the CMD
instruction.
When you start your .NET app using the exec form of the CMD
instruction, you ensure that you can receive signals from the operating system (e.g.,SIGINT
and SIGTERM
) inside your application and handle them to shut down your application gracefully.
Now different from a Node process, a .NET application by default listens to these signals. It uses the ConsoleLifetime class that is wired up by default.
If you want your app to shut down gracefully, you can handle SIGINT
and SIGTERM
. SIGINT
is caught using Console.CancelKeyPress
and SIGTERM
using AppDomain.CurrentDomain.ProcessExit
. See below for an example.
4. Health Checks
A Health Check is a command used to determine the health of a running container. With the HEALTH CHECK
command in the Dockerfile you tell Docker how to test the container to see if it’s working.
It is important to note that the HEALTH CHECK
command does not work inside a Kubernetes cluster as it doesn’t handle the HEALTH CHECK
command.
Kubernetes uses probes to detect if your container is running correctly. It supports the following three probes.
- Liveness probe. This is for detecting whether the application process has crashed.
- Readiness probe. This is for detecting whether the application is ready to handle requests.
- Startup probe. This is used when the container starts up, to indicate that it’s ready.
Whether you create a Docker HEALTH CHECK
or a Kubernetes probe, you implement them using an HTTP endpoint. If the endpoint returns a success status code, the HEALTH CHECK
or probe is successful.
I will show you how to implement a Docker HEALTH CHECK
. Changing this to a Kubernetes probe should be relatively easy.
I created a standard WEB API project to demonstrate the health check. You can find it here. I added a new controller specially for receiving the health check requests.
On line 13, you see the action that is executed when the application receives a GET request on http://localhost/healthcheck
. You need to implement the actual checking of the health of your application. This depends on the type of application. You could, for example, perform a query on the database and map the result.
With this controller in place, we can add the HEALTH CHECK
command to our Dockerfile. We specify that to perform the health check the Docker runtime has to execute curl --fail http://localhost/healthcheck
. If this command returns an exit code other then 0, the container is not healthy.
The same controller action can be used as the url to call in a Kubernetes readiness probe.
5. Logging From Your .NET Application
Logging from a .NET application when running in a Docker container is simple. Log to stdout or stderr, depending on the situation.
The rationale behind this is to let something else handle the logging. This makes sense as we use Docker containers in a microservice architecture, distributing responsibilities among many services.
I wouldn’t recommend using Console.WriteLine
or Console.Error.WriteLine
. Instead, use the standard logging API with built-in logging providers. See row 15 in the example below.
6. Configuration Using Environment Variables
When your app is running inside a Docker container, Docker expects you to configure your app using environment variables.
Thankfully .NET 5.0 has some great default configuration options. When you generate an new ASP.NET Core web app with dotnet new or Visual Studio it generates the following code.
The method CreateDefaultBuilder on line nine provides the default configuration in the following order.
- Uses configuration from the
appsettings.json
file - Uses configuration from the
appsettings.[Environment.json]
file. - Uses Environment variables
- Uses command-line arguments
In each step you can overwrite settings from a previous step. This means that you can overrule any settings through the environment. For example by changing the logging level.

Note that you specify the hierarchy from the appsettings.json
using double underscores between the levels.
Conclusion
In this article, I described the following 6 points to improve running your .NET applications in Docker containers in a production environment.
- It is essential to choose the right base image for your container. The smaller your image, the faster it deploys and starts
- Always use a non-root container. Non-root containers add an extra layer of security and are recommended for production environments.
- Handle SIGINT and SIGTERM in your application so that you can shut down your application gracefully.
- Use Health checks to tell Docker or your cluster manager about the state of your running container.
- Don’t log to files or databases. Always use stdout and stderr. Use the default logging API with a built-in logging provider.
- By default, a .NET 5.0 application provides much functionality to configure your application. You can overrule all settings via Environment variables.
You can find all the examples in this Github repository.
Thank you for reading.