I was messing around with ASP.NET Core 1.1 MVC today and wanted to see how easy it would be to deploy an application into a Docker container. Taking the next, I wanted to build up an application stack with Docker Compose.

In the future, use of Docker Compose will enable a direct line to Docker Deploy and the ability to push to many hosts running Docker Engine in swarm mode.

Create an ASP.NET Core MVC App

The first step is creating the app. This example uses .NET Core 1.1 (and for what its worth, I'm on a Mac).

brian:~/Documents/code/$ dotnet --version
1.0.0-preview2-1-003177

The app is straightforward, create a new folder and then create a new web application.

mkdir app
cd app
dotnet new -t web

You can run your app and check it out by browsing to http://localhost:5000 after running restore and run.

dotnet restore
dotnet run

Now there is a functional app, but it only listens on localhost. This needs to change that so it binds to any address. This is important because the Docker container's localhost is differen than the host's localhost when the container is running on a bridged network (the default).

The easiest way to go about this is doing nothing! If you supply the environment variable ASPNETCORE_URLS it will override the default binding URL of http://localhost:5000.

A more explicit method is to change how Kestrel listens directly. To do this, open Project.cs and add a configuration by calling the UseUrls to enable any address on port 5000.

var host = new WebHostBuilder()
    .UseKestrel()
    .UseUrls("http://*:5000")
    .UseContentRoot(Directory.GetCurrentDirectory())
    .UseIISIntegration()
    .UseStartup<Startup>()
    .Build();

More information about configuring the host can be found in the Microsoft Docs for Hosting.

Now that the application is configured it is ready to be deployed. Next is creating the Dockerfile.

Dockerfile for ASP.NET Core MVC

The Dockerfile is based on the baseline Ubuntu 16.04. There are a variety of images that Microsoft provides, but for this exercise, I wanted to directly create one.

Microsoft provides the microsoft/aspnetcore that comes preconfigured with .NET CORE 1.1 and ASP.NET Core MVC. In my opinion, the extension of this Dockerfile is a bit hokey compared to the libary/node images. I'll stick with my own until there is an official aspnetcore image.

Create a Dockerfile in the root directory of the project:

FROM ubuntu:16.04

RUN apt-get update && apt-get install -y \
    apt-transport-https

RUN sh -c 'echo "deb [arch=amd64] https://apt-mo.trafficmanager.net/repos/dotnet-release/ xenial main" > /etc/apt/sources.list.d/dotnetdev.list'
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 417A0893

RUN apt-get update && apt-get install -y \
    dotnet-dev-1.0.0-preview2.1-003177

RUN mkdir -p ~/Documents/db:/usr/src/app/wwwroot/db
WORKDIR /usr/src/app
COPY . /usr/src/app

EXPOSE 5000

CMD ["dotnet", "app.dll"]

Breaking this down, the new image is based on the the ubuntu:16.04 image.

Next, APT needs to be configured to allow https traffic for the subsequent installation to proceed. The support for https traffic is performed by first updating APT and then installing apt-transport-https.

RUN apt-get update && apt-get install -y \  
    apt-transport-https

Next, the commands are converted from the .NET Core Ubuntu Installation Instructions. These commands configure APT with the .NET Core repositories and add the security keys needed to use this repository.

RUN sh -c 'echo "deb [arch=amd64] https://apt-mo.trafficmanager.net/repos/dotnet-release/ xenial main" > /etc/apt/sources.list.d/dotnetdev.list'
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 417A0893

After APT is configured, the final piece is installing the .NET Core SDK (note, you would want to use the latest version, the one below is current as of the time of this article).

RUN apt-get update && apt-get install -y \
    dotnet-dev-1.0.0-preview2.1-003177

At this point, the container will have .NET Core installed. The final bits of the Dockerfile are for configuring the application to function correctly.

RUN mkdir /usr/src/app
WORKDIR /usr/src/app
COPY . /usr/src/app

EXPOSE 5000

CMD ["dotnet", "app.dll"]

In this case, the folder /usr/src/app is created, the working directory is set as the current folder, and the files in the current "build" directory will be copied to /usr/src/app. This places the code into the working directory.

Additionally, port 5000 is exposed. This is important to allow communication outside of the container.

Lastly, is the execution command for the container. This command is calling dotnet with the output dll for the project. This container assumes that the build command will be run from the publish location.

To ensure the Dockerfile is included in the build output, you will need to modify the project.json file to include it in the build output. In this file, add the Dockerfile as follows:

  "publishOptions": {
    "include": [
      "wwwroot",
      "**/*.cshtml",
      "appsettings.json",
      "web.config",
      "Dockerfile"
  }

Now when the project is published, a Docker image can be created from the output. To accomplish this, navigate to the published directory and run:

docker build -t myapp .

Once the docker build completes, it can be run manually:

docker run --rm -p 5000:5000 myapp

The next step is using Docker Compose to spin up the container and Nginx.

Docker Compose with ASP.NET Core MVC and NGINX

Docker Compose creates a scriptable way to configure the interactions (networking and volumes) for a stack of containers. Previously, Docker Compose was for development and QA purposes, as it was focused on a single instance deployment.

Docker has implemented swarm mode in Docker Engine and is working towards a direct migration of Compose YAML files into Docker Stacks via Bundle. That's a lot of words that don't mean much right now. The point being, use of Docker Compose will offer a direct migration path to deployment on multi-host environments in future versions of Docker.

This is great for ASP.NET MVC apps hosted through Kestrel because we want to put NGINX in front our the app.

To get started, create the docker-compose.yml file in the root directory of the project.

version: '2'
services:
  web:
    image: myapp
  nginx:
    image: nginx:1.11
    links:
    - 'web'
    ports:
    - '80:80'
    command: |
      /bin/bash -c "echo '
      server {
        listen 80;
        location / {
            proxy_pass http://web:5000;
            proxy_http_version 1.1;
            proxy_set_header Connection keep-alive;
            proxy_set_header Upgrade $$http_upgrade;
            proxy_set_header Host $$host;
            proxy_set_header X-Real-IP $$remote_addr;
            proxy_cache_bypass $$http_upgrade;
        }
      }' | tee /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"

Breaking this down, this Compose file contains two services: web and nginx.

The web service is straightforward in that it only contains the image definition. In this example, the image was built under the name myapp.

The nginx service is a bit more complicated.

First, it contains a link to the web service. This link enables the nginx container to talk to the exposed ports on the web image through the hostname web. This is important because it enables configuration of nginx to properly pass requests.

Next, the nginx service exposes port 80 on the host and links it to 80 inside the container.

Lastly, the nginx service uses a custom command to first override the default.conf for nginx with a configuration that points all requests to http://web:5000 which is the hostname and port that for the app that was created earlier.

The last thing that needs to be done is adding the docker-compose.yml file to the build output as was done with the the Dockerfile.

Once the project.json is modified to include this build, the project can be republished and the following command run from the publish output directory:

docker-compose up

With this command, it's possible to load localhost in the browser and hit the ASP.NET MVC application. The request is made on port 80 through NGINX. NGINX forwards the request to the container with the app on port 5000.

There is now a fully "dockerized" ASP.NET Core MVC App!