Previous posts in this series:

Running applications in Docker

It was all the fuss around Docker, and particularly various Cloud vendors' enthusiastic adoption of Docker (Azure, Amazon, Google, etc), that started me down this route in the first place, so let's take a look at how to run an ASP.NET 5 application using Docker.

What is Docker?

The Docker software is a tool for working with containers: packaged bundles of applications and dependencies that can be run self-contained on Linux servers. They're like virtual machines, except they share the kernel and other resources of the OS they're running on, so they're more efficient in terms of memory and CPU usage, which means you can run more of them on the same hardware. It also means you can run Docker inside an actual VM.

To package something for Docker, you create a Dockerfile containing configuration data, start-up commands and so on. Most Dockerfiles reference a parent Dockerfile to use as a base.

The Docker company maintains the software and runs a Registry where people and companies share public Dockerfiles for other people to use.

Installing Docker

There is a docker.io package in the Ubuntu package sources, but it's an old version, so you're better off installing from Docker's own package source:

$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 36A1D7869245C8950F966E92D8576A8BA88D21E9
$ sudo sh -c "echo deb https://get.docker.com/ubuntu docker main > /etc/apt/sources.list.d/docker.list"
$ sudo apt-get update
$ sudo apt-get install lxc-docker

Once that's finished, check it installed OK:

$ docker --version
Docker version 1.4.1, build 5bc2ff8  

sudo docker

Docker runs as a daemon on your machine, and you communicate with it using the docker command. This works over a Unix port, which is owned by the root user, so you have to use sudo docker every time you want to do something. This gets really annoying, so there's a workaround: you create a Unix group called docker, which then gets given read/write access to the port when the daemon starts. Here's how to do that (replace ${USER} with your username):

$ sudo groupadd docker
$ sudo gpasswd -a ${USER} docker
$ sudo service docker restart
$ newgrp docker

(Kudos to AskUbuntu user Rinzwind for that one.)

Creating a Dockerfile for ASP.NET

Microsoft have published a base image for ASP.NET 5 applications on the Docker Registry, but at this time of writing it is (a) still using the beta1 release of ASP.NET and (b) based on Mono 3.10, whereas the current version of Mono is 3.12.

So I decided to create my own ASP.NET Dockerfile.

My first attempt used the official Mono repository, but while attempting to install some other dependencies into my own containers I kept running into compatibility issues. I found that the official Mono image is based on Debian Wheezy which, whilst it is the current stable version, is built on the Linux 3.2 kernel, which is three years old now. Three years is an eternity in Linux time: the current "longterm" kernel version is 3.14 (yay, Pi!), and the latest "stable" version is 3.17.

So I decided to create my own Mono Dockerfile.

My Mono Dockerfile

Don't worry, I didn't go completely mad and create my own from-scratch (or FROM scratch) base image.

I copied the official Dockerfile for Mono 3.12 and changed the FROM debian:wheezy line at the start to FROM ubuntu:14.04 to use the latest Ubuntu LTS release. I also added back in the mozroots command because it didn't seem to work without it. I've published my Dockerfile to the public registry, and it looks like this:

FROM ubuntu:14.04

MAINTAINER Mark Rendle <[email protected]>

#based on dockerfile by Michael Friis <[email protected]>

RUN apt-get update \  
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*

RUN apt-key adv --keyserver pgp.mit.edu --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF

