Azure Container Apps (Beta)
Deploy Broch on Azure with Container Apps, Postgres Flexible Server, and Key Vault for secrets. The Terraform module lives in broch-io/broch-deploy/terraform/azure-container-apps: clone the repo, fill in terraform.tfvars, terraform apply, then bind the custom domain.
Beta. This module is less exercised by automation than the Azure appliance (Marketplace or Azure CLI). Test it before you rely on it. Wildcard certs need extra setup: Container Apps managed certs don’t issue wildcards, and every tunnel URL is a subdomain.
What Gets Provisioned
Section titled “What Gets Provisioned”- Resource group + Log Analytics workspace (required by the Container Apps Environment)
- Container Apps Environment + Container App with HTTPS ingress, one replica
- User-assigned managed identity for the Container App, with read access to the Key Vault
- Key Vault (RBAC mode) holding the generated
BROCH_MASTER_KEY, the OIDC client secret, and the Postgres connection string - VNet (
vnet_address_space, default10.2.0.0/16) with subnets for the environment and the database - Postgres Flexible Server 16 + database, private: no public endpoint
- A custom-domain stub on the Container App (binding it is a one-time manual step, below)
All resources are created in your subscription. Broch has no access to your environment.
To estimate spend, use the Azure Pricing Calculator with your chosen sizes.
Prerequisites
Section titled “Prerequisites”- Terraform 1.6+ and Azure CLI (
az loginagainst the target subscription), with permission to register resource providers. The first plan registers the seven this module needs, which takes 5–10 minutes on a fresh subscription - Owner, or Contributor + User Access Administrator, on the target subscription (the Key Vault RBAC role assignments need the latter)
- Wildcard domain & DNS on any provider. You create the records yourself
- Identity provider app registered, with redirect URI
https://broch.{your-domain}/auth/callback(your Broch server URL — no wildcard in the redirect URI) - License — activated in-app after first sign-in (nothing to set before deploy)
az loginaz account set --subscription <subscription-id>
git clone https://github.com/broch-io/broch-deploy.gitcd broch-deploy/terraform/azure-container-apps
cp terraform.tfvars.example terraform.tfvars$EDITOR terraform.tfvars # non-secret values; delete the auth_client_secret line
# Pass the IdP client secret as an environment variable, not in the file.# Set it before every plan, apply or destroy (upgrades included): an empty# value overwrites the stored secret. Paste the value, then press Enter.read -rs TF_VAR_auth_client_secretexport TF_VAR_auth_client_secret
terraform initterraform planterraform applyterraform plan refuses settings Broch would refuse at startup: an unknown auth_provider, a missing client ID or secret, a blank wildcard_hostname, or a value your provider needs (Auth0: auth_domain and auth_audience; Okta: auth_domain; Entra ID: auth_tenant_id and auth_instance, such as https://login.microsoftonline.com/; generic OIDC: auth_authority). Values saved later in the app don’t count, so keep them in terraform.tfvars too. vnet_address_space must be an IPv4 /16 and can’t change after the first apply.
Terraform generates the master key and writes it to Key Vault. The Terraform state holds every secret in plaintext (master key, client secret, database credentials): keep terraform.tfstate somewhere access-controlled, such as an azurerm remote backend.
The Container App starts on its default *.azurecontainerapps.io domain.
Bind the custom domain
Section titled “Bind the custom domain”Azure binds a custom domain only after you prove DNS control, so this is a one-time step after the first apply. The commands use the default name_prefix (broch); adjust the resource names if you changed it.
APP_FQDN=$(terraform output -raw container_app_fqdn)VERIF_ID=$(terraform output -raw container_app_verification_id)DOMAIN=$(terraform output -raw broch_url | sed 's|https://||')
# Add these DNS records in your provider:# A broch.{your-domain} → the environment's static IP (what $APP_FQDN resolves to)# TXT asuid.broch.{your-domain} → $VERIF_ID
az containerapp hostname bind \ --hostname "$DOMAIN" \ --resource-group broch-rg \ --name broch-app \ --validation-method HTTPRe-run the bind if the managed cert hasn’t propagated yet (5–10 minutes). That cert covers broch.{your-domain} only. Tunnel subdomains (*.broch.{your-domain}) need a wildcard cert you provision yourself, served by Front Door or Application Gateway in front of the app, or uploaded to the Container Apps environment. Point the *.broch.{your-domain} record at whichever serves it. See TLS Certificates.
Verify
Section titled “Verify”curl https://broch.{your-domain}/healthz # liveness — 200 once the process is upcurl https://broch.{your-domain}/healthz/ready # readiness — 200 once the license and database check out/healthz/ready returns 503 until an admin completes setup. Sign in at https://broch.{your-domain} and finish in the browser: see Self-Managed Deployment → After You Install.
Upgrading
Section titled “Upgrading”$EDITOR terraform.tfvars # set broch_image = "ghcr.io/broch-io/broch:<new-version>"read -rs TF_VAR_auth_client_secret # the current value, unless you're rotating itexport TF_VAR_auth_client_secretterraform applyContainer Apps rolls out a new revision with the new image. Database migrations run on the new revision’s first start. Pin a tag, not latest. See Upgrading & Rollback for the back-up-first and rollback procedure.
Backup
Section titled “Backup”Postgres Flexible Server takes automated daily backups with point-in-time restore, retained for 7 days. Restore through the Azure portal or az postgres flexible-server restore. Back up the database and the master key (in Key Vault), and take a backup before every upgrade. See the Azure Database for PostgreSQL backup docs.
Rotating Secrets
Section titled “Rotating Secrets”Terraform owns the Key Vault secrets, and the app reads a pinned version of each. Rotate through Terraform, not az keyvault secret set: a direct edit is never picked up, and the next apply reverts it.
read -rs TF_VAR_auth_client_secret # paste the new value, then Enterexport TF_VAR_auth_client_secretterraform applyterraform apply
az containerapp revision restart \ --name broch-app \ --resource-group broch-rg \ --revision "$(az containerapp revision list --name broch-app --resource-group broch-rg --query '[0].name' -o tsv)"The first apply writes a new secret version; the second points the app at it, and is a no-op if the first already did. The restart loads it. Don’t rotate the master key casually: it wraps all at-rest encrypted state, and changing it is recoverable but not free: the encrypted state self-heals, signed-in users re-authenticate once, and usage counters reset. See At-Rest Encryption.
Tradeoffs: What the Default Module Is Not
Section titled “Tradeoffs: What the Default Module Is Not”- Postgres reachable only inside the VNet. To run
pg_dumporpsql, use a VM or Cloud Shell attached to the VNet, or peer a network you control. - Single replica (
min_replicas = max_replicas = 1). No autoscaling; see Scaling. - No Front Door or Application Gateway. Add one with your wildcard cert, or upload the cert to the environment, to serve tunnel subdomains.
See the module README for the full list.
Teardown
Section titled “Teardown”The Postgres server carries prevent_destroy, so terraform destroy stops at plan. To keep the data, back up the database and record the master key first, then:
$EDITOR database.tf # delete `prevent_destroy = true`terraform destroyThis deletes the whole resource group, including the Postgres server and its data and anything you added to the group by hand. If destroy stops on a subnet still in use, or with polling support for the Content-Type "" was not implemented, run it again.
Key Vault has purge protection on: the destroyed vault, master key included, stays recoverable for 7 days (az keyvault recover) and can’t be purged early. The vault name carries a random suffix, so a re-apply doesn’t collide with it.