DEMO/Infrastructure

← Back to dashboard

Cloud Hosting: Azure Container Apps

Audience: operator · Profile: both · Read this when: you are running the dashboard on Azure Container Apps: Postgres, the app with its gateway sidecar, the worker, custom domains, outbound addresses, and workload identity instead of a PAT.

Part of Cloud Hosting.

This is what the reference install runs, so everything below is verified against a live deployment rather than derived.

Postgres

az postgres flexible-server create \
  --resource-group RG-DASH --name pg-dash --location eastus2 \
  --tier Burstable --sku-name Standard_B1ms --version 16 \
  --storage-size 32 --public-access Disabled \
  --vnet vnet-dash --subnet snet-pg

Public access disabled means the Container Apps environment must be VNet-joined to reach it. B1ms allows only 50 connections, which is why the shipped DB_POOL_SIZE/DB_MAX_OVERFLOW defaults are a conservative 5 + 5 — the app is two gunicorn processes and the worker one, so a deployment holds 3 × (size + overflow) = 30. B2s and every General Purpose tier allow 429 or more.

Environment

az containerapp env create \
  --name cae-dash --resource-group RG-DASH --location eastus2 \
  --infrastructure-subnet-resource-id "$(az network vnet subnet show \
      -g RG-DASH --vnet-name vnet-dash -n snet-aca --query id -o tsv)"

The subnet must be delegated to Microsoft.App/environments. Leave the environment external (the default) — VNet-joining is about reaching the private database on the way out, not about hiding ingress.

The app, with its gateway sidecar

Create the app first, then add the sidecar — az containerapp create takes one container, and a second is a template edit.

Create it pointing ingress at 8000, the app's own port, so you have a working dashboard to check before adding anything. Moving ingress to the gateway's port 80 is part of the same update that adds the gateway; do it earlier and ingress points at a port nothing is listening on.

az containerapp create \
  --name dash --resource-group RG-DASH --environment cae-dash \
  --image chrweav/infra-dashboard:latest \
  --target-port 8000 --ingress external \
  --cpu 1 --memory 2Gi --min-replicas 1 --max-replicas 1 \
  --secrets database-url="postgresql://..." jwt-secret-key="$(openssl rand -hex 32)" \
  --env-vars \
      DATABASE_URL=secretref:database-url \
      JWT_SECRET_KEY=secretref:jwt-secret-key \
      PUBLIC_BASE_URL=https://dash.example.com \
      WEBAUTHN_RP_ID=dash.example.com \
      WEBAUTHN_ORIGIN=https://dash.example.com \
      CORS_ORIGINS=https://dash.example.com \
      APP_ENV=production

Then add the sidecar. There is no CLI flag for a second container, so this is a template edit: export the current YAML, add an entry under properties.template.containers, and apply it back.

- name: gateway
  image: <registry>/dash-gateway:1
  resources: { cpu: 0.25, memory: 0.5Gi }
  env:
    - name: AGENT_HOSTNAME
      value: agents.example.com
    - name: UI_HOSTNAME
      value: dash.example.com
az containerapp show   -n dash -g RG-DASH -o yaml > dash.yaml
# …add the gateway container to properties.template.containers…
az containerapp update -n dash -g RG-DASH --yaml dash.yaml
az containerapp ingress update -n dash -g RG-DASH --target-port 80

After the port move, the app is reachable only through the gateway — which means only on the two hostnames it knows about. The environment's default *.azurecontainerapps.io name will start returning 404, and that is the design working, not a fault.

PUBLIC_BASE_URL with two hostnames. It is the remote-agent signing audience, and enrolment codes embed it — so once agents enrol on agents.example.com, set it to that host. It used to double as the SSO callback origin, which then pointed the IdP at a hostname the gateway 404s for everything but /api/agent/*. The SSO callbacks now come from UI_BASE_URL, else WEBAUTHN_ORIGIN (already https://dash.example.com above), so nothing more is needed as long as WEBAUTHN_ORIGIN is the UI host. Set UI_BASE_URL=https://dash.example.com explicitly if it is not. Register https://dash.example.com/api/auth/oauth/{azure,oidc}/callback with the IdP — see OIDC Step 1.

Custom domains

az containerapp hostname add    -n dash -g RG-DASH --hostname dash.example.com
az containerapp hostname bind   -n dash -g RG-DASH --hostname dash.example.com \
                                --validation-method CNAME

Repeat for the agent hostname. Two things worth knowing: bind both names before enrolling any agent, and a managed certificate often reports a bind error and then succeeds anyway — wait and re-check before you start troubleshooting a problem you do not have.

The worker

az containerapp create \
  --name dash-worker --resource-group RG-DASH --environment cae-dash \
  --image chrweav/infra-dashboard:latest \
  --command python --args "-m" "web_dashboard.jobs_worker" \
  --cpu 0.5 --memory 1Gi --min-replicas 1 --max-replicas 1 \
  --secrets database-url="postgresql://..." jwt-secret-key="<same key>" \
  --env-vars DATABASE_URL=secretref:database-url \
             JWT_SECRET_KEY=secretref:jwt-secret-key \
             PUBLIC_BASE_URL=https://dash.example.com APP_ENV=production

Full detail, including why the worker is its own Container App rather than another sidecar: job-worker.md.

Pages

The larger parts have a page each:

Page Read it when
The SPIRE sidecar on Azure Container Apps you are running the dashboard's SPIRE server as a sidecar on Azure Container Apps
Outbound addresses on Azure Container Apps managed nodes' firewalls need the dashboard's outbound addresses, and you are choosing how to give them
Pathfinder without a PAT on Azure Container Apps you want the dashboard on Azure Container Apps to reach BeyondTrust Pathfinder with a managed identity instead of a stored token