RUN echo "deb http://download.mono-project.com/repo/debian wheezy/snapshots/3.12.0 main" > /etc/apt/sources.list.d/mono-xamarin.list \  
    && apt-get update \
    && apt-get install -y mono-devel ca-certificates-mono fsharp mono-vbnc nuget \
    && rm -rf /var/lib/apt/lists/*

RUN mozroots --machine --import --sync --quiet  

Apart from the FROM directive, which specifies the base image, this Dockerfile only uses RUN directives. These are shell commands that are executed as the image is built, rather than when an instance of the image is launched. The ones above

  • Update the APT index and install curl
  • Add a key to the APT trusted keys
  • Add the Mono Projects own repo to the APT package sources and install mono-devel, fsharp and some other packages
  • Import some trusted root certificates from Mozilla's LXR store

So when you launch an instance of this image, for example like

$ docker run -t -i markvnext/mono /bin/bash

you'll have all those things available in the bash shell that starts in the context of that container.

My ASPNET Dockerfile

Once I'd published the Mono Dockerfile to the Registry, I was able to create my own variation of the official ASPNET Dockerfile using it as the base:

FROM markvnext/mono

ENV KRE_VERSION 1.0.0-beta2  
ENV KRE_USER_HOME /opt/kre

RUN apt-get -qq update && apt-get -qqy install unzip supervisor  
RUN mkdir -p /var/lock/apache2 /var/run/apache2 /var/run/sshd /var/log/supervisor

RUN curl -sSL https://raw.githubusercontent.com/aspnet/Home/v$KRE_VERSION/kvminstall.sh | sh  
RUN bash -c "source $KRE_USER_HOME/kvm/kvm.sh \  
    && kvm install $KRE_VERSION -a default \
    && kvm alias default | xargs -i ln -s $KRE_USER_HOME/packages/{} $KRE_USER_HOME/packages/default"

# Install libuv for Kestrel from source code (binary is not in wheezy and one in jessie is still too old)
RUN apt-get -qqy install \  
    autoconf \
    automake \
    build-essential \
    libtool
RUN LIBUV_VERSION=1.1.0 \  
    && curl -sSL https://github.com/libuv/libuv/archive/v${LIBUV_VERSION}.tar.gz | tar zxfv - -C /usr/local/src \
    && cd /usr/local/src/libuv-$LIBUV_VERSION \
    && sh autogen.sh && ./configure && make && make install \
    && cd / \
    && rm -rf /usr/local/src/libuv-$LIBUV_VERSION \
    && ldconfig

ENV PATH $PATH:$KRE_USER_HOME/packages/default/bin

CMD ["/usr/bin/supervisord"]  

Here you can see two new Docker directives:

  • ENV sets environment variables
  • CMD sets the default command to be run when an instance of the container starts.

Which means in this Dockerfile, we are:

  • Setting the KRE_VERSION and KRE_USER_HOME environment variables
  • Installing unzip and supervisor (see below)
  • Installing the KVM bootstrapper
  • Installing the bits necessary to build libuv from source
  • Building libuv 1.1.0 from source
  • Setting supervisord as the default command for instances

We covered libuv in part one, so the new thing here is Supervisor.

Supervisor

Whatever you put into CMD in a Dockerfile gets run as process 1 on the contained instance. The process with PID 1 is usually /sbin/init (or some alternative) which is the ultimate parent of all processes running on the machine. It is not necessarily the best plan to run an actual application as this process, so a lot of Docker best practice guides recommend using a process manager. I experimented with a few and Supervisor was the simplest to work with. It's a Python app, but Ubuntu comes with Python pre-installed so there's not a lot of overhead.

An additional benefit to using Supervisor is that it will relaunch processes when they terminate unexpectedly. This means that if k kestrel crashes with an unhandled (or unhandleable) exception, Supervisor will calmly restart it and your site will keep running. (Docker itself has restart policies to do this at the container level, but (a) that only works for single-application containers, and (b) it takes longer.)

Oh, and it also works around a minor bug(?) in the current version of the k command that requires an attached TTY to stay alive.

Supervisor is configured using a supervisord.conf file that looks something like this:

[supervisord]
nodaemon=true

[program:kestrel]
directory=/app/MyApp  
command=k kestrel  

The nodaemon entry tells Supervisor not to run in daemon mode; Docker handles the daemonization for us, so we want Supervisor to run as a normal program.

You can have multiple [program:xyz] sections in the configuration file, and Supervisor will run them in the order they appear in the file. In this example, a process is started which changes the current working directory to /app/MyApp and runs k kestrel.

Containing an application

Now we have a base image which has Ubuntu 14.04, Mono and ASP.NET 5 (plus some other dependencies) installed, so we can create a simple container on top of that to hold an application. I've put a sample app that you can try up on GitHub. The Dockerfile for that looks like this:

FROM markvnext/aspnet

ADD ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf

ADD . /app/  
WORKDIR /app  
RUN kpm restore  
EXPOSE 5004  

So here we've got yet more Docker directives:

  • ADD copies files and directories from the current directory to the specified location on the container:
    • here we're copying supervisord.conf into the /etc hierarchy, where supervisord looks for it;
    • and the entire contents of the current directory (the one from where we call docker build in a minute) into an /app directory
  • WORKDIR sets the working directory for subsequent commands at build time
  • EXPOSE tells Docker to expose TCP ports to be available to external consumers; in this case we're exposing 5004, which is the default port used by Kestrel.

Notice that we don't have to specify the CMD directive in this file; it uses the one it inherits from markvnext/aspnet.

If you clone the sample repository and cd into it:

$ git clone https://github.com/markvnext/first-docked-app.git
$ cd first-docked-app

then you can build your own container like this:

$ docker build -t firstapp .

The -t firstapp flag just tags the container with a friendly name.

That's going to take a while because it's going to download the Ubuntu, Mono and AspNet images from the registry, as well as run the kpm restore for your own application. Depending on the speed of your internet connection, you can either make a cup of coffee or watch an episode of your preferred primetime drama series. Subsequent builds will use cached copies of those images, unless they've been updated.

Once the build has finished, you can launch an instance like this:

$ docker run -d -p 5004:5004 firstapp

The -d flag tells Docker to run the container "daemonized", that is, as a background process.

The -p 5004:5004 flag links the exposed TCP port 5004 on the container to the 5004 port on the host machine. The host port is first, and the container port second, so if you wanted the application to be available from the host on port 8080, you'd use -p 8080:5004.

Now you should be able to browse to http://localhost:5004/ and see the ASP.NET MVC start page in all its Dockerized glory.

Working with running containers

You can see all running containers using:

$ docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS                    NAMES  
83b37fb7f55b        firstapp:latest     "/usr/bin/supervisor   4 seconds ago       Up 3 seconds        0.0.0.0:5004->5004/tcp   jolly_bell  

Stop a container using:

$ docker stop 83b37fb7f55b

Pro-tip: docker supports bash auto-completion, so you can just type docker stop 83[TAB] to expand the ID. Alternatively, you can just run docker stop 83 as long as that uniquely identifies the container.

Start a previously-stopped container again:

$ docker start 83b37fb7f55b

Clean up after yourself:

$ docker rm 83b37fb7f55b

Show stopped containers:

$ docker ps -a

There's a complete command-line reference on the Docker website; take some time to learn your way around it.

That's it for this post. Next time, I'll write about how to use Nginx as a reverse proxy in front of your application, and why you might want to do that.