🚀 Run Azure DevOps Self-Hosted Agents in Windows Server 2025 Containers

If you’ve ever used Azure DevOps pipelines, you probably know the convenience of Microsoft-hosted agents. But sometimes… they’re just not enough. Maybe you need custom tools, maybe you want builds inside your own network, or maybe you just want to speed things up on a beefy server you already own.

That’s where self-hosted agents come in. And here’s the fun part: you can run them neatly packaged as Windows containers on Windows Server 2025 🖥️🐳.

This guide walks you through the whole process: building an image, running containers, and hooking them into your Azure DevOps pool.

🧰 What you’ll need

  • A Windows Server 2025 machine (physical or VM)
  • Docker installed (make sure you enabled Windows containers during setup!)
  • An Azure DevOps Personal Access Token (PAT) with Agent Pools (read & manage)
  • (Optional) Azure CLI

Step 1 — Create the Docker files

Make yourself a folder to work in:

mkdir C:\DockerFiles\AzDOAgent
cd C:\DockerFiles\AzDOAgent

Inside, you’ll need two files:

📄 Dockerfile

# escape=`
FROM mcr.microsoft.com/windows/servercore:ltsc2025
SHELL ["powershell","-NoLogo","-NoProfile","-Command"]

# Install PowerShell 7
RUN Invoke-WebRequest https://github.com/PowerShell/PowerShell/releases/download/v7.4.3/PowerShell-7.4.3-win-x64.msi -OutFile pwsh.msi ; `
    Start-Process msiexec.exe -ArgumentList '/i','pwsh.msi','/qn','/norestart' -Wait ; `
    Remove-Item pwsh.msi -Force

# Install Git
RUN Invoke-WebRequest https://github.com/git-for-windows/git/releases/download/v2.47.1.windows.1/Git-2.47.1-64-bit.exe -OutFile git.exe ; `
    Start-Process .\git.exe -ArgumentList '/VERYSILENT','/NORESTART','/SUPPRESSMSGBOXES' -Wait ; `
    Remove-Item .\git.exe -Force

# Agent home
RUN New-Item -Type Directory -Path C:\agent -Force | Out-Null
WORKDIR C:\agent

# Defaults consumed by start script
ENV AGENT_VERSION="4.260.0" `
    AZP_URL="" `
    AZP_POOL="Default" `
    AZP_AGENT_NAME="" `
    AZP_TOKEN="" `
    AZP_WORK="C:\\agent\\_work"

# Copy start script
COPY start-agent.ps1 C:\agent\start-agent.ps1

CMD ["cmd.exe","/C","pwsh -File C:\\agent\\start-agent.ps1"]

📄 start-agent.ps1

$ErrorActionPreference = 'Stop'
Set-Location C:\agent
function Log($m){ Write-Host "[start-agent] $m" }

# Validate required env vars
$need = @('AZP_URL','AZP_TOKEN')
foreach ($v in $need){
  $val = [Environment]::GetEnvironmentVariable($v)
  if ([string]::IsNullOrWhiteSpace($val)){ throw "$v is required" }
}
if ([string]::IsNullOrWhiteSpace($env:AZP_POOL))       { $env:AZP_POOL = 'Default' }
if ([string]::IsNullOrWhiteSpace($env:AZP_AGENT_NAME)) { $env:AZP_AGENT_NAME = $env:COMPUTERNAME }
if ([string]::IsNullOrWhiteSpace($env:AZP_WORK))       { $env:AZP_WORK = 'C:\agent\_work' }
if ([string]::IsNullOrWhiteSpace($env:AGENT_VERSION))  { $env:AGENT_VERSION = '4.260.0' }

# Download agent (pinned to 4.260.0)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$ver = $env:AGENT_VERSION
$zip = "C:\agent\agent.zip"
$url = "https://download.agent.dev.azure.com/agent/$ver/vsts-agent-win-x64-$ver.zip"

$max = 5
for ($i=1; $i -le $max; $i++) {
  try {
    Log "Downloading agent $ver from $url (attempt $i/$max)..."
    Invoke-WebRequest -Uri $url -OutFile $zip
    if (Test-Path $zip) { break }
  } catch {
    if ($i -eq $max) { throw }
    Start-Sleep -Seconds (5 * $i)
  }
}

Expand-Archive -Path $zip -DestinationPath . -Force
Remove-Item $zip -Force
New-Item -ItemType Directory -Path $env:AZP_WORK -Force | Out-Null

# Configure the agent
for ($i=1; $i -le 5; $i++) {
  try {
    Log "Configuring agent (attempt $i/5)..."
    .\config.cmd --unattended `
      --url $env:AZP_URL --auth pat --token $env:AZP_TOKEN `
      --pool $env:AZP_POOL --agent $env:AZP_AGENT_NAME --work $env:AZP_WORK --replace
    break
  } catch {
    if ($i -eq 5) { throw }
    Start-Sleep -Seconds (5 * $i)
  }
}

Log "Starting agent..."
.\run.cmd

🔨 Step 2 — Build your image

Now run:

docker build --isolation=hyperv -t ado-agent:win2025 .

This builds a shiny new image called ado-agent:win2025.

▶️ Step 3 — Spin up your agents

Here’s a simple PowerShell snippet to launch two agents:

$AZDO_ORG_URL="https://dev.azure.com/<your-org>"
$AZDO_POOL="Personal"
$AZDO_PAT="<YOUR_PAT>"   # PAT with Agent Pools (read & manage)
$AGENT_VER="4.260.0"
$COUNT=2

1..$COUNT | ForEach-Object {
  $name = "ado-win-$_"
  docker run -d --restart=always --name $name `
    -e AZP_URL=$AZDO_ORG_URL `
    -e AZP_POOL=$AZDO_POOL `
    -e AZP_TOKEN=$AZDO_PAT `
    -e AZP_AGENT_NAME=$name `
    -e AGENT_VERSION=$AGENT_VER `
    ado-agent:win2025
}

💡 The --restart=always flag means your agents will automatically come back online if the server or Docker service restarts.

👀 Step 4 — Check in DevOps

Head over to Organization Settings → Agent Pools in Azure DevOps.
Open your pool (e.g., Personal) and… voilà! ✨ You should see your new agents appear online.

✅ Step 5 — Test it with a pipeline

Create a simple pipeline in your project:

pool: Personal
steps:
- powershell: |
    echo "Hello from $(Agent.Name) 🎉"
    git --version
    dotnet --version
  displayName: "Sanity check"

Run it — and watch your brand-new containerized Windows 2025 agent do its job.


🎯 Wrap-up

And that’s it! You now have Azure DevOps self-hosted agents running as Windows containers on Windows Server 2025.

Why this rocks:

  • 🔄 Easily scale — just spin up more containers
  • 🧩 Stay in control — install whatever tools you need inside your image
  • 💪 Stable — agents restart automatically on reboot

👉 Next step: push your image to a private registry (like ACR or Docker Hub) and you’ll be able to roll it out across multiple servers in no time.

Leave a Reply

Your email address will not be published. Required fields are marked *