Azure

A starter recipe for self-hosted Flux on Azure — Container Apps, Azure Database for PostgreSQL, Key Vault.

A starter recipe for running Flux on Azure. Container Apps for compute, Azure Database for PostgreSQL Flexible Server for the database, Key Vault for the bootstrap configuration.

Topology

Provisioning order

1. Networking and database

Create a VNet with a subnet delegated for Container Apps and a subnet for the Postgres flexible server. Deploy Azure Database for PostgreSQL Flexible Server into the database subnet, with private access only. Create the flux database and the flux user.

2. Key Vault

az keyvault create --name flux-prod-kv --resource-group flux-rg --location eastus

az keyvault secret set --vault-name flux-prod-kv \
  --name bootstrap-token \
  --value "$(python -c 'import secrets; print(secrets.token_hex(32))')"

az keyvault secret set --vault-name flux-prod-kv \
  --name encryption-key \
  --value "$(python -c 'import secrets; print(secrets.token_hex(32))')"

az keyvault secret set --vault-name flux-prod-kv \
  --name database-url \
  --value "postgresql://flux:PASSWORD@flux-prod-pg.postgres.database.azure.com:5432/flux?sslmode=require"

3. Container Apps Environment

Create the environment in the Container Apps subnet, attached to the Log Analytics workspace.

4. Container Apps secret store

Container Apps has its own per-app secret store; the cleanest production pattern is to reference Key Vault secrets via the managed identity. Bicep snippet:

resource fluxServer 'Microsoft.App/containerApps@2024-03-01' = {
  name: 'flux-server'
  location: location
  identity: { type: 'UserAssigned', userAssignedIdentities: { '${managedIdentity.id}': {} } }
  properties: {
    managedEnvironmentId: env.id
    configuration: {
      secrets: [
        {
          name: 'bootstrap-token'
          keyVaultUrl: 'https://flux-prod-kv.vault.azure.net/secrets/bootstrap-token'
          identity: managedIdentity.id
        }
        {
          name: 'encryption-key'
          keyVaultUrl: 'https://flux-prod-kv.vault.azure.net/secrets/encryption-key'
          identity: managedIdentity.id
        }
        {
          name: 'database-url'
          keyVaultUrl: 'https://flux-prod-kv.vault.azure.net/secrets/database-url'
          identity: managedIdentity.id
        }
      ]
      ingress: {
        external: true
        targetPort: 8000
        transport: 'http'
      }
    }
    template: {
      containers: [
        {
          name: 'flux-server'
          image: 'fluxregistry.azurecr.io/flux:0.56.0'
          env: [
            { name: 'FLUX_DATABASE_URL', secretRef: 'database-url' }
            { name: 'FLUX_WORKERS__BOOTSTRAP_TOKEN', secretRef: 'bootstrap-token' }
            { name: 'FLUX_SECURITY__ENCRYPTION__ENCRYPTION_KEY', secretRef: 'encryption-key' }
            { name: 'FLUX_SECURITY__AUTH__API_KEYS__ENABLED', value: 'true' }
          ]
          resources: { cpu: 1, memory: '2Gi' }
        }
      ]
      scale: { minReplicas: 1, maxReplicas: 1 }
    }
  }
}

The minReplicas: 1, maxReplicas: 1 pair keeps the starter to one replica; to run more, raise maxReplicas and add sticky sessions to the ingress block (ingress: { ..., stickySessions: { affinity: 'sticky' } }) so each worker’s SSE stream stays on the replica that owns its dispatch queue. The worker app uses secrets referencing only the bootstrap token, and env includes FLUX_WORKERS__SERVER_URL pointing at the server app’s internal FQDN within the Container Apps Environment.

5. Worker app

Same shape, different container args, different scale rules. Container Apps supports scale rules on CPU, memory, HTTP RPS, or custom KEDA scalers. For worker pools, a custom scaler on Flux’s queue-depth metric (scraped via Prometheus, fed into Azure Monitor) is the production answer; CPU-target works as a starter.

6. Ingress and TLS

Container Apps’ built-in ingress terminates TLS with an auto-managed certificate on the *.azurecontainerapps.io subdomain. For a custom domain, attach the cert via the Container Apps custom-domain feature. Either way, TLS terminates at the ingress and Flux serves plain HTTP behind it — Flux’s flux start server has no --ssl-keyfile flag.

Secrets pattern

Three layers, in order of trust:

  1. Key Vault — the source of truth. RBAC-controlled, audited, geo-replicated.
  2. Container Apps managed identity — the bridge. Has Key Vault Secrets User on the vault; no human ever has both vault access and container access.
  3. Container env vars — injected at runtime via secretRef. The Container Apps secret store fetches from Key Vault using the managed identity; the container only sees the resolved value.

This same separation between bootstrap-configuration secrets (Key Vault) and Flux’s application-level secrets primitive (inside Flux’s Postgres database, encrypted at rest with the encryption key) applies on Azure as on the other clouds.

What can go wrong

Container Apps revision keeps failing

Symptom. The revision sticks in Failed or Activating; logs show Key Vault permission errors.

Cause. The managed identity isn’t assigned to the Container App, or it lacks Key Vault Secrets User on the vault.

Fix. Confirm az containerapp show --name flux-server --query identity lists the identity, then az role assignment create --assignee-object-id <mi-principal-id> --role 'Key Vault Secrets User' --scope <vault-resource-id>.

Workers flap after the server scales out

Symptom. A scale rule pushed the server above one replica and workers now cycle between ONLINE and OFFLINE; dispatch latency spikes.

Cause. The server app’s ingress has no sticky sessions, so each worker request lands on a random replica while its SSE stream lives on one. (Duplicate cron fires are not a symptom of extra replicas — the scheduler cycle is an advisory-lock singleton across replicas.)

Fix. Add stickySessions: { affinity: 'sticky' } to the server app’s ingress, or pin maxReplicas: 1 if you didn’t intend to scale. See High availability.

Postgres connection refused

Symptom. Server fails to start; logs show OperationalError: could not connect to server.

Cause. Container Apps subnet doesn’t have a route to the Postgres subnet, or the Postgres firewall doesn’t allow the Container Apps subnet.

Fix. Confirm both subnets are in the same VNet (or peered), and that the Postgres firewall rule allows the Container Apps subnet’s address range. Test from a debug container with psql.


This recipe is derived from Azure service documentation and Flux’s deployment requirements; the author hasn’t personally validated it end-to-end on Azure as of 2026-07. Treat managed-identity bindings, networking, and Container Apps scale rules as starting points to validate in a sandbox subscription before promoting.