Self-hosting

Install on AKS

Install Agent Relay on Azure Kubernetes Service with in-cluster PostgreSQL, MinIO and Traefik. Community-contributed preview.

Agent version of this guide · Markdown

Install Agent Relay on Azure Kubernetes Service. It follows Install on EKS and changes only what Azure forces. Run the steps in order and check each Done when line before you move on.

Release: agent-relay-2026.11.2 (chart 0.7.7). Community-contributed: proven once on AKS, see Known limits.

Status: community-contributed, proven once. One fresh install on 2026-11-14 (AKS 1.36, East US 2) of release agent-relay-2026.11.0 (chart 0.6.46) with the searchIndexer.locker.credentials option applied to that chart by hand. The four edge checks passed, a headless sign-in by operator device approval worked, two agents exchanged messages through Relaycast, and an uploaded session was indexed. See Known limits before a production rollout.

This guide installs release agent-relay-2026.11.2 (chart agent-relay 0.7.7, sessions-pipeline 0.8.32), the first release that carries searchIndexer.locker.credentials. Earlier releases do not render the indexer's MinIO keys, so its uploads are never indexed.

The files are in docs/azure-install of the infra repository.

FileWhat it is
values.yamlSite values, applied after the chart package's values-eks.yaml and before the release's release-images.values.yaml
postgres-tls.yamlcert-manager Issuers and Certificates: a 10-year CA and the server certificate for the in-cluster Postgres
postgres-values.yamlOverrides for deps/cnpg-cluster-values.yaml (the pinned HoopDb Postgres)
postgres-setup.shDatabases, roles and credential files: the selfhosted counterpart of rds-setup.sh and cloud-sql-setup.sh
ingress.yamlThe edge: one Traefik Ingress, the host and path rules of scripts/selfhost/eks-alb-ingress.sh

How Azure differs

