.NET, Docker and Visual Studio

2020-10-23 .net docker

Introduction

At my employer, theFactor.e, we started using Docker containers for our .NET application development. Since we have multiple developers with different Windows versions (Windows 10 (ranging from versions 1903 to 2004) and Windows Server 2016 / 2019) we used the default isolation mode (which was a mix of Hyper-V and process isolation modes).

This blog post will address this issue and will make sure we will use the correct image version so we can always use process isolation 🥳.

Windows versions

This blog post will go into detail on specific Windows versions and matching Docker images. When I talk about 1809, 1903 or 2004, I mean your Windows release ID. You can find this ID by running winver, or querying the registry at HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion and check the value ReleaseID.

Isolation modes?

There are two isolation modes. Hyper-V and ‘process’. Running a Docker container in Hyper-V isolation mode means it runs in a virtual machine. Better for security, but not so much for performance. When you are developing your own software, you want the best performance you can achieve, so we need to look into the process isolation mode.

Process isolation is running a Docker container on your host, sharing your kernel. It does not run in a virtual machine, which means better performance! 🎉

* Starting from Windows 10 1809 (which is two years old by now) Microsoft enabled process isolation mode for Windows 10 (it was already available for Windows Server).

Getting started with Visual Studio and Docker

When creating a project and enabling Docker support, Visual Studio will create a basic Dockerfile:

#Depending on the operating system of the host machines(s) that will build or run the containers, the image specified in the FROM statement may need to be changed.
#For more information, please see https://aka.ms/containercompat 

FROM mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-ltsc2019
ARG source
WORKDIR /inetpub/wwwroot
COPY ${source:-obj/Docker/publish} .

As Microsoft points out (in the first two lines of the dockerfile template), the image specified may need to be changed. When you use Docker Desktop you’ll have Hyper-V support so building above dockerfile will work perfectly fine.

However, when you try to build an image which derives from a non matching kernel version and you don’t use Hyper-V (which happens by default on Server versions of Windows, so maybe your build server), this will not work and you will get an error like:

Step 2/7 : FROM mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-2004
1>  4.8-windowsservercore-2004: Pulling from dotnet/framework/aspnet
1>  a Windows version 10.0.19041-based image is incompatible with a 10.0.17763 host
1>C:\Users\gfeiken\source\repos\Test app\Test app\Dockerfile : error CTC1014: Docker command failed with exit code 1.
1>C:\Users\gfeiken\source\repos\Test app\Test app\Dockerfile : error CTC1014: a Windows version 10.0.19041-based image is incompatible with a 10.0.17763 host

So, why is this important to fix?

Well, apart from not able to build your solution (due to the error above), you don’t want to use Hyper-V because of the performance penalty.

There’s not really a visible performance penalty when your application is running, but the stopping and starting of your application takes a lot longer with Hyper-V, so every second waited is a second wasted (and during the day, these seconds accumulate fast… 😅).

Updating your project

To automatically use the correct Docker image, you’ll first need to install my NuGet package in any of your projects: GerwimFeiken.Publishing (source available on GitHub).
This package will automatically run when you build your project to register an environment variable: GerwimFeiken_Publishing_WindowsReleaseId. We use this variable to set the correct base image version.

Now, change your FROM in your Dockerfile to this (assuming you use aspnet:4.8-windowsservercore)

ARG WINDOWS_VERSION
FROM mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-${WINDOWS_VERSION}

Now to inject the environment variable in your Dockerfile, we need to update our project .csproj file. Add the following:

<PropertyGroup>
    <DockerfileBuildArguments>--build-arg WINDOWS_VERSION=$(GerwimFeiken_Publishing_WindowsReleaseId)</DockerfileBuildArguments>
</PropertyGroup>

This will inject the environment variable GerwimFeiken_Publishing_WindowsReleaseId as the WINDOWS_VERSION build argument.

Now your project can be run by anyone in process isolation mode! 🎉

* Due to how Windows works, you might need to restart Visual Studio to make the environment variable into effect

Docker-compose projects

If your project uses docker-compose files rather than a single Dockerfile, we can also make this work. Here’s an example:

version: '3.5'

services:
  website:
    image: ${DOCKER_REGISTRY-}website
    isolation: 'process'
    build:
      context: .\publish
      dockerfile: ../../Dockerfile
      args:
        - WINDOWS_VERSION=${GerwimFeiken_Publishing_WindowsReleaseId}
    depends_on:
      - mssql
  mssql:
    image: myregistry.com/mssql/windows:${GerwimFeiken_Publishing_WindowsReleaseId}
    isolation: 'process'
    ports:
      - "1433"
    networks:
      - internal

In the example above both the service website and mssql use the environment variable GerwimFeiken_Publishing_WindowsReleaseId to run the correct image in process isolation mode.

Caveat

By using this strategy, we assume the image used, e.g. mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-2004 exists. Now, this is Microsoft’s repository (which means they’ll update it regularly and will match the available (and supported) Windows versions), but for your own repository you’ll need to maintain multiple images.

If you only had a single image before, e.g. myregistry.com/mssql/windows:ltsc2019 you’ll need to build a multitude of them now (tags taken from Docker Hub):

  • myregistry.com/mssql/windows:ltsc2016
  • myregistry.com/mssql/windows:ltsc2019
  • myregistry.com/mssql/windows:1903
  • myregistry.com/mssql/windows:1909
  • myregistry.com/mssql/windows:2004
  • myregistry.com/mssql/windows:2009

And Microsoft will deprecate older (out of support) and add newer versions to this list each release (which is every six months). So we need to automate this image management, which is a topic for another week. Stay tuned!

Update: the new blog post addressing this issue is now live: Enhancing Docker build pipelines based on external tags

FAQ

I’ve just installed GerwimFeiken.Publishing but it doesn’t work

Yeah, Visual Studio doesn’t listen to the WM_SETTINGCHANGE event. Which means you’ll need to restart Visual Studio as the environment variable GerwimFeiken_Publishing_WindowsReleaseId has either just been set or has been updated.

If it still doesn’t work, please create a new issue on GitHub and describe your problem as accurately as possible and provide steps to reproduce your issue if possible.

When do I choose Hyper-V over process isolation?

When you want:

  • Better security (because it runs in a virtual machine)
  • Run a different kernel then you have (e.g. run a Windows ltsc2019 image when you are running a newer version) - check the compatibility matrix posted by Microsoft