Self-Hosted Build Agent (DigitalOcean)¶
Version: 1.0 Last Updated: February 2026 Setup Script:
deployment/build-agent/setup-build-agent.sh
This document describes the self-hosted Azure DevOps build agent running on a DigitalOcean droplet, used to accelerate Docker image builds through persistent local caching.
Table of Contents¶
- Overview
- Architecture
- Why Self-Hosted
- Droplet Specification
- Agent Pool Configuration
- Pipeline Integration
- Job Distribution Strategy
- Setup Instructions
- Maintenance
- Monitoring & Troubleshooting
- Security Considerations
- Cost Analysis
Overview¶
The CI/CD pipeline uses a hybrid agent strategy: a Microsoft-hosted agent handles lightweight jobs (linting, testing, Nx builds) while a self-hosted DigitalOcean droplet handles all Docker image packaging. This leverages persistent Docker layer caching on the self-hosted agent to dramatically reduce build times.
Key Benefits¶
- Docker layer caching: Persistent cache between builds (vs. cold cache on every MS-hosted run)
- Pre-installed tools: Cosign and Syft are pre-installed instead of downloaded each run
- Parallel execution: 2 agent instances enable true parallel job execution
- Cost-effective: ~$48/month for performance equivalent to several MS-hosted parallel jobs
Architecture¶
Why Self-Hosted¶
The Problem¶
Microsoft-hosted agents are ephemeral — each job gets a fresh VM. This means:
- Every Docker build starts with cold cache (pulls all base images, rebuilds all layers)
- Cosign and Syft are downloaded and installed on every single job (~30s each)
- With 1 free parallel job, 8+ Docker builds queue sequentially
The Solution¶
A self-hosted agent on a persistent droplet provides:
| Aspect | MS-Hosted (ephemeral) | Self-Hosted (persistent) |
|---|---|---|
| Docker cache | Cold every run | Warm between runs |
| Base image pull | Every run (~30s) | Only on first run |
| Layer rebuild | All layers (~3-5 min) | Only changed layers (~30s-1 min) |
| Tool installation | Cosign + Syft every run (~1 min) | Pre-installed (0s) |
| Parallel jobs | 1 free (extra: $40/mo each) | 2 instances included |
Expected Build Times (per service)¶
| Scenario | MS-Hosted | Self-Hosted (cold) | Self-Hosted (warm) |
|---|---|---|---|
| Docker build + push | 5-10 min | 5-10 min | 1-2 min |
| Cosign signing | 1-2 min | 1-2 min | 30s |
| SBOM generation | 1-2 min | 1-2 min | 30s |
| Total per service | 7-14 min | 7-14 min | 2-3 min |
Full Pipeline Impact¶
Droplet Specification¶
| Property | Value |
|---|---|
| Provider | DigitalOcean |
| Plan | s-4vcpu-8gb ($48/month) |
| OS | Ubuntu 22.04 LTS |
| vCPU | 4 |
| RAM | 8 GB |
| Disk | 160 GB SSD |
| Region | AMS3 (Amsterdam) — close to DO Container Registry |
| Purpose | Azure DevOps self-hosted build agent |
Installed Software¶
| Software | Version | Purpose |
|---|---|---|
| Docker CE | Latest stable | Container builds |
| Docker BuildKit | Included with Docker | Multi-stage build caching |
| Node.js | 20 LTS | Nx builds, pnpm |
| pnpm | 9 | Package management |
| .NET SDK | 8.0 | Required by PublishCodeCoverageResults@2 |
| reportgenerator | Latest | HTML coverage report generation (installed as dotnet global tool) |
| Cosign | 2.2.4 | Container image signing |
| Syft | Latest | SBOM generation |
| Azure DevOps Agent | 4.269.0 | Pipeline job execution |
Agent Pool Configuration¶
Azure DevOps Setup¶
Before running the setup script, create the agent pool in Azure DevOps:
- Go to Organization Settings → Agent pools
- Click Add pool
- Select Self-hosted
- Name:
DO-Build-Agents - Grant access permission to all pipelines (or select specific pipelines)
- Check Auto-provision this agent pool in all projects
Agent Instances¶
The droplet runs 2 agent instances, allowing 2 pipeline jobs to execute simultaneously:
| Instance | Name | Service | Port |
|---|---|---|---|
| 1 | do-build-agent-1 |
vsts.agent.*.DO-Build-Agents.do-build-agent-1 |
— |
| 2 | do-build-agent-2 |
vsts.agent.*.DO-Build-Agents.do-build-agent-2 |
— |
Both instances share the same Docker daemon and layer cache, maximizing cache hit rates.
Pipeline Integration¶
Pool Configuration¶
The pipeline uses a default pool at the top level and per-job overrides for Docker-heavy jobs:
# Default: MS-hosted agent (lightweight jobs)
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: Build
jobs:
# Runs on MS-hosted (inherits default pool)
- job: BuildAll
steps: ...
# Runs on self-hosted (pool override)
- job: PackageGateway
pool:
name: 'DO-Build-Agents'
steps: ...
Jobs Using Self-Hosted Pool¶
| Stage | Job | Pool | Why |
|---|---|---|---|
| Validate & Test | TypeCheck | DO-Build-Agents |
Parallel with Lint + Tests |
| Validate & Test | UnitTests | DO-Build-Agents |
Parallel with Lint + TypeCheck |
| Build | PackageWeb | DO-Build-Agents |
Docker cache |
| Build | PackageDocs | DO-Build-Agents |
Docker cache |
| Build | PackageGateway | DO-Build-Agents |
Docker cache |
| Build | PackageOrderService | DO-Build-Agents |
Docker cache |
| Build | PackagePrintService | DO-Build-Agents |
Docker cache |
| Build | PackageShippingService | DO-Build-Agents |
Docker cache |
| Build | PackageGridflockService | DO-Build-Agents |
Docker cache |
| Build | PackageSlicer | DO-Build-Agents |
Docker cache |
Jobs Remaining on MS-Hosted¶
| Stage | Job | Why |
|---|---|---|
| Validate & Test | Lint | Lightweight, no Docker needed |
| Build | DetectAffected | Lightweight Nx detection |
| Build | BuildAll | Nx compilation only, no Docker |
| DeployStaging | DeployStaging | Needs SSH keys from Secure Files |
| AcceptanceTest | All jobs | Needs browser/Playwright |
| DeployProduction | All jobs | Needs SSH keys from Secure Files |
Job Distribution Strategy¶
Validate & Test Stage (3 agents in parallel)¶
Build & Package Stage (2 agents in parallel)¶
Note: Azure DevOps automatically distributes jobs across available agents in the pool. The 8 Docker package jobs will be picked up by whichever agent instance is free, naturally balancing across the 2 instances.
Setup Instructions¶
Prerequisites¶
- DigitalOcean droplet created with Ubuntu 22.04 LTS (4 vCPU / 8 GB RAM)
- SSH access to the droplet (root or sudo user)
- Azure DevOps PAT with scope:
Agent Pools (read, manage) - Agent pool
DO-Build-Agentscreated in Azure DevOps (see Agent Pool Configuration)
Step 1: Create the Agent Pool in Azure DevOps¶
- Navigate to
https://dev.azure.com/{your-org}/_settings/agentpools - Click Add pool → Self-hosted
- Name:
DO-Build-Agents - Enable Auto-provision this agent pool in all projects
- Click Create
Step 2: Generate a Personal Access Token (PAT)¶
- Navigate to
https://dev.azure.com/{your-org}/_usersSettings/tokens - Click New Token
- Name:
Build Agent Registration - Scopes: Agent Pools →
Read & manage - Expiration: Set to a reasonable period (the PAT is only needed during setup)
- Copy the token — it will not be shown again
Step 3: SSH into the Droplet and Run Setup¶
# SSH into the droplet
ssh root@<DROPLET_IP>
# Download the setup script (or SCP it from your machine)
# Option A: SCP from local machine
scp deployment/build-agent/setup-build-agent.sh root@<DROPLET_IP>:/tmp/
# Option B: Or copy-paste the script contents
# Make it executable and run
chmod +x /tmp/setup-build-agent.sh
/tmp/setup-build-agent.sh \
--org-url https://dev.azure.com/YOUR_ORG \
--pat YOUR_PERSONAL_ACCESS_TOKEN \
--pool DO-Build-Agents \
--agents 2
Step 4: Verify Agent Registration¶
- Go to
https://dev.azure.com/{your-org}/_settings/agentpools - Click on DO-Build-Agents
- You should see 2 agents online:
do-build-agent-1anddo-build-agent-2
Step 5: Warm the Docker Cache (Optional but Recommended)¶
Trigger a full pipeline run with ForceFullVersioningAndDeployment = true to populate the Docker layer cache on the self-hosted agents. Subsequent builds will be significantly faster.
Maintenance¶
Automated Maintenance¶
The setup script installs two cron jobs:
| Schedule | Script | Purpose |
|---|---|---|
| Weekly | /etc/cron.weekly/docker-cleanup |
Remove dangling images and old build cache (>7 days) |
| Daily | /etc/cron.daily/disk-check |
Check disk usage, aggressive cleanup at >90% |
Cleanup logs are written to /var/log/docker-cleanup.log.
Manual Maintenance¶
Check Agent Status¶
# Check both agent services
systemctl status vsts.agent.*
# View agent logs (live)
journalctl -u vsts.agent.* -f
Restart Agents¶
# Restart a specific agent
/opt/azagent/agent-1/svc.sh stop
/opt/azagent/agent-1/svc.sh start
# Restart all agents
for i in 1 2; do
/opt/azagent/agent-${i}/svc.sh stop
/opt/azagent/agent-${i}/svc.sh start
done
Update Agent Version¶
# Stop the agent service first
/opt/azagent/agent-1/svc.sh stop
# The agent can self-update, or re-run the setup script with new version
cd /opt/azagent/agent-1
./config.sh remove --auth pat --token <PAT>
# Re-run setup or manually update
Manual Docker Cleanup¶
# Check disk usage
df -h /
docker system df
# Gentle cleanup (keep recent cache)
docker container prune --force --filter "until=168h"
docker image prune --force
# Aggressive cleanup (when disk is full)
docker system prune --all --force --filter "until=48h"
Update Pre-installed Tools¶
# Update Cosign
curl -sSL https://github.com/sigstore/cosign/releases/download/v2.x.x/cosign-linux-amd64 \
-o /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign
# Update Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Update Node.js (if major version changes)
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
# Update .NET SDK
apt-get update && apt-get install -y dotnet-sdk-8.0
# Update reportgenerator (for azagent user)
su - azagent -c "dotnet tool update -g dotnet-reportgenerator-globaltool"
Monitoring & Troubleshooting¶
Agent Appears Offline in Azure DevOps¶
# Check if the service is running
systemctl status vsts.agent.*
# Check agent logs
journalctl -u vsts.agent.* --since "1 hour ago"
# Restart the agent
/opt/azagent/agent-1/svc.sh stop
/opt/azagent/agent-1/svc.sh start
Docker Builds Failing with OOM¶
# Check memory usage during builds
free -h
docker stats --no-stream
# If consistently OOM, consider:
# 1. Reducing to 1 agent instance (--agents 1)
# 2. Upgrading droplet to 8 vCPU / 16 GB RAM
Disk Space Running Low¶
# Check what's using space
df -h /
docker system df
du -sh /opt/azagent/*/
# Run manual cleanup
docker system prune --all --force --filter "until=48h"
# Check cleanup log
tail -50 /var/log/docker-cleanup.log
Docker Cache Not Being Used¶
# Verify buildx builder exists
docker buildx ls
# Inspect the builder
docker buildx inspect forma3d-builder
# If builder is missing, recreate it
docker buildx create --name forma3d-builder --driver docker-container --use
docker buildx inspect --bootstrap
Pipeline Jobs Not Picking Up Self-Hosted Agent¶
- Verify the pool name in
azure-pipelines.ymlmatches exactly:DO-Build-Agents - Check agent capabilities in Azure DevOps → Agent pools → DO-Build-Agents → Agents → Capabilities
- Ensure the pipeline has permission to use the pool
Security Considerations¶
Agent User¶
- Agents run as a dedicated
azagentuser (non-root) - The
azagentuser has Docker group access (required for builds) - No sudo privileges on the agent user
PAT Scope¶
- The PAT used during setup only needs
Agent Pools (read, manage)scope - The PAT is only used during initial registration — it is not stored on the droplet
- After registration, the agent uses its own OAuth token to communicate with Azure DevOps
Docker Socket¶
- The Docker socket is accessible to the
azagentuser via thedockergroup - This is standard for self-hosted agents running Docker builds
- The Docker daemon runs as root (standard Docker CE configuration)
Network Access¶
- The agent initiates outbound HTTPS connections to Azure DevOps (no inbound required)
- Docker builds push to DigitalOcean Container Registry over HTTPS
- No ports need to be opened in the droplet firewall for agent communication
Secrets Handling¶
- Pipeline secrets (DOCR_TOKEN, COSIGN_PASSWORD, etc.) are passed as environment variables at runtime
- Secrets are not persisted on the droplet between jobs
- The
cosign.keyfile is downloaded via Azure DevOps Secure Files during each job and cleaned up after
Cost Analysis¶
Monthly Cost Comparison¶
| Configuration | Monthly Cost | Build Stage Time | Full Pipeline |
|---|---|---|---|
| 1 MS-hosted (free tier) | $0 | ~75 min | ~133 min |
| +1 MS-hosted parallel job | $40 | ~38 min | ~96 min |
| +4 MS-hosted parallel jobs | $160 | ~19 min | ~77 min |
| DO 4vCPU/8GB (2 agents) | $48 | ~9-15 min | ~59-68 min |
ROI¶
- Cost: $48/month
- Time saved per full build: ~65 min (133 → 68 min)
- Builds per month (estimated): ~60 (2 per working day)
- Developer time saved: ~65 hours/month
- Effective cost per build: $0.80
Related Documentation¶
| Document | Description |
|---|---|
| Pipeline Reference | Full pipeline stage reference |
| Staging Deployment Guide | Staging setup and configuration |
| Cosign Setup Guide | Container image signing |
Setup Script: deployment/build-agent/setup-build-agent.sh |
Automated droplet provisioning |
Revision History:
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-02-15 | AI-generated | Initial self-hosted build agent documentation |