Azure DevOps and Visual Studio Awesomeness

Containerized Azure Pipelines Agent using Docker with WSL2

I recently installed Windows Services for Linux V2 (WSL2) and saw a Tech Preview of Docker Desktop WSL 2 posted and was extremely excited to give it a try. If you are interested, read on.

Updated 08/05/2019 – update date/time in WSL2 due to Windows 10 sleep/hibernate

Windows 10 Pro Insider Preview

Get the latest Windows Insider Build, you find out more here on the Quick Start page

Enable WSL (WSL 1.0)

If you do *not* have WSL already configured in Windows 10, you can execute the following command in PowerShell (as Administrator)

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

Open the Microsoft Store and select Ubuntu 18.04 LTS

Enable WSL 2

Execute the following command in PowerShell (as Administrator)

Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform

You will have to restart your PC to complete the previous step.

Open a PowerShell command prompt (as Administrator). This first command will list the distributions you have installed with WSL

wsl -- list -v

You should see Ubuntu-18.04 listed and with a Version 1. You can now switch Ubuntu-18.04 to Version 2 by executing the following command:

wsl --set-version "Ubuntu-18.04" 2

You can see from the image below it took me two (2) tries to get that to happen successfully.

Set a distro to be backed by WSL 2

You should now be able to install the Docker Desktop WSL 2 Tech Preview, if you already have a version of Docker installed it will be upgraded to this version.

Once installed you should see the familiar icon in your task bar, Click on the Docker Desktop Icon to display the menu. Select the WSL 2 Tech Preview menu item

Docker Desktop | WSL 2 Tech Preview

Once you click that menu item you will be presented with a dialog to start the daemon running in WSL 2.

Docker Desktop | WSL 2 Technical Preview

Create Docker file

I wanted an excuse to use Visual Studio Code with the new Remote WSL extension and open a folder in WSL. I launched VS Code with the extension installed and chose to open a folder in Ubuntu 18.04

Visual Studio Code – Command Palette | New Window Using Distro…
Select Distro Ubuntu 18.04

Once you do that you can use VS Code to open your home folder to create a directory and the necessary docker files we need to configure the agent.

Visual Studio Code | Remote WSL

Here is the Dockerfile (below) I created, portion of it was obtained from here. The issues I had with that particular configuration is I wanted to use Ubuntu 18.04, compile a .NET core application (which required the sdk) and execute a PowerShell script (which required PowerShell core) as part of the build definition of my .NET Core Web Application.

The Dockerfile was modified to use Ubuntu 18.04, install the dot net core sdk 2.2 and install PowerShell core.

FROM ubuntu:18.04

# To make it easier for build and release pipelines to run apt-get,
# configure apt to not require confirmation (assume the -y argument by default)
ENV DEBIAN_FRONTEND=noninteractive
RUN echo "APT::Get::Assume-Yes \"true\";" > /etc/apt/apt.conf.d/90assumeyes

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
        ca-certificates \
        curl \
        jq \
        git \
        iputils-ping \
        libcurl4 \
        libicu60 \
        libunwind8 \
        libssl1.0.0 \
        libssl1.1 \
        wget \
        software-properties-common \

RUN wget -q -O packages-microsoft-prod.deb
RUN dpkg -i packages-microsoft-prod.deb

RUN add-apt-repository universe
RUN apt-get install apt-transport-https
RUN apt-get update
RUN apt-get install dotnet-sdk-2.2
RUN apt-get install powershell


COPY ./ .
RUN chmod +x

CMD ["./"]

Here is the file which is exactly what was in the article. I did not have to change this script whatsoever.

set -e

if [ -z "$AZP_URL" ]; then
  echo 1>&2 "error: missing AZP_URL environment variable"
  exit 1

if [ -z "$AZP_TOKEN_FILE" ]; then
  if [ -z "$AZP_TOKEN" ]; then
    echo 1>&2 "error: missing AZP_TOKEN environment variable"
    exit 1

  echo -n $AZP_TOKEN > "$AZP_TOKEN_FILE"


if [ -n "$AZP_WORK" ]; then
  mkdir -p "$AZP_WORK"

rm -rf /azp/agent
mkdir /azp/agent
cd /azp/agent


cleanup() {
  if [ -e ]; then
    print_header "Cleanup. Removing Azure Pipelines agent..."

    ./ remove --unattended \
      --auth PAT \
      --token $(cat "$AZP_TOKEN_FILE")

print_header() {
  echo -e "${lightcyan}$1${nocolor}"

# Let the agent ignore the token env variables

print_header "1. Determining matching Azure Pipelines agent..."

  -u user:$(cat "$AZP_TOKEN_FILE") \
  -H 'Accept:application/json;api-version=3.0-preview' \

if echo "$AZP_AGENT_RESPONSE" | jq . >/dev/null 2>&1; then
    | jq -r '.value | map([.version.major,.version.minor,.version.patch,.downloadUrl]) | sort | .[length-1] | .[3]')

if [ -z "$AZP_AGENTPACKAGE_URL" -o "$AZP_AGENTPACKAGE_URL" == "null" ]; then
  echo 1>&2 "error: could not determine a matching Azure Pipelines agent - check that account '$AZP_URL' is correct and the token is valid for that account"
  exit 1

print_header "2. Downloading and installing Azure Pipelines agent..."

curl -LsS $AZP_AGENTPACKAGE_URL | tar -xz & wait $!

source ./

trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM

print_header "3. Configuring Azure Pipelines agent..."

./ --unattended \
  --agent "${AZP_AGENT_NAME:-$(hostname)}" \
  --url "$AZP_URL" \
  --auth PAT \
  --token $(cat "$AZP_TOKEN_FILE") \
  --pool "${AZP_POOL:-Default}" \
  --work "${AZP_WORK:-_work}" \
  --replace \
  --acceptTeeEula & wait $!

print_header "4. Running Azure Pipelines agent..."

# `exec` the node runtime so it's aware of TERM and INT signals
# AgentService.js understands how to handle agent self-update and restart
exec ./externals/node/bin/node ./bin/AgentService.js interactive

Build the container

Once these file are saved in your WSL 2 environment, open a wsl prompt and change directories into ~/dockeragent and execute the command below to build your Dockerfile

docker build -t dockeragent:latest .
docker build

Start the container

Once that command completes we can run our docker container. This will install the latest version of the agent, configure it, and run the agent targeting the default pool of the Azure DevOps service specified on the command line.

docker run -e AZP_URL=<Azure DevOps instance> -e AZP_TOKEN=<PAT token> -e AZP_AGENT_NAME=mydockeragent dockeragent:latest
docker run

Let’s take the containerized agent for a spin from Azure DevOps, to do this I will trigger a build of my demo ASP.NET Core Web App.

Trigger a new build | Select Default for the Agent pool where our agent is registered

Here is the log of the agent running our build

Agent log in the background, the WSL prompt window inset showing our job being processed

You can also use this agent in your YAML build definition by specifying Default as the pool value


Once you have saved your edits you can trigger another build to verify it is okay. Note you will have to authorize the pool the first time.

This pool does not exist or has not been authorized for use

I clicked the Authorize resources button and then triggered another build

Successful Build using the docker agent

Hope this is helpful if you are interested in trying out Docker with WSL 2.


If you sleep or hibernate Windows 10 while WSL2 is running your clock will completely be out of sync and the Azure DevOps Agent will fail to start due to the date/time issue.

You can easily update the date/time by issuing the following command

sudo hwclock -s
Update date/time in WSL2

I discovered this after waking my laptop up to continue working on my container and this blog post.