Self-hosted Azure DevOps agents in docker

Self-hosted Azure DevOps agents in docker

Self-hosted Azure DevOps agents in docker

With more of our codebase moving to .NET Core, we can begin to see real cost savings from its multi-platform nature. We no longer require Windows-only build agents, which means lower rental costs and properly 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 have included both versions in the 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, such as hosting in ECS or EKS, agents were not removed even after their containers were deleted. After some investigation, I discovered that the .agent and .credential files for on-premises installations only persisted the server’s base URL. For an installation at https://corporate.devops_server.com/collection_name, only https://corporate.devops_server.com gets persisted. This isn’t a problem during normal use, but in an autoscaling deployment the agents remain in the agent list after their hosts scale down.

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 persist the environment and path variables required by our common build jobs. Microsoft agents do not use the traditional Linux files for these variables; they 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 set its name 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

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 it 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