March 18th, 2015

A Nancy .NET microservice running on Docker in under 20mb

How to create minimal Docker images for your .NET Mono applications.

— Matthew Fellows —

After hearing about a colleague working on a microservices project in Golang, producing Docker images under 10mb (example), I became interested in how we might go about doing the same for other languages, in particular in the .NET/Windows environment where I have spent the last several months working on a continuous delivery project at SEEK.

For the impatient, there is a GitHub repo where you can skip straight to the fun.

Buzz words aside, let’s get stuck into it.

UPDATE: I have modified the project to now use a series of Docker containers responsible for building, testing and running the entire system. Vagrant is still used as a basic Docker `shell` so the build scripts can work cross-platform.

UPDATE 26/01/2016 – Apparently small Docker images are now being referred to as ‘Microcontainers’.

Storage is cheap, why do we care how big an image is?

Well, for one, its interesting. .NET is often seen as really big and heavy, possibly to do with it generally being associated with Windows which very much is big and clunky.

But more importantly, there are at least 3 good reasons for this:

  1. Security – the less stuff you have on a runtime machine the smaller your attack surface is. For instance; if you don’t need a shell, and didn’t install Bash, you wouldn’t ever had any chance of being exposed to shellshock.
  2. Performance – following 1), if there are services running that aren’t actually required, you will get better performance out of your containers – more CPU/Memory for your app.
  3. Continuous Delivery pipeline speed – you want your pipeline to be fast, and anything that gets in the way of that is a sin. Docker does cache intermediate layers so this can speed things up, but mitigating this with separate build and runtime images can assist (which we shall see how to do below).

Onwards to Tiny Town

So, the first step is getting a repeatable environment where we can create and build these images in an isolated and cross-platform way. I’m a big fan of Vagrant, so we create a Vagrantfile that will provision the Mono build environment onto a Debian Linux machine where we can build and run the Docker app (Look here for the non-Vagrant version). You will need Vagrant and Virtualbox before you can proceed if you are following along:

Now we need to create a (trivially simple) Application to test (full code):

as the folder is synced into the Vagrant VM, we can now build it within the VM directly:

Finally, again from within the Vagrant box, we can cURL the application to get our obligatory “Hello World” response:

curl $(boot2docker ip):8888
Hello World

Cool!!! Now, prove to me how small it is:

[email protected] ~/ $ docker images  | egrep "(mfellows|REPO)"
REPOSITORY           TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
mfellows/mono-api    latest              7c0b8d6ba28c        3 days ago          18.45 MB

Wow, that small – how’d you do it?!?

Creating our Dockerfile

Here is our Dockerfile:

OK, so I may have lied to you. Sorry about that. As you may have noticed, there is no Mono runtime environment on this image. In fact, there is really nothing on it at all part from a base OS, the App and a core system library. And this is the trick: if you want a small image, you need to statically compile the Mono binary.

The mkbundle
command is the key to doing this:

mkbundle  generates  an  executable  program  that  will contain static
copies of the assemblies listed on the command line.

It seems this is a fairly esoteric command, so I couldn’t find a lot about it on the Interwebs, but reading through the man pages combined with experimentation, I was able to get it working with this build script:

Put simply, this basically:

  1. Restores Nuget packages and builds the solution using xbuild, producing console.exe
  2. Finds all .dll files in the ./packages directly
  3. Creates a static binary (–static) consoleapp from console.exe, bundling all system dependencies it requires (–deps) and all of the .dll dependencies from (2) into it

The result is a binary that can run natively on Linux without Mono other dependencies. Let’s take a look at the file:

file ./consoleapp

ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, not stripped

So, there is still some work to be done to make it a real statically linked binary and also to strip it of its debug symbols (yep, we can make it even smaller!), but that is for a future post. For now, we have a 13mb binary.

The last step of build.sh is to build the docker image (line 14), which warrants a final explanation of our Dockerfile. In essence, it:

  1. Pulls the tiny 4.789 MB Busybox image
  2. Adds libc, the standard library in GNU for system calls
  3. Pops the static binary in and makes it the Docker container entry point

That’s all there is to it.

Image Pipelining

In our Machine Factory talk (+slides), I discuss the concept of machine pipelining and enrichment and this is the perfect candidate:

  • Create a Production Docker image (Base) with only the basic runtime dependencies for the application. No more, no less. In this case, this is our busybox image.
  • From this Base, we enrich the image with the Mono runtime and build dependencies so that we can automate the build, test and production of our static artifact.
  • From the CI Image, we add any extra dev dependencies we may need (e.g. debugger etc.) to run it locally

This gives us the benefits of fast, isolated and repeatable local development with the ability to have a higher degree of confidence that our environments are at parity with one another, and exceptionally fast build and deploy times – all things we need to achieve continuous delivery.

  • christian jacobsen

    Nice post! Tried to follow the installation but got a 404 on deb/jessie-amd64. Tried with debian/jessie64 which worked but then got errors concerning docker: “Package docker.io is not available, but is referred to by another package
    This may mean that the package is missing, has been obsoleted, or
    is only available from another source”

    • http://www.onegeek.com.au Matt Fellows

      Ahh! How annoying! It looks like at the time I created this post the docker package was not yet in ‘stable’ and was subsequently removed due to instability with dependencies or something. It should be possible to find a Wheezie box and modify the docker installation step. I’ll see what I can conjure up and modify the post in the next week or so.