Self-hosted Azure Pipelines / Azure DevOps agent, Linux & Windows, optional Java/Android/.NET/Node
1.2K
Containerized self-hosted agent for Azure Pipelines / Azure DevOps, with optional build essentials and Python dev headers (Linux), Java, Android SDK (with emulator on Linux), PowerShell Core, .NET SDK, and Node.js toolchains.
| Variant | Image tag |
|---|---|
| Linux | docker.io/jnitecki/azp-agent:latest |
| Windows | docker.io/jnitecki/azp-agent:windows-latest |
Both variants expect the standard Azure Pipelines agent environment variables:
| Variable | Meaning |
|---|---|
AZP_URL | Azure DevOps organization URL, e.g. https://dev.azure.com/<org>. |
AZP_TOKEN | Personal access token with Agent Pools (read, manage) scope. |
AZP_POOL | Agent pool name to register into (defaults to Default). |
AZP_AGENT_NAME | Optional; defaults to the container's own hostname if unset. |
AZP_WORK | Optional working directory (defaults to _work). |
AZP_RUN_ONCE | Optional; true (default) runs a single job, then the agent deregisters and the container exits. false keeps the agent registered and taking jobs until the container is stopped. Also accepts 1/0 and yes/no. |
With the default AZP_RUN_ONCE=true, run the container with a restart policy (e.g. --restart unless-stopped, as run.ps1 does) so a fresh agent registers after every job. Note that a restarted container keeps its filesystem, so to start each job from a clean environment, run a new container (e.g. --rm plus an external supervisor) instead of restarting the old one.
docker run -e AZP_URL=https://dev.azure.com/<org> \
-e AZP_TOKEN=<pat> \
-e AZP_POOL=<pool> \
docker.io/jnitecki/azp-agent:latest
docker run -e AZP_URL=https://dev.azure.com/<org> `
-e AZP_TOKEN=<pat> `
-e AZP_POOL=<pool> `
docker.io/jnitecki/azp-agent:windows-latest
The Linux agent reports these variables as capabilities, so pipelines can demand them:
| Variable | Set when | Value |
|---|---|---|
GCC | gcc (build-essential) is installed | Full gcc version, e.g. 13.3.0 |
GCC_<major> | gcc (build-essential) is installed | Path to gcc, e.g. GCC_13=/usr/bin/gcc |
PYTHON_DEV | Python development headers (python3-dev) are installed | Python version the headers are for, e.g. 3.12 |
ANDROID_EMULATOR | The Android emulator is installed and emulator -accel-check succeeds at container start | Path to the emulator, e.g. /opt/android-sdk/emulator/emulator |
ANDROID_EMULATOR_<api> | As ANDROID_EMULATOR, one per installed emulator system image | Path to the emulator, e.g. ANDROID_EMULATOR_36=/opt/android-sdk/emulator/emulator |
PODMAN | Podman is installed and a test container started at container start runs successfully | Path to podman, e.g. /usr/bin/podman |
pool:
name: <pool>
demands:
- GCC_13
- PYTHON_DEV -equals 3.12
The emulator is only included in the amd64 image (Google publishes no Linux arm64 emulator). It needs hardware acceleration, so the container must be given access to KVM (e.g. --device /dev/kvm, or --privileged as run.ps1 does) on a host that supports it. Without it, the emulator stays installed but ANDROID_EMULATOR* are not reported, so jobs that demand them won't be routed to this agent.
Nested Podman needs the container to run --privileged (as run.ps1 does). When running the container with Podman, also give nested Podman's storage its own volume, e.g. -v /var/lib/containers (as run.ps1 does when it runs the container with Podman and the image includes Podman). The volume then sits on the host's normal filesystem (e.g. WSL2's ext4 disk) instead of on the container's own overlay, so the nested fuse-overlayfs works on top of a regular filesystem. The official quay.io/podman/stable image is set up the same way.
At start, before registering, the agent checks that nested Podman actually works by running a small test container. The test needs no registry or network access: it builds a throwaway local image from the agent's own shell binaries, runs it with --cgroups=disabled --network=none and a read-only bind mount, creates a directory inside it, and removes the image again. If the test fails, the agent logs a warning with the error and removes PODMAN, so jobs that demand it are not routed to this agent. If Podman's image storage is on an overlay or FUSE filesystem, the warning also suggests mounting a volume at /var/lib/containers. The test adds some time to each container start (each job with the default AZP_RUN_ONCE=true) and gives up after 2 minutes per step.
If you have this repository cloned, run.ps1 scripts the above: it pulls the current image, stops/removes any previous azp-agent/azp-agent-NN container(s) together with their anonymous volumes, and starts fresh one(s). -AzpUrl/-AzpToken/-AzpPool are optional — if omitted, each falls back to the matching AZP_URL/AZP_TOKEN/AZP_POOL environment variable, then to a hardcoded default at the top of the script (empty by default; fill in locally, never commit real values), and the script fails fast if a value is still missing. -RunOnce true|false sets AZP_RUN_ONCE the same way (parameter, then environment variable), but if neither is given the image default applies.
# Single Linux agent named after this host, using the latest image
./run.ps1 -AzpUrl https://dev.azure.com/<org> -AzpToken <pat> -AzpPool <pool>
# Three Linux agents: azp-agent-01..03, named <hostname>-01..03
./run.ps1 -AzpUrl https://dev.azure.com/<org> -AzpToken <pat> -AzpPool <pool> -InstanceCount 3
# Persistent agent that keeps taking jobs instead of exiting after one
./run.ps1 -AzpUrl https://dev.azure.com/<org> -AzpToken <pat> -AzpPool <pool> -RunOnce false
# Windows variant, explicit version
./run.ps1 -AzpUrl https://dev.azure.com/<org> -AzpToken <pat> -AzpPool <pool> -Os Windows -Version 4.248.0
Content type
Image
Digest
sha256:1a1d1dd3d…
Size
1.3 GB
Last updated
4 days ago
docker pull jnitecki/azp-agent