Dabbling with .Net Core on Docker.
I have a couple of legacy apps that I am migrating to .Net core and I wanted to make them easier to deploy so I decided to take a look at Docker. Although this particular app is based on ServiceStack, the process applies to any app. The first real step I took was to decide which version of .Net Core to target. In my case I went for the LTS which is currently 3.1. For this particular app, which is a web service, I wanted to add a few web pages. I’d also decided that this app needed a fresh start. It’s grown organically over quite a few years and this has created more than a few issues, some of which can be eliminated with a rewrite. Additionally I can make greater use of the facilities that ServiceStack makes so easy to use. So I setup a new project using the Nuxt template which happens to target .net core 3.1. After adding in various features using the mix tool I had the skeleton of an app with authentication, caching, MQ access, logging etc.
Looking around on the web there are a number of guides to creating Docker containers but some of them seem to give different or conflicting advice. This seems to be due to the power of Docker and the fact some things can be done more than one way. What I am going to describe here is what worked for me for the app above. I’m assuming you have dabbled a little with Docker and installed a container or two but aren’t particularly confident with it yet and that you have an app all ready to be containerised. My docker uses Linux containers, although this should work for Windows containers it’s not been tested. I also tend to use the command line for this sort of stuff — you have been warned! Having said that, I do like the Docker extensions for VS Code.
The first step is to publish the app. In my case this was a simple
dotnet publish -c Release
This compiles the app into a publish folder, in my case the folder is at
.\myWebService\bin\Release\netcoreapp3.1\publish\
if you look in that folder you will see the myWebService.dll file.
Before we go further we need a .dockerignore file.
[O|o]bj/
this helps to keep the size of the container down. The original suggestion I saw included [b|B]in/
in the dockerignore but this breaks the Dockerfile below as it means it can’t copy the contents of the publish folder.
The next step is to add the Dockerfile. Mine is in the root of the startup project, you can use a template if you wish but it’s a simple text file called Dockerfile (with no extension). In my case I had added this file by using the mix tool and it contained the following
FROM microsoft/dotnet:2.1-aspnetcore-runtime
COPY app /app
WORKDIR /app
EXPOSE 5000/tcp
ENV ASPNETCORE_URLS https://*:5000
ENTRYPOINT ["x"]
There are a few issues with this one that meant it wasn’t suitable. The first thing is that we are using .net core 3.1. Looking at the MS docker site it was changed to the following
FROM mcr.microsoft.com/dotnet/core/sdk:3.1
The next line copies the published files into a folder in the container, so in my case the publish folder relative to the Dockerfile is bin/Release/netcoreapp3.1/publish/ and I decided to use a (new) folder called myWSFolder in the container, so the second line becomes
COPY bin/Release/netcoreapp3.1/publish/ myWSFolder/
I also want to make the new myWSFolder folder in the container the working directory with
WORKDIR /myWSFolder
Finally I changed my entrypoint to the command I would use to run the app from the command line ie
ENTRYPOINT ["dotnet", "myWebService.dll"]
so my Dockerfile now looks like this
FROM mcr.microsoft.com/dotnet/core/sdk:3.1COPY bin/Release/netcoreapp3.1/publish/ myWSFolder/WORKDIR /myWSFolderEXPOSE 5000/tcpENV ASPNETCORE_URLS https://*:5000ENTRYPOINT ["dotnet", "myWebService.dll"]
The line EXPOSE 5000 just tells docker that the app runs on port 5000 and ENV ASPNETCORE_URLS https://*:5000 sets an environment variable, in this case the standard aspnetcore_urls variable.
We’re now ready to create the container image.
From the command line, firstly make sure you are in the same folder as your Dockerfile. Now you can run the build command
docker build -t myWebService -f Dockerfile .
Firstly note that the . at the end of the line is important (and easily missed). this tells docker to look for the Dockerfile in the current directory. When this command completes you will have a docker image as you will see if you run
docker images
This command will give you an image ID which will be something like c4155a9104a8 yours will be different to this. You need this ID to create the container.
Now you have the image you can create the container as follows
docker create --name myContainer myWebService c4155a9104a8
This creates a stopped container called myContainer, note that it’s using the ID. If you now look at the Docker dashboard you will see your container or you can use
docker ps - a
you can now use the name you set to start the container
docker start myContainer
or you can use the dashboard.
The sharp eyed among you may suspect that the last part of this is a little long winded and you would be correct. My argument is that it never hurts to have an understanding of whats going on under the hood. The docker create and docker start can be replaced with a more familiar and simpler command which you should be reasonably familiar with
docker run -d -p 8080:5000 --name myContainer myWebService
Creating a private registry
Now you have created a container you may need somewhere to store it so others can play with it. If its for example a dev container you don’t want to put it on Docker Hub. One option is to setup you’re own registry on a local server. Unsurprisingly this can easily be done using Docker using something like
docker run -d -p 5000:5000 --restart=always --name registry registry:2
Of course now that we have our own registry that we can push and pull from we will need tags. One way to do this is to alter the docker build command from above. If for example we use
docker build -t myWebService/myTag-f Dockerfile .
then we will be creating an image with the tag myTag
You can also use the Docker Tag command. For example, if I have already run the build command above — so my tag is myTag, I can set this to a version as follows
docker tag myWebService/latest myWebService:2.3
and now it has version 2.3.
Data in Docker
If you’re app needs access to the file system e.g. for storing data, then you should be looking at docker volumes. I have a windows host so I want to map to a folder on that. The first step is to make sure the drive is shared. Go to Docker settings from the icon, and then Resource and file sharing. In my case I have a folder at d:\myData. As you can see below my D drive is already shared.
Clicking the + near the bottom would allow me to share D:\myData
it’s simple to turn that into a volume using
docker volume create myVolume
We now have a volume called myVolume.
This can easily be added to any container by altering the docker run command
docker run -d -p 8080:5000 --name myContainer myWebService --volume myVolume:/usr/share/my/Container/Mount
This maps myVolume to /usr/share/my/Container/Mount in the container.
note that if you use -v/- -volume that the order of the fields is significant, if you’re unhappy with this you should investigate - -mount.
If you want the volume to be read only for a given container you can specify with a third parameter e.g.
docker run -d -p 8080:5000 --name myContainer myWebService --volume myVolume:/usr/share/my/Container/Mount:ro
makes the volume read only from within the container.
Updating
Clearly there are two types of update that can be required for a .net core app. The first is that the version of .NET needs to be updated. Assuming the app still works on the new framework then this is just a case of changing the Dockerfile to reference the newer version. Primarily this involves changing the FROM line to a reference to the newer image. It might also be worth checking out any new conventions that are being used by Microsoft with regard to their docker files.
Updating the app
The process for this is relatively straightforward, you basically replace the existing container with a new one based on the new code. So for my example
docker stop myContainer
docker rm myContainer
removes the container, we now run our build and run commands from above e.g.
docker build -t myWebService:3.0 -f Dockerfile .
docker run -d -p 8080:5000 --name myContainer myWebService --volume myVolume:/usr/share/my/Container/Mount
Note that I have changed the tag as this is a new version.
The first two commands — the ones that remove the container can be merged into one, so we end up with
docker rm -f myContainer
docker build -t myWebService:3.0 -f Dockerfile .
docker run -d -p 8080:5000 --name myContainer myWebService --volume myVolume:/usr/share/my/Container/Mount
If that’s too complicated for you then there is always something like WatchTower
You are now in a position to quickly rollout your application using your chosen CI/CD platform.
Troubleshooting
We all know things never go smoothly all the time. This is an ongoing list of issues I came across and how I dealt with them
Failed to Copy
I wanted to put out a new version. When I tried to use the docker create command above I got an error that it couldn’t copy to a specific folder. Eventually I realise this was due to the dockerignore file as I was copying from the bin folder and that was in the exclusions. Simply commenting out the bin foldgner from the ignore file resolved this issue.
Container failed to start
After I got the container setup it refused to start. In order to test it I ran it from the publish folder using the command line
dotnet myWebService.dll
and it should work as expected.
In my case I needed to check the logs. I used the dashboard for this and the exception that came up gave me enough of a clue to resolve the issue.