AWS (EKS guide)Azure (this guide)Why
RDS + rds-setup.sh (databaseMode: rds)In-cluster CloudNativePG, the pinned deps/cnpg-cluster-values.yaml, + postgres-setup.sh (databaseMode: selfhosted)The services image has rds, cloudsql and selfhosted database modes. Azure Database for PostgreSQL fits none: its admin role is neither rds_superuser nor cloudsqlsuperuser, and it is not a superuser.
S3 through EKS Pod IdentityThe chart's bundled MinIO (session-object-storage) on an Azure disk, static keys from a SecretThe services speak S3 or GCS. There is no Azure Blob Locker provider.
OpenSearch (SigV4)PostgreSQL search (search.backend: postgres), as on GKEThe OpenSearch writer signs with AWS credentials.
ALB + ACMTraefik + cert-manager (Let's Encrypt HTTP-01)Same hosts, paths and edge denials.
Route 53 or your DNSOptional: an Azure public IP DNS label per host (<label>.<region>.cloudapp.azure.com)No DNS zone is needed.

One chart option makes this possible: the search indexer could not take static S3 keys, so it could not read a MinIO Locker. searchIndexer.locker.credentials.mode: secret (with existingSecret, accessKeyIdKey, secretAccessKeyKey) renders LOCKER_ACCESS_KEY_ID and LOCKER_SECRET_ACCESS_KEY, as ingestEdge and nativeConsumer already can. It is unset by default and existing renders do not change.

Before you start

Tools: az, kubectl, helm 3.10+, jq, yq, openssl.

The chart and its image pins come from the release's assets; nothing is built from a checkout. The infra repository supplies only the guide's files and the pinned deps/ values. Every later command runs in docs/azure-install:

git clone https://github.com/AgentWorkforce/infra.git && cd infra/docs/azure-install
mkdir release && gh release download agent-relay-2026.11.2 -R AgentWorkforce/infra -D release
(cd release && shasum -a 256 -c SHA256SUMS && tar -xzf agent-relay-0.7.7.tgz) | tail -1
# Public GHCR images: no mirror root.
sed 's#__IMAGE_MIRROR_ROOT__/##' release/release-images.values.yaml > release-images.values.yaml
CHART=release/agent-relay-0.7.7.tgz

No gh? Download the same assets from the release page into release/.

Done when: every checksum line ends in OK (the last is release-notes.md: OK).

1. Cluster

az group create -n <resource group> -l <region>
az aks create -g <resource group> -n <cluster> --node-count 2 --node-vm-size <4 vCPU / 16 GB size> \
  --node-osdisk-size 128 --enable-cluster-autoscaler --min-count 2 --max-count 4 \
  --network-plugin azure --network-plugin-mode overlay --network-dataplane cilium \
  --enable-oidc-issuer --enable-workload-identity --enable-managed-identity --no-ssh-key
az aks get-credentials -g <resource group> -n <cluster>

Done when: kubectl get nodes shows two Ready nodes and one StorageClass is marked (default).

2. Operators and the edge

helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo add jetstack https://charts.jetstack.io
helm repo add traefik https://traefik.github.io/charts
helm upgrade --install cnpg cnpg/cloudnative-pg --version 0.29.1 -n cnpg-system --create-namespace \
  -f ../../deps/cnpg-operator-values.yaml --wait
helm upgrade --install cert-manager jetstack/cert-manager -n cert-manager --create-namespace \
  --set crds.enabled=true --wait
helm upgrade --install traefik traefik/traefik -n traefik --create-namespace \
  --set "service.annotations.service\.beta\.kubernetes\.io/azure-dns-label-name=<root label>" \
  --set service.spec.externalTrafficPolicy=Local --set ingressClass.enabled=true --wait

The CloudNativePG version is this repository's pin (deps/pins.txt).

For Azure DNS labels, add one more LoadBalancer Service per remaining host (api, cast, history), each selecting the Traefik pods and carrying its own service.beta.kubernetes.io/azure-dns-label-name annotation. With your own DNS zone, point four names at Traefik's one address instead.

Create a ClusterIssuer named letsencrypt with an HTTP-01 solver on the traefik ingress class.

Done when: all four host names resolve to a Traefik address.

3. Postgres

kubectl create namespace agent-relay
kubectl apply -f postgres-tls.yaml
kubectl -n agent-relay wait --for=condition=Ready certificate/postgres-ca certificate/hoopdb-server
helm upgrade --install hoopdb cnpg/cluster --version 0.9.0 -n agent-relay \
  -f ../../deps/cnpg-cluster-values.yaml -f postgres-values.yaml

The server certificate is issued under a 10-year CA because the read API, ingest edge and search indexer pin the CA's SHA-256. CloudNativePG's own CA lasts 90 days and rotates.

Done when: kubectl -n agent-relay get cluster hoopdb reports Cluster in healthy state.

4. Databases and roles

export SERVICES_IMAGE="$(yq '."sessions-pipeline".worker.image | .repository + "@" + .digest' release-images.values.yaml)"
bash postgres-setup.sh pre

It creates the relaycontrol database and its roles, the HoopDb bootstrap roles, the postgres-ca ConfigMap and every credential file under ~/.agent-relay-install (mode 0700). It prints no password. SQL runs as the CloudNativePG superuser by kubectl exec into the primary, as the chart's own privileged Jobs do.

Done when: it ends with pre done.

5. Values and Secrets

Replace the placeholders in values.yaml: __ORG_ID__, __ORG_NAME__, __ADMIN_EMAIL__, __HOST_ROOT__, __HOST_API__, __HOST_CAST__, __HOST_HISTORY__, __SESSIONS_BUCKET__, and __POSTGRES_CA_SHA256__ (from ~/.agent-relay-install/postgres-ca.sha256). __LLM_MODEL__ stays, as in the EKS guide.

Create the Secrets as in step 10 of the GKE guide, without the Relaycast database keys (Relaycast runs on SQLite here) and with one more Secret for MinIO:

S=~/.agent-relay-install/secret-files
( umask 077; mkdir -p "$S/sessions-pipeline-locker"
for k in selfhost-bootstrap-token selfhost-setup-key relaycast-internal-secret \
         relaycast-workspace-bootstrap-secret relaycontrol-introspect-secret session-read-api-secret \
         selfhost-encryption-key; do
  [ -s "$S/agent-relay-secrets/$k" ] || openssl rand -hex 32 | tr -d '\n' > "$S/agent-relay-secrets/$k"
done
[ -s "$S/agent-relay-secrets/SUMMARY_MASTER_KEYS" ] || printf 'k1:%s' "$(openssl rand -hex 32)" > "$S/agent-relay-secrets/SUMMARY_MASTER_KEYS"
cp "$S/agent-relay-secrets/relaycontrol-introspect-secret" "$S/agent-relay-ingest-edge-runtime/introspect-secret"
[ -s "$S/sessions-pipeline-locker/rootUser" ] || printf 'agentrelay' > "$S/sessions-pipeline-locker/rootUser"
[ -s "$S/sessions-pipeline-locker/rootPassword" ] || openssl rand -hex 24 | tr -d '\n' > "$S/sessions-pipeline-locker/rootPassword" )
for name in agent-relay-secrets agent-relay-read-api agent-relay-ingest-edge-runtime \
            search-indexer-runtime search-indexer-owner sessions-pipeline-locker; do
  kubectl -n agent-relay create secret generic "$name" --from-file="$S/$name" \
    --dry-run=client -o yaml | kubectl apply -f -
done

Done when: kubectl -n agent-relay get secrets lists all six Secrets.

6. Install

helm upgrade --install agent-relay "$CHART" --namespace agent-relay \
  -f release/agent-relay/values-eks.yaml -f values.yaml -f release-images.values.yaml --timeout 25m
for j in $(kubectl -n agent-relay get jobs -o name | grep -E 'relay-control-migrate|agent-relay-worker-migrate'); do
  kubectl -n agent-relay wait --for=condition=complete "$j" --timeout=20m
done

Done when: both migrate Jobs complete.

Expected: the read API, ingest edge, search indexer and the search-indexer-bootstrap Job fail until step 7, as on EKS.

7. Switch on the service logins

bash postgres-setup.sh post
kubectl -n agent-relay rollout restart deploy/agent-relay-read-api deploy/agent-relay-ingest-edge deploy/agent-relay-search-indexer
for j in $(kubectl -n agent-relay get jobs -o name | grep search-indexer-bootstrap); do kubectl -n agent-relay delete "$j"; done
helm upgrade --install agent-relay "$CHART" --namespace agent-relay \
  -f release/agent-relay/values-eks.yaml -f values.yaml -f release-images.values.yaml --timeout 25m
kubectl -n agent-relay wait --for=condition=complete "$(kubectl -n agent-relay get jobs -o name | grep search-indexer-bootstrap)" --timeout=10m
kubectl -n agent-relay rollout status deploy --timeout=10m
bash postgres-setup.sh finalize

Done when: three login ok ... tls=true lines, the bootstrap Job completes, every pod is Running, and finalize prints owner createrole: false.

8. Edge and checks

Replace the four __HOST_*__ placeholders in ingress.yaml, then:

kubectl apply -f ingress.yaml
kubectl -n agent-relay wait --for=condition=Ready certificate/agent-relay-tls --timeout=10m
curl -fsS "https://<api host>/readyz" && echo " api ok"
curl -fsS "https://<cast host>/health" && echo " cast ok"
curl -fsS -o /dev/null -w "dashboard %{http_code}\n" "https://<root host>/"
curl -s -o /dev/null -w "history %{http_code}\n" "https://<history host>/v1/sessions"

Done when: api ok, cast ok, dashboard 200 and history 401.

Edge denials go to a Service with no endpoints, as on GKE, so they answer 503 (the ALB edge answers 404).

9. First sign-in

Continue with step 15 of the GKE guide (setup-key sign-in, then users). The namespace is agent-relay.

Done when: the admin is signed in to the dashboard.

10. Output and the daily digest

values.yaml turns on the Output and daily-digest reads and the indexer's output facts writer, but a workspace answers them only after the output facts bootstrap Job has marked it (until then: 501 output_backfill_incomplete / digest_backfill_incomplete). The Job marks only workspaces that already have sessions, so run it after the first sessions of a workspace arrive. Merge this into the existing sessions-pipeline.searchIndexer block of values.yaml:

sessions-pipeline:
  searchIndexer:
    outputFacts:
      bootstrap:
        enabled: true
        runId: "1"
helm upgrade agent-relay "$CHART" --namespace agent-relay \
  -f release/agent-relay/values-eks.yaml -f values.yaml -f release-images.values.yaml --timeout 25m
kubectl -n agent-relay wait --for=condition=complete --timeout=2h \
  job -l app.kubernetes.io/component=output-facts-bootstrap

When another workspace starts uploading, change runId and run it again. Exit codes and verification are in the sessions-pipeline chart README.

Done when: the Job completes and the workspace's Output page loads.

Known limits

  • Any release other than agent-relay-2026.11.0 (patched); values.yaml renders against agent-relay-2026.11.2 but was not installed from it.
  • The output facts bootstrap (step 10).
  • Azure Database for PostgreSQL, Azure Blob Storage, Application Gateway.
  • NetworkPolicy enforcement beyond a default install, private clusters, internal load balancers.
  • Backup and restore, upgrades, HA (every stateful component is one replica on one Azure disk).
  • The desktop app, the dashboard, SSO, titles and summaries.
  • The Traefik edge keeps HTTP closed rather than redirecting it to HTTPS.

Stuck? See troubleshooting or support.