Mojo UpDocs
Self-hosted AIOE

Install on Azure

Deploy AIOE to Azure Container Apps with the Bicep template, with PostgreSQL, Redis, a registry and logs.

The Azure template (main.bicep in your AIOE release) builds a complete AIOE in one resource group: the API and console on Azure Container Apps, Azure Database for PostgreSQL Flexible Server, Azure Managed Redis, a container registry, a managed identity that pulls from it, and a Log Analytics workspace. It runs the same images as the Helm chart.

The deployment has two phases, because a container app cannot start before its image is in the registry: first the infrastructure, then the apps.

What it creates

Names start with aioe-<env>, where env is a parameter (for example prod).

ResourceNameSize and settings
Log Analytics workspaceaioe-<env>-logsPay as you go, 30-day retention. Container logs go here.
User-assigned identityaioe-<env>-idHolds AcrPull on the registry; every app pulls with it.
Container registryaioe<env>acrBasic, admin user off.
PostgreSQL Flexible Serveraioe-<env>-pgPostgreSQL 16, Burstable B2s, 32 GB, 14-day backups, no high availability, no geo-redundant backup. Database aioe, admin user aioe.
Azure Managed Redisaioe-<env>-redisBalanced B0, TLS 1.2 or later, port 10000, no eviction.
Container Apps environmentaioe-<env>-caeSends logs to the workspace.
API container appaioe-<env>-api0.5 vCPU, 1 GiB. One to five replicas, scaling on 200 concurrent requests. External HTTPS ingress, port 3001.
Console container appaioe-<env>-console0.25 vCPU, 0.5 GiB. One to three replicas. External HTTPS ingress, port 8080.
Relay and gateway appsaioe-<env>-relay, aioe-<env>-gatewayOnly with deployMesh=true. See Overlay and relays.

Database network access

The template lets Container Apps reach PostgreSQL through its public endpoint, with a firewall rule that admits Azure-internal addresses only. Moving the environment into a virtual network with a private endpoint is a hardening step the template does not do for you.

Before you start

  • The Azure CLI, signed in, with rights to create resources and role assignments in a resource group.
  • Your two app registrations, from Connect your identity provider.
  • Access to the aioe-api and aioe-console images for your release. The console image has the API's public address built into it, so it must be built for your API host. Ask Mojo Up if your release does not include one.
  • The two host names you will use, for example aioe.example.com and console.aioe.example.com, and access to their DNS zone.
  • A Key Vault, or another secret store, for the database password and the platform's keys.

Parameters

ParameterDefaultWhat it is
envprodShort environment name used in every resource name.
locationthe resource group'sAzure region.
deployAppsfalsefalse for phase 1, true once the images are in the registry.
imageTaglatestTag of aioe-api and aioe-console to run.
apiHost, consoleHostMojo Up's ownYour API and console host names. Always set them.
postgresAdminPassword(required, secure)Password of the database admin user aioe.
entraTenantId(required)Your Entra tenant ID. The issuer is built from it.
entraApiAudience(required)The API registration's client ID.
entraPublicClientId(required)The client registration's client ID.
entraApiScope(required)The full API scope, for example api://aioe.example.com/access.
tenantSlug, tenantOrganisationMojo Up's ownYour organisation's short name and display name. Always set them.
tenantDomainsMojo Up's ownYour email domains, comma separated. The API host and its default Azure host name are added for you.
deployMeshfalseDeploy the overlay's relay and gateway.
meshImageTaglatestTag of the aioe-mesh-services image.
gatewaySecretempty (secure)Shared secret between the API and the gateway. Needed with deployMesh.
signingKeyempty (secure)The platform's Ed25519 signing key as a JWK. Set it: the API can run up to five replicas, and they must share one key.
secretsKeyempty (secure)32 random bytes, base64, sealing the secrets the platform keeps. Empty means it keeps none.
entraClientSecretempty (secure)The API registration's client secret, for Azure DevOps through Entra. Empty means Azure DevOps links with a personal token only.
apiCertificateName, consoleCertificateNameemptyNames of the managed certificates for your host names, once bound. Every later deployment must carry them.

How to make the keys is in Platform keys and rotation.

Deploy

Create the resource group and keep the password

Terminal
RG=rg-aioe
az group create -n $RG -l australiaeast
PW=$(openssl rand -base64 24)
# Store $PW in your Key Vault now; every later deployment needs it.

Write a parameters file

parameters.json
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "env": { "value": "prod" },
    "imageTag": { "value": "<release tag>" },
    "apiHost": { "value": "aioe.example.com" },
    "consoleHost": { "value": "console.aioe.example.com" },
    "entraTenantId": { "value": "<tenant-id>" },
    "entraApiAudience": { "value": "<API registration client ID>" },
    "entraPublicClientId": { "value": "<client registration client ID>" },
    "entraApiScope": { "value": "api://aioe.example.com/access" },
    "tenantSlug": { "value": "example" },
    "tenantOrganisation": { "value": "Example Pty Ltd" },
    "tenantDomains": { "value": "example.com" }
  }
}

Do not put secure values in this file. Pass them on the command line from your secret store, or in a temporary parameters file you never commit.

Phase 1: the infrastructure

Terminal
az deployment group create -g $RG -n aioe-phase1 \
  --template-file main.bicep --parameters @parameters.json \
  --parameters deployApps=false postgresAdminPassword="$PW"

ACR=$(az deployment group show -g $RG -n aioe-phase1 \
  --query properties.outputs.acrLoginServer.value -o tsv)

Put the images in the registry

Import or push aioe-api and aioe-console with your release tag into the new registry, for example:

Terminal
az acr import -n ${ACR%%.*} --source <source registry>/aioe-api:<release tag> --image aioe-api:<release tag>
az acr import -n ${ACR%%.*} --source <source registry>/aioe-console:<release tag> --image aioe-console:<release tag>

Add --username and --password when the source registry is private.

Phase 2: the apps

Terminal
az deployment group create -g $RG -n aioe-phase2 \
  --template-file main.bicep --parameters @parameters.json \
  --parameters deployApps=true postgresAdminPassword="$PW" \
  signingKey="$SIGNING_KEY" secretsKey="$SECRETS_KEY"

The API applies the database migrations before it serves. Find the apps' default addresses with:

Terminal
az deployment group show -g $RG -n aioe-phase2 --query properties.outputs -o json

apiDefaultFqdn and consoleDefaultFqdn answer straight away; /healthz on the API reports its version.

Add your host names

Container Apps needs two records per host name: a CNAME to the app's default host name, and a TXT record asuid.<host> carrying the environment's verification ID.

Terminal
az containerapp env show -g $RG -n aioe-prod-cae \
  --query properties.customDomainConfiguration.customDomainVerificationId -o tsv
TypeNameValue
CNAMEaioeaioe-prod-api.<environment domain>
TXTasuid.aioethe verification ID
CNAMEconsole.aioeaioe-prod-console.<environment domain>
TXTasuid.console.aioethe verification ID
CNAMEaioe-discoveraioe.example.com (workbench discovery, see DNS discovery)

The environment domain is the environmentDomain output. Then bind a managed certificate to each host name:

Terminal
az containerapp hostname add  -g $RG -n aioe-prod-api --hostname aioe.example.com
az containerapp hostname bind -g $RG -n aioe-prod-api --hostname aioe.example.com \
  --environment aioe-prod-cae --validation-method CNAME
az containerapp hostname add  -g $RG -n aioe-prod-console --hostname console.aioe.example.com
az containerapp hostname bind -g $RG -n aioe-prod-console --hostname console.aioe.example.com \
  --environment aioe-prod-cae --validation-method CNAME

Find the certificate names the binding created (az containerapp env certificate list -g $RG -n aioe-prod-cae --managed-certificates-only -o table) and add them to your parameters file as apiCertificateName and consoleCertificateName.

Carry the certificate names on every deployment

A deployment without apiCertificateName and consoleCertificateName replaces the apps' custom-domain bindings with none, and your host names stop answering until you bind them again.

Logs

Container logs, including the API's JSON log lines, go to the Log Analytics workspace aioe-<env>-logs. For traces and metrics, set OTEL_EXPORTER_OTLP_ENDPOINT on the API app: see Observability.

Next steps

On this page