Skip to content

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.

  • 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, default 10.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.

  • Terraform 1.6+ and Azure CLI (az login against 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)
Terminal window
az login
az account set --subscription <subscription-id>
git clone https://github.com/broch-io/broch-deploy.git
cd 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_secret
export TF_VAR_auth_client_secret
terraform init
terraform plan
terraform apply

terraform 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.

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.

Terminal window
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 HTTP

Re-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.

Terminal window
curl https://broch.{your-domain}/healthz # liveness — 200 once the process is up
curl 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.

Terminal window
$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 it
export TF_VAR_auth_client_secret
terraform apply

Container 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.

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.

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.

Terminal window
read -rs TF_VAR_auth_client_secret # paste the new value, then Enter
export TF_VAR_auth_client_secret
terraform apply
terraform 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.

  • Postgres reachable only inside the VNet. To run pg_dump or psql, 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.

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:

Terminal window
$EDITOR database.tf # delete `prevent_destroy = true`
terraform destroy

This 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.