A container journey: .NET 5 web app dockerization

Martin Horvath
9 min readJan 3, 2021

I spent a few hours during the Christmas holidays to see if I could migrate my .net5 (aspnetcore) app and all of it’s components into Docker on my shiny new Synology DS220+ NAS. This is part one of the journey and the part-time goal is a .NET5 web application in a local Docker.

My .NET 5 (Microsoft removed the core branding recently) application helps me automating my day-to-day tasks, comes with reports about my time tracking and serves as a playground to try out new technologies.

Image by Julius Silver from Pixabay


Create a asp.net core webapp

If you have a webapp to work with, skip this. Otherwise don’t worry, dotnet comes with a “new” command that allows the creation of applications based on templates. We’ll use the “webapp” template to create a web application based on the official documentation: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-new

Inside Visual Studio Code, open a new terminal, create a folder for your new app and the app itself with the dotnet command. Finally open the app in VS Code with File -> Open folder.

cd ~
mkdir aspnetcore-webapp
cd aspnetcore-webapp
dotnet new webapp
App opened in VS Code. launch.json is generated later

There’s a small tweak required

To make the webapp later available for connections outside of the container, we need to tell the startup to UseUrls. Another thing we’re gonna prepare is the support for docker container environment variables.
Open your Program.cs and replace the default CreateHostBuilder with this one:

public static IHostBuilder CreateHostBuilder(string[] args) =>
.ConfigureWebHostDefaults(webBuilder => { webBuilder
.ConfigureAppConfiguration((hostingContext, config) => {
config.AddEnvironmentVariables(prefix: "primebird_");

Give it a quick shot and debug the application to ensure it’s working. This can be done using the Run -> Start debugging menu entry and choose net core if prompted. This will create the launch.json file seen above. The welcome page above is what you should then see in the browser. Check the vs code debug output for the port, most likely it’s 5001.

This looks good so far and we can publish this for the next step. Publishing a .net app means that binaries (dll files and more) are compiled. You can either publish a Release or Debug package. I prefer the Debug flag for local experiments as some logging etc. inside the .net app (might) behave differently. Never do this if you are targeting or packing production code.
Open a new terminal in VS Code (if you don’t have one) and run this command:

dotnet publish --configuration Debug

Your folder structure should then look as follows (This is just an extract, you’ll have more files and folders, but ensure those are present):

<Some location on your disk>
- aspnetcore-webapp
-- bin
--- Debug
---- net5.0
----- publish
------ wwwroot
------ aspnetcore-webapp.dll
------ aspnetcore-webapp.Views.dll
-- Pages
-- wwwroot
-- Program.cs
-- Startup.cs

Verify your built files work

Before continuing to docker, ensure a last time that your binaries work. If you haven’t stopped the debug mode, do this now. You can run the webserver with the following command (from the open terminal):

dotnet bin/Debug/net5.0/publish/aspnetcore-webapp.dll

Open once again the webapp by hitting https://localhost:5001. The terminal tells you where the server can be accessed:

info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Now listening on: https://localhost:5001
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: /home/martin/Development/aspnetcore-webapp

Hello Docker

We need Docker for two reasons: To create a docker-image for our webapp and to run it as a container. Let’s start with understanding what a docker container actually is and how to create one:

A Docker image consists of read-only layers each of which represents a Dockerfile instruction. The layers are stacked and each one is a delta of the changes from the previous layer.

I like to compare concepts with real world examples to boost understanding:

| Docker | This example | Real world |
| Layer 0 | mcr.microsoft.com/dotnet/aspnet:5.0 | Pizza base |
| Layer 1 | A to be built layer with our app | Tomato sauce |
| Layer 2 | We don't need another layer... | Cheese |
| Layer 3 | We don't need another layer... | Toppings |

If you make Pizza, you probably buy the super delicious pizza base from your local pizza baker and complete the pizza at home. You follow the instructions in the recipe (this is in Docker terms a Dockerfile) of your grandma to make the awesome tomato sauce and put it on top of the base. Then the cheese, the toppings and altogether it’s baked.

You don’t care much about the pizza base ingredients or how they were put together because the base is ready, but the recipe of grandma could look as follows:

For the pizza, prepare the following and put onto the base:
- Chop tomatoes, onion, garlic and basil
- Braise lightly
- ...

Dockerfile for aspnetcore-webapp

Now that you have a rough idea about Docker containers, let’s get things ready for our .net app. First, we create a new Dockerfile in the root of the created app and open that file (in Visual Studio Code or any editor).

touch Dockerfile

This means that the aspnetcore-webapp will become a layer. Therefore we need to choose an image to stack onto. What we are looking for is an operating system image that is able to run asp.net applications. Luckily Microsoft prepared base images for our convenience and we can make use of mcr.microsoft.com/dotnet/aspnet:5.0. Images, names and tags have changed a lot in the past. Following the readme-breadcrumbs leads (at the time of writing) to this docker-registry entry: https://hub.docker.com/_/microsoft-dotnet-aspnet

So let’s put together the Dockerfile by adding the following content:

FROM mcr.microsoft.com/dotnet/aspnet:5.0
COPY bin/Debug/net5.0/publish/ app/
ENTRYPOINT dotnet aspnetcore-webapp.dll

We take the aspnet:5.0 image as a basis. This is a small Linux OS with the .net5 runtime installed and just essentials enabled. If you are using an existing app which is probably targeting .net core 3, then you need to choose the proper base image.

The first line tells the build command (that follows later) to pull it directly from dockerhub if it is not locally cached.

The second line starting with COPY then copies the whole publish-folder from the local project (created by the dotnet publish command) into the new containers “app” folder.

The third line then moves inside the container into the app folder where we copied our published app to.

The fourth line contains the entrypoint, which is a direct shell command in this case — to be executed when the container is run.
Note that you fired the same command to test your compiled webapp in an earlier step!

Build the .net app docker image

Now we have all the bits and pieces ready to build our Docker image. For this, all we need is a terminal window and it’s ok to use the one inside Visual Studio Code. Just ensure you’re in the root folder of your application (i.e. where the Dockerfile is).
If you experience any socket permission denied errors, run the docker command as sudo! (i.e. sudo docker …)

docker build -t aspnetcore-webapp .
Sending build context to Docker daemon 10.93MB
Step 1/4 : FROM mcr.microsoft.com/dotnet/aspnet:5.0
---> 5f9a6a778eac
Step 2/4 : COPY bin/Debug/net5.0/publish/ app/
---> c4d100cb93ba
Step 3/4 : WORKDIR /app
---> Running in ced507c950bc
Removing intermediate container ced507c950bc
---> 38e5cb66ac2b
Step 4/4 : ENTRYPOINT dotnet aspnetcore-webapp.dll
---> Running in ffa21d95c08d
Removing intermediate container ffa21d95c08d
---> bf1e224f5191
Successfully built bf1e224f5191
Successfully tagged aspnetcore-webapp:latest

The -t option allows us to tag the to be built image and the period at the end is important to use the current directory for the build.

We now can see this image registered in our local docker images. Below command is chained with grep to filter for all images with the name aspnet in it’s name.

~$ docker image ls | grep aspnet
aspnetcore-webapp latest bf1e224f51 53 sec ago 210MB
mcr.microsoft.com/dotnet/aspnet 5.0 5f9a6af1x9 3 weeks ago 205MB

Even if we have created an image in the last step, no physical file in the working directory was created. Images are built and directly registered in the local docker. If you want a hard copy of the image, you can use docker save to extract an archive and store it somewhere (https://docs.docker.com/engine/reference/commandline/save/).

Create a self signed certificate for the webapp

We’re nearly done but an important step for running the webapp in the container is missing: The SSL certificate. We will create a self signed certificate for the sake of simplicity.

Do not use this for production purpose!
For your convenience, I’ve uploaded a sample pfx to the projects github repo: https://github.com/martinhorvath/com.primebird.net5webapp

First, we create a folder that we can later share with the docker container:

mkdir ~/dockershare
cd ~/dockershare

Openssl can be used to create such a PFX certificate, but there are also webservices out there where one can download a development certificate to play with. Choose any password you like during the certificate creation.

openssl req -x509 -newkey rsa:4096 -sha256 -keyout devkey.key -out devcert.crt -subj "/CN=dev.local" -days 600openssl pkcs12 -export -name "dev.local" -out devcert.pfx -inkey devkey.key -in devcert.crt

Double check the full path of the folder and ensure it contains the devcert.pfx (in my case it is /home/martin/dockershare/devcert.pfx and I’ll further refer to it as <dockersharepath>)

Run the image inside a container

With everything in place, we can now run that image inside a docker container. With below command, docker will create a new container from that image and run it.

  • d tells docker to run the container in the background
  • p 5001:5001 exposes port 5001 to the outside and maps to port 5001 inside the container. I’ve seen some base images automatically listening on port 80 internally. This leads to a website not found error later. In this case, change the mapping from 5001:5001 to 5001:80. The container log will tell you on which port the app is listening.
  • e is used to set environment variables and we need two of them: ASPNETCORE_Kestrel__Certificates__Default__Password to share the certificate password with the container and ASPNETCORE_Kestrel__Certificates__Default__Path to tell the .net webapp where to load the certificate from. This path is relative to the container.
  • mount can be used to share a folder from your machine with the docker container. The source is a full path on your local machine. The target is a path inside the container, created at runtime.
  • name allows us to customize this containers name
  • the last part is the image name aspnetcore-webapp
docker run -d -p 5001:5001 --name aspnetwebapp-ct1 -e ASPNETCORE_Kestrel__Certificates__Default__Password=<passwordForTheCertificate> -e ASPNETCORE_Kestrel__Certificates__Default__Path=/opt/webapp/devcert.pfx --mount type=bind,source=<dockersharepath>,target=/opt/webapp aspnetcore-webapp

After that, the container is registered in the local Docker and can be checked with the following command (again filtered with grep):

docker ps -a | grep aspnet
3b3757573b92 aspnetcore-webapp "/bin/sh -c 'dotnet …" 56 seconds ago Up 55 seconds>5001/tcp aspnetwebapp-ct1

At this time, the website is also available (again) on http://localhost:5001


Things can go wrong… sorry :-)

docker container logs 3b3757573b92
  • Website not found but container is running: This is most likely due to a wrong port-mapping. Switch the p-parameter, stop and remove the container and run it again (ID=3b3757573b92 comes from docker ps -a):
docker container stop 3b3757573b92
docker container rm 3b3757573b92
docker run -d -p 5001:80 ...same as above
  • Container does not start: It is super important that the paths to the pfx are correct. Double check the mount-target and the path to the pfx.



Martin Horvath

I'm a consultant working on international projects in the field of geospatial data and customer experience, with a passion for technology and mountaineering.