Self-hosted Azure DevOps agents in docker


With more of our codebase moving to dotnet core, we can begin to see real cost savings from its multi-platform nature. We no longer required Windows only build agents, which means less rental costs and actual scalable infrastructure. Here’s how I recently moved a client’s self-hosted Azure DevOps agents to linux docker containers.

Create the dockerfile

Depending on the host platform for your containers, you can base it on ubuntu or Amazon linux. I will include both versions in gist for this post.

FROM ubuntu:21.10

# 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 \

Or Amazon Linux:

FROM amazonlinux:latest

# 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 yum update -y && yum install -y git cmake gcc make \

Modify the start.sh

The documentation gets you started with a sample start.sh that mostly works. In some scenarios, ie hosted in ecs or eks, the agents were not removed even after the containers were deleted. After investigation, I discovered that the .agent and .credential files for on-prem installations only persisted the base url of the server. So for an installation to https://corporate.devops_server.com/collection_name, only https://corporate.devops_server.com gets persisted. This isn’t a problem for usage, but in an auto-scale deployment when agent hosts are scaled down, they aren’t removed from agents list.

To address this, I simply reassert the value in both files during the removal phase using the following:

print_header "Cleanup. updating $AZP_URL..."
    # the 'dirname' command strips the collection name from the url 
    base_url=$(dirname "$AZP_URL")
    # then I replace the baseUrl with the full url
    if [ "$base_url" != "$AZP_URL" ]; then
      sed -i "s,$base_url,$AZP_URL,g" .agent
      sed -i "s,$base_url,$AZP_URL,g" .credentials
    fi

In this file, I also chose to handle the persistence of environment and path variables required for our common build jobs. Microsoft agents do not use the traditional linux files for these variables, but use .env and .path files. Here’s how I persist environment variables:

# persist variables
touch .env
echo "CREATED_ON=$CREATED_ON" >> .env
export JAVA_HOME=$(readlink -f /usr/bin/java | sed "s:/bin/java::")
echo "JAVA_HOME=$JAVA_HOME" >> .env
export PATH="$PATH:$JAVA_HOME/bin"

Since I want each agent to be unique, I also modify the naming of the agent in this file. This may not be necessary as the name can be passed in the docker run command:

PREFIX="docker-agent"
SUFFIX=$(openssl rand -hex 2);
if [[ -z "${ECS_CONTAINER_METADATA_URI}" ]]; then
  echo "Not running in ECS"
else
  echo "HOST_TYPE=ECS" >> .env
  SUFFIX=$(echo "$ECS_CONTAINER_METADATA_URI" | awk -F/ '{print $NF}' | cut -d "-" -f 1)
  PREFIX="ecs-task"
fi
if [[ -z "${KUBERNETES_SERVICE_HOST}" ]]; then
  echo "Not running in EKS"
else
  echo "HOST_TYPE=EKS" >> .env
  SUFFIX=$(echo "$HOSTNAME" | awk -F- '{print $NF}')
  PREFIX="eks-pod"
fi

Then later we can name the agent:

print_header "3. Configuring Azure Pipelines agent..."
./config.sh --unattended \
  --agent "${AZP_AGENT_NAME:-$PREFIX-$SUFFIX}" \

Wrapping up

From here it’s just a case of building the docker image, uploading to a repository of your choice and hosting the containers.

# build
docker build -t azure_devops_linux_agent:latest . ;

#run
docker run -e AZP_URL=https://corporate.devops_server.com/collection_name \
            -e AZP_TOKEN=ef11fac1e8bf434e8d201cfddcb9acf4 \
            -e AZP_POOL=core_docker \
            -e DOTNET_CLI_HOME=/tmp \
            -v /var/run/docker.sock:/var/run/docker.sock \
            -v /var/vsts:/var/vsts \
            -d azure_devops_linux_agent:latest

The results can be seen here:

agent logs in Docker

And in Azure DevOps:

registered agent in Azure DevOps