Building .NET Core Linux Docker Images with Visual Studio Team Services

If you've been wondering how to build a .NET Core app into a Docker image using VSTS, well here's the current steps as of June 2016.

Prerequisites

You'll need the following to complete this walk through

Provision the Linux Build Agent

To build a .NET Core Docker Image, you'll need a VSTS Build Agent. Since this post focuses on a Linux Docker Image, we'll use the new Linux VSTS Build Agent that Donovan explains how to provision here:

Installing the Docker Tools for Visual Studio Team System

If' you're unfamiliar with how to add VSTS extension, you can follow the steps here

Starting with a project to build

You'll need an ASP.NET Core project to deploy. Following the steps here, we'll walk you through the creation of an ASP.NET Core site, debug it, and set it up for deployment.

Configuring VSTS Docker Build Steps

Now that we have everything installed, we're ready to start configuring. I do assume you've already configured VSTS and are ready to configure the unique part of VSTS and Docker. See here more info on Visual Studio Team Services

Create a new build definition

  1. Choose the + sign in the top left corner to create a new build definition
  2. Choose the Empty build definition. We'll look to create one from this post
    create-empty-build-definition
  3. Configure your build definition
    1. 1-3 configure your repo, including the branch you wish to build from
    2. 4 - you'll likely want continuous integration
    3. 5 - choose the build agent you previously configured
    4. click Create build-definition-settings

Add the build steps

  1. Click the Add build step...
    empty-add-build-step
  2. Add 2 command line steps by hitting the [add] button twice. Note, although this step looks like a Windows/Doc command shell, the same step can be run on a Linux image
    add-commandline
  3. Add 4 docker steps
    add-docker-steps

We'll now walk through the various build steps, and the configuration for each one.

dotnet restore

  1. step name = dotnet restore
  2. tool = dotnet
  3. Arguments = restore -v minimal
  4. Advanced --> Working Folder = $(Build.StagingDirectory)
    This is the root of your source, where the project.json file lives as it will perform the restore based on the project.json

dotnet publish

  1. step name = dotnet publish
  2. tool = dotnet
  3. Arguments = publish -c $(Build.Configuration) -o $(Build.StagingDirectory)/app/
    the app/ directory is where we'll place the resulting files that will be placed in the runtime container. This must match the COPY command in your dockerfile as this is the source of that COPY command
  4. Advanced --> Working Folder - same as above as both commands must be run in the context of your project

docker build

  1. step name = docker build
  2. Skip the Docker Host and Registry Connection for a moment
  3. Action = Build an image
  4. Docker File = $(Build.StagingDirectory)/app/dockerfile
    This is the "release" mode dockerfile that has your optimized settings. It should not be the dockerfile.debug which is only used for the developers inner loop.
  5. Image Name = {your hub username}/{your image name}:$(Build.BuildNumber)
    for me, this was stevelasker/example-voting-app-worker:$(Build.BuildNumber)
  6. Context = $(Build.StagingDirectory)/app
    This one is really important, and the source of a lot of confusion. When you issue docker commands from the client to the host, this directory is first copied up to the docker host. When you see a COPY command, the left side is from the Context directory
  7. Working Directory = $(Build.StagingDirectory)/app

Docker Host Connection

  1. From within the docker build step, choose manage next to the Docker Host Connection
  2. Click New Service Endpoint
  3. Choose docker Host
  4. Enter the Name for your connection and your Docker Hub details to create the service endpoint
  5. Connection Name = LinuxBuildHost
    In this Linux VSTS build agent, the host is on the same machine
  6. Server URL = tcp://138.91.245.113:2376/
    Get this from the output of docker-machine ls, for the specific host you'll connect to. Notice the tcp protocol and port number
  7. CA Certificate, Certificate, Key - Use the handy dandy tooltip to figure out which files to use. Just simply open the file from your docker machine directory (eg: %userprofile%\.docker\machine\machines\vsts-linux-buildagent) into notepad. Be sure to copy the entire contents, not just what's between the --- BEGIN and ---END text.
  8. Press OK, then switch tabs to the previous screen
  9. Refresh the list and select the host you just configured

Docker Registry Connection

  1. From within the docker build step, choose manage next to the Docker Registry Connection
  2. Click New Service Endpoint
  3. Choose Docker Registry
  4. Connection Name: Docker Registry
  5. Docker ID = your username for docker hub
  6. Password = yup, your password
  7. Email = the email you associated with your docker hub account
  8. Press ok, then switch tabs to the previous build step configuration page
  9. Refresh the list, and select the registry you just configured

Docker Push Build Number - Public Registry

  1. Step Name = Docker Push :BuildNumber
  2. Docker Host Connection = LinuxBuildHost
  3. Docker Registry Connection = Docker Registry
  4. Action = Push an image
  5. Image Name = stevelasker/example-voting-app-worker:$(Build.BuildNumber)
  6. Advanced --> Working Directory = $(System.DefaultWorkingDirectory)

Validation Steps

This is where you'd most likely do your validation steps against the images. You now have a production ready image (binary). You haven't yet tested it, but you haven't set it to the :latest tag, which typically indicates the thing you want to deploy. For the purposes of this post, we'll skip this step. But, I wanted to cover why we first tagged our image with the build number, push, then re-tagged with latest, and re-pushed

Docker Tag Latest

  1. Step Name = Docker Tag Latest
  2. Docker Host Connection = LinuxBuildHost
  3. Docker Registry Connection = Docker Registry
  4. Action = Run a docker command
  5. Command = tag stevelasker/example-voting-app-worker:$(Build.BuildNumber) stevelasker/worksonmymachine:latest
    Notice I've used the original image name, and re-tagged it with latest. This doesn't create yet another copy. The Docker layering system knows this image is based on another image, that actually has nothing different, other than the tag. In the subsequent step, where we push, docker will know to just send the metadata up for latest.
  6. Advanced --> Working Directory = $(System.DefaultWorkingDirectory)

Docker Push Latest - Public Registry

  1. Step Name = Docker Push :Latest
  2. Docker Host Connection = LinuxBuildHost
  3. Docker Registry Connection = Docker Registry
  4. Action = Push an image
  5. Image Name = stevelasker/example-voting-app-worker:latest
  6. Advanced --> Working Directory = $(System.DefaultWorkingDirectory)

CI Complete - lets validate it

Our configuration should now be complete, and look something like this:
configured-build-steps

Lets save the definition, and give it a try

Queue a build

At this point, you should be able to queue a build, which should find our Linux VSTS Build agent, and execute the steps

If you have problems, just look close at the logs. You may have forgotten a cert, typo'd one of the parameters, or forgotten the advanced working folder for some of the steps.

If you've hooked up continuous integration, you can also kick things off by making a change

Instancing a built image

In this post, we didn't get into the specifics of deploying a built image. You may deploy to the Azure Container Service with Swarm or the Mesos template. You may have instanced Kubernetees. You may have some dedicated VM Hosts, or submit the image to Service Fabric.

To test the image, lets simply do a pull from the hub we pushed to.

From a powershell prompt, on a desktop. If possible, a machine that you didn't write the code that went into the image to have full isolation from other images. try the following

docker run -d -p 80:80 stevelasker/worksonmymachine:latest

That wraps up this post. Please keep the feedback coming

Steve