Quick Guide to Deploy Azure DevOps Agents with Docker Desktop

If you need more information about deploying Azure DevOps agents, Microsoft offers detailed documentation.
Run a self-hosted agent in Docker
This article is a quick summary for those who need it right away!
Prerequisites
Docker Desktop installed.
A text editor.
You only need three files and some information from your Azure DevOps organization. If you want, you can create a Git repository to store them.
The information you will need:
The URL of your Azure DevOps organization: https://dev.azure.com/{your-organization-name}
A PAT token with full access. Use personal access tokens - Azure DevOps | Microsoft Learn
The name of the pool you want to use.
Dockerfile : To build your image and run the container on your computer.
FROM ubuntu:22.04
ENV TARGETARCH="linux-x64"
# Also can be "linux-arm", "linux-arm64".
RUN apt update && \
apt upgrade -y && \
apt install -y curl git jq libicu70
# To install sshpass directly on the agent and save
RUN apt-get install sshpass
# Install Azure CLI
RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash
WORKDIR /azp/
COPY ./start.sh ./
RUN chmod +x ./start.sh
# Create agent user and set up home directory
RUN useradd -m -d /home/agent agent
RUN chown -R agent:agent /azp /home/agent
USER agent
# Another option is to run the agent as root.
ENV AGENT_ALLOW_RUNASROOT="true"
ENTRYPOINT [ "./start.sh" ]
Start.sh - Start the agent on the docker container.
#!/bin/bash
set -e
if [ -z "${AZP_URL}" ]; then
echo 1>&2 "error: missing AZP_URL environment variable"
exit 1
fi
if [ -n "$AZP_CLIENTID" ]; then
echo "Using service principal credentials to get token"
az login --allow-no-subscriptions --service-principal --username "$AZP_CLIENTID" --password "$AZP_CLIENTSECRET" --tenant "$AZP_TENANTID"
# adapted from https://learn.microsoft.com/en-us/azure/databricks/dev-tools/user-aad-token
AZP_TOKEN=$(az account get-access-token --query accessToken --output tsv)
echo "Token retrieved"
fi
if [ -z "${AZP_TOKEN_FILE}" ]; then
if [ -z "${AZP_TOKEN}" ]; then
echo 1>&2 "error: missing AZP_TOKEN environment variable"
exit 1
fi
AZP_TOKEN_FILE="/azp/.token"
echo -n "${AZP_TOKEN}" > "${AZP_TOKEN_FILE}"
fi
unset AZP_CLIENTSECRET
unset AZP_TOKEN
if [ -n "${AZP_WORK}" ]; then
mkdir -p "${AZP_WORK}"
fi
cleanup() {
trap "" EXIT
if [ -e ./config.sh ]; then
print_header "Cleanup. Removing Azure Pipelines agent..."
# If the agent has some running jobs, the configuration removal process will fail.
# So, give it some time to finish the job.
while true; do
./config.sh remove --unattended --auth "PAT" --token $(cat "${AZP_TOKEN_FILE}") && break
echo "Retrying in 30 seconds..."
sleep 30
done
fi
}
print_header() {
lightcyan="\033[1;36m"
nocolor="\033[0m"
echo -e "\n${lightcyan}$1${nocolor}\n"
}
# Let the agent ignore the token env variables
export VSO_AGENT_IGNORE="AZP_TOKEN,AZP_TOKEN_FILE"
print_header "1. Determining matching Azure Pipelines agent..."
AZP_AGENT_PACKAGES=$(curl -LsS \
-u user:$(cat "${AZP_TOKEN_FILE}") \
-H "Accept:application/json" \
"${AZP_URL}/_apis/distributedtask/packages/agent?platform=${TARGETARCH}&top=1")
AZP_AGENT_PACKAGE_LATEST_URL=$(echo "${AZP_AGENT_PACKAGES}" | jq -r ".value[0].downloadUrl")
if [ -z "${AZP_AGENT_PACKAGE_LATEST_URL}" -o "${AZP_AGENT_PACKAGE_LATEST_URL}" == "null" ]; then
echo 1>&2 "error: could not determine a matching Azure Pipelines agent"
echo 1>&2 "check that account "${AZP_URL}" is correct and the token is valid for that account"
exit 1
fi
print_header "2. Downloading and extracting Azure Pipelines agent..."
curl -LsS "${AZP_AGENT_PACKAGE_LATEST_URL}" | tar -xz & wait $!
source ./env.sh
trap "cleanup; exit 0" EXIT
trap "cleanup; exit 130" INT
trap "cleanup; exit 143" TERM
print_header "3. Configuring Azure Pipelines agent..."
# Despite it saying "PAT", it can be the token through the service principal
./config.sh --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..."
chmod +x ./run.sh
# To be aware of TERM and INT signals call ./run.sh
# Running it with the --once flag at the end will shut down the agent after the build is executed
./run.sh "$@" & wait $!
docker-compose.yml - Easily start and stop your container with some environment variables.
version: '3.8'
services:
agent:
build:
context: .
dockerfile: Dockerfile
environment:
- AZP_URL=your_organization_url
- AZP_TOKEN=your_token
- AZP_AGENT_NAME=give_it_a_name
- AZP_POOL=in_which_pool_you_want_it
Let’s test it
Open a terminal in the directory where the three files are located and type: docker-compose up
Your agent should be ready! You can go to your Azure DevOps project settings, and you should see your new agent in the agent pools you selected.
In conslusion
When you have all the information, you can see that it is easy to set up custom agents tailored to your needs.
Subscribe to my newsletter
Read articles from Marc-André Simoneau directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
