Docker for .NET Core Developers - from Zero to Hero

...
  • By Ivan Gavryliuk
  • In Docker  |  Containers
  • Posted 28/11/2017

I've decided to write this post as I'm seeing a lot of developers having diferent opinions about Docker and as I'm more interested in modern .NET rather than other platforms, I wanted to shed some light on this topic.

What Docker is and what it isn't

First of all, what is Docker? In my humble opinion, it's just an evolution of a Virtual Machine. Of course it's much more than than, but let's be honest, Virtual Machine is also much more than Docker. The both have advantages and disadvantages, however I'll concentrate only on Docker advantages against VMs in this post.

Installing on Windows

Before I start, I'd assume you have some minimal knowledge of Docker and have it installed. If you are working with .NET, you are probably using Windows. It sounds like a stereotype these days, because .NET is not Windows anymore (only classic .NET is). However, if you came a long way in .NET you are probably coming from Windows.

To install on Windows, just grab an installer and click the usual next-next thing, so you get the docker running. Then switch to Linux containers mode, because it's more challenging and exciting to get .NET running on another OS.

Apparently if it says "switch to windows containers" you are already in linux mode.

Building a simple app for Linux

To understand how to build an app for Linux you have to understand basics of Docker but basically you'll build your linux app on top of an official image from Microsoft. Follow this link and you'll see a lot of tags, but the two most important ones are sdk and runtime. They have different sizes and serve a slightly different purpose:

Sdkruntime

Tags

sdk tag references an image which includes everything you need to build an application from source code i.e. compilers, tasks and other stuff.

runtime tag gives you only .NET runtime which can run your compiled application, so you cannot build the source code there.

There are plenty other tags, mostly allowing you to reference a specific version of runtime or sdk, instead of referencing the latest one, like

microsoft/dotnet:<version>-sdk or for latest microsoft/dotnet:sdk

microsoft/dotnet:<version>-runtime or for latest microsoft/dotnet:runtime

Compiling an app

How do you decide which image to reference? Well, to be absolutely sure it works on Linux, it would be nice to build it on Linux, right? So we'll use the sdk image to build our code under Linux.

To be absolutely clear and simple, i've created a simple one-liner console app like this:

using System;
using System.Runtime.InteropServices;

namespace netcorelinux
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World! ({0})", RuntimeInformation.OSDescription);
        }
    }
}

All it does is prints "Hello World" and current OS it runs on, so when I run it on my Windows dev machine the output is:

Dotnetrunwindows

So let's make it work on Linux. To do that I'll put a new text file called Dockerfile (no extension) and use SDK image to build it:

FROM microsoft/dotnet:sdk
WORKDIR /app

COPY *.csproj ./
RUN dotnet restore

COPY . ./
RUN dotnet publish -c Release -o out
ENTRYPOINT ["dotnet", "out/netcorelinux.dll"]

Let's go over it line by line:

  1. Referencing the latest .NET SDK from official docker repository
  2. Setting current directory in our new image to /app. It simply means that all further docker commands working with files will treat /app as current folder in our new image.
  3. .csproj gets copied into current folder (./app).
  4. dotnet restore command downloads all the required packages set in .csproj
  5. dotnet publish builds and publishes our source code to out folder in this container.
  6. ENTRYPOINT command says what to run when a new container is created from our image - in this case we're just launching the application from out folder.

Note that all of this runs inside a Linux container, using the SDK image referenced in the first line.

Once the file is saved, we can build the new image by typing docker build -t dev2411 . - essentially this command creates a new image called dev2411 from current folder (.):

Dockerbuilddev

Your output may differ slightly if Docker needs to download missing images, they're already cached in my case.

And typing docker images reveals the image locally:

Dockerps

Ok, let's run it now by typing docker run --rm dev2411:

Dockerrun

Yeah, it runs, and current OS is Linux!

Multi-stage builds

Building an app from a Docker image just works, however there is only one slight optimisation you may want to do. So far we're built an app which references an SDK image, which is quite big (~500Mb) whereas a runtime image is small (~95Mb) so it would be nice to have an image on top of runtime, right?

The only problem here is that we need to build our app using tools included in SDK image, which are not available in runtime image! To achieve this, you can utilise something called multi-stage builds. Let me show you a solution and explain how it works. Here is a complete Dockerfile performing a multi-stage build:

FROM microsoft/dotnet:sdk AS build-env
WORKDIR /app

# build the app in SDK container
COPY *.csproj ./
RUN dotnet restore
COPY . ./
RUN dotnet publish -c Release -o out

# copy to prod container
FROM microsoft/dotnet:runtime
WORKDIR /app
#now it's not in _out_ but in the root
COPY --from=build-env /app/out ./

ENTRYPOINT ["dotnet", "netcorelinux.dll"]

Still pretty small, right? What's happening here is we still use SDK image for building our project, just like in a previous Dockerfile, however later we switch to a runtime image, and copy build output from the out folder to the root folder of out new image. Pretty neat.

You can run that image just like before, but it's referencing a much smaller image!

Thoughts

Local development

What we've done before looks a bit awkward. Why on earth would I wrap my development pipeline in a Docker container, if I can run this locally on Winodows just as good, especially considering .NET is cross-platform? Not sure.

You might need some Linux specific API of course, but then there is a Windows Subsystem for Linux which is just as good.

Debugging in a docker container is a bit of a pain, and build times increase dramatically.

Sharing applications between development teams

I'm not entirely sure it's fit for this. If your dev team needs to access a specific application, it's much easier just to publish it to a development server and give an address to your colleague. This still works best for web sites, APIs, etc.

The may be cases when sharing Docker images is better, but I can't think of any.

Continuous Integration

I think that's where Docker really shines. CI/CD is hard. You need to provision a proper virtual machine with correct dependencies, some of them conflict with each other if there are multiple applications requiring different runtimes. There are tricks to manage this, like embed runtimes within applications, correctly configure them etc. but this is hard and error-prone. Docker is just perfect for this - you create a self-sufficient image with everything in it and deploy all of it at once.

The price you are paying for this is relatively low comparing to the benefits.

Final thoughts

In general, in my understanding and wonderings around Docker, there is not much you can utilise during development, as managing containers just adds a bit of headache every time you try to write and run code locally. Everything becomes more complicated - running your app, debugging, etc. Build times generally increase dramatically on large solutions, as every time you need to build an image you'll start from a clean filesystem, instead of performing an incremental update.

Nevertheless, Docker is great for many reasons, but is not really helpful on development stages I'd say at all.


Thanks for reading. If you would like to follow up with future posts please subscribe to my rss feed and/or follow me on twitter.