Azure Container Apps
Run Grunt in your Azure subscription with managed PostgreSQL and durable blob storage
Azure Container Apps (ACA) runs Grunt in your Azure subscription with managed PostgreSQL and blob storage. Services run with Production configuration, Microsoft Entra sign-in, HTTPS and deployment enforcement. An administration machine runs the installer; the applications run in ACA.
#Get the ACA release
Download the ACA release package and its SHA-256 checksum. The package pulls digest-pinned images from acrgruntonpremprod.azurecr.io; use the registry credentials supplied for your deployment. It contains azure/README.md, azure/deploy.py, azure/plan.json and release.json.
For ACA, follow the setup and verification steps on this page. For Compose upgrades, follow the Compose upgrade instructions, including configuration changes as well as image pins. The separate Compose download does not include the ACA installer.
#Before you start
You need:
- An Azure subscription, a region supporting Container Apps Consumption and PostgreSQL Flexible Server 17, and permission to create a new dedicated resource group, resources and role assignments. For example, Contributor plus User Access Administrator, and permission to register resource providers.
- A Linux or macOS administration machine with persistent storage, Python 3.9 or later, Docker, Azure CLI and access to a browser. On Windows you can use Windows Subsystem for Linux (WSL) with persistent storage, or an existing Linux management VM over SSH. This WSL is different from Grunt's Web Service Layer service, also called WSL below.
- An Entra app registration in the customer tenant and its client secret, plus a Grunt deployment client and separate pull-only registry credentials.
- Outbound access to Azure platform services, the image registry, Microsoft sign-in and Grunt's activation, renewal and aggregate usage endpoints. Images target Linux AMD64; an ARM64 administration machine needs Docker AMD64 emulation for the one-time key generator.
Check your tools and install the Azure CLI components:
python3 --version
docker --version
az version
az extension add --name containerapp --upgrade
az bicep install
For an existing Linux VM, connect from Windows with ssh -i <key.pem> <user>@<vm> and copy the downloaded ZIP and checksum with scp -i <key.pem> <local-file> <user>@<vm>:~/. Use your VM's actual user, address and key path. If OpenSSH reports UNPROTECTED PRIVATE KEY FILE, inspect the key owner and permissions with icacls <key.pem> on Windows. Remove only the extra access identified in the error while preserving the owner's access; follow your organization's key-management policy.
Sign in to the customer tenant and select the subscription explicitly:
az login --tenant <tenant-id>
az account list --output table
az account set --subscription <subscription-id>
az account show --output table
If a remote VM falls back to device-code login and your tenant blocks it with error 530035, use browser login through a loopback SSH tunnel instead of changing tenant security settings. On the VM run BROWSER='echo %s' az login --tenant <tenant-id>. In a second Windows terminal forward the callback port shown in the printed URL with ssh -i <key.pem> -N -L 127.0.0.1:<port>:127.0.0.1:<port> <user>@<vm>, then open the printed URL in your local browser and finish sign-in. Keep the tunnel open until login completes.
Register the required providers in the selected subscription; registration can take time:
for provider in Microsoft.App Microsoft.OperationalInsights Microsoft.Network Microsoft.DBforPostgreSQL Microsoft.Storage Microsoft.ManagedIdentity; do az provider register --namespace "$provider" --wait; done
Check the subscription's PostgreSQL capabilities for your intended region before deploying:
az postgres flexible-server list-skus --location <azure-region> --output json
Inspect the response for PostgreSQL 17, supported tiers, sizes and any restriction reasons, and check your subscription's PostgreSQL and Container Apps quotas. A listed size and available quota do not guarantee allocation; an existing VM in the region does not establish managed-database availability. Azure bills all resources to your subscription.
The installer creates a new VNet, private PostgreSQL access and public HTTPS endpoints for Auth, Public API, MCP and Editor. Core and the Web Service Layer remain internal to the Container Apps environment. It uses Azure's default domains and certificates.
#1. Configure and deploy
Download the ZIP and its checksum to the administration machine, in the same directory. Verify the checksum before extracting the package. Stop if the checksum check fails.
# Linux; on macOS use: shasum -a 256 -c grunt-on-premise-sha-13b82935a4e6-41419289929-1-linux-amd64.zip.sha256
sha256sum -c grunt-on-premise-sha-13b82935a4e6-41419289929-1-linux-amd64.zip.sha256
python3 -m zipfile -e grunt-on-premise-sha-13b82935a4e6-41419289929-1-linux-amd64.zip grunt-on-premise-aca
cd grunt-on-premise-aca
You do not need Grunt source code, Aspire, .NET or private build feeds.
Choose a unique private folder for each installation on the administration machine's persistent disk, outside the extracted release. Keep and securely back it up: it holds configuration, administrator credentials and installation keys. Replace customer-a below with a unique name and run these commands from the extracted release directory:
export GRUNT_ACA_DIR="$HOME/.local/share/grunt-aca/customer-a"
mkdir -p "$HOME/.local/share/grunt-aca" &&
mkdir -m 700 "$GRUNT_ACA_DIR" &&
(umask 077; set -C; cat azure/config.example.json > "$GRUNT_ACA_DIR/config.json")
If the installation folder already exists, these commands stop without overwriting its configuration. Choose a different name for a new installation. For a retry or upgrade, reuse the existing folder, configuration and state instead of running these initialization commands again. In a new terminal, set GRUNT_ACA_DIR to that same folder before running the installer or verification commands.
In the customer Entra tenant, create a single-tenant app registration and client secret. Copy its tenant ID, application ID and secret. Open $GRUNT_ACA_DIR/config.json and fill in every field:
| Fields | What to enter |
|---|---|
subscriptionId | The selected customer subscription ID. |
resourceGroup, location | A new dedicated resource group name and region, for example rg-grunt-customer and westeurope. Check region and quota availability first. |
prefix | 3–16 lowercase letters, digits or hyphens; start with a letter, end with a letter or digit, and avoid consecutive hyphens. |
registryUsername, registryPassword | Pull credentials for the registry listed in this release's release.json. The installer performs its own Docker registry login; a manual login is optional. |
ENTRA_TENANT_ID, ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET | The customer tenant ID, single-tenant Entra app's application ID and client secret for user sign-in. |
DEPLOYMENT_CLIENT_ID, DEPLOYMENT_CLIENT_SECRET | The separate deployment client credentials created in the Client Portal. |
Registry access, Grunt activation and customer Entra sign-in use separate credentials. Create your deployment client at portal.grunt.io and use the registry credentials supplied for the package. The Client Portal's Environment field is a label; it does not select an activation service or registry.
Run the installer from the extracted release directory:
python3 azure/deploy.py --config "$GRUNT_ACA_DIR/config.json" --state "$GRUNT_ACA_DIR/state"
The installer creates the state directory, credentials and signing keys, then provisions the infrastructure. Bootstrap, schema migrations and grants run as sequential jobs; a failed job stops application rollout. Creating a job resource is not the same as its execution succeeding. The installer prints the Entra Web redirect URI and saves public URLs in $GRUNT_ACA_DIR/state/endpoints.json.
Keep the configuration and state outside temporary containers and release extraction paths. They and the resource group's ownership tag can exist even if provisioning fails. Preserve them and rerun the same command to resume after resolving a failure; do not run two installers simultaneously. A state directory is bound to its subscription, region, resource group and prefix. For a different region, plan a separate installation and state; check ownership and confirm any abandoned group is empty before deciding on cleanup. Never replace keys or delete state to bypass an ownership error. Contact Grunt if existing state is lost.
#2. Enable Microsoft sign-in
In the Entra app registration created earlier, add the printed URI as a Web redirect:
https://<auth-app-FQDN>/signin-entra
Enable the ID-token email optional claim. In the enterprise application, require assignment and assign each allowed user directly, or assign groups if your tenant has the required Entra P1 or P2 licensing. Grunt accepts only the configured tenant and creates local users on first sign-in. An Entra email claim does not itself establish verified email for operations that require it.
#3. Verify the installation
Confirm the bootstrap, migration and grants executions succeeded in Azure before checking the applications. Run the public HTTPS health and identity discovery checks:
python3 azure/verify.py --endpoints "$GRUNT_ACA_DIR/state/endpoints.json"
Then complete these acceptance checks:
- In the Grunt Web Service Layer container's Azure Portal console, run the activation status command from
azure/README.md. Requireenabled: trueandstatus: "active"; healthy applications and Microsoft sign-in do not establish activation. - Open
<editor endpoint>/editor, sign in using an assigned Microsoft account, and create, save, export and reopen a chart. - Check activation and usage acknowledgement: after a saved change, confirm
acceptedThroughSequenceadvances andunreportedCommitsreturns to zero. Reporting can take time. - Restart the application revisions, then repeat sign-in, reopen and export, and confirm activation is still active.
- Connect an OAuth-capable MCP client to
<mcp endpoint>/mcp; complete registration, sign-in, consent and token exchange, then initialize, list tools and call a tool.
A successful restart checks persistence across revisions; it does not replace testing a backup restore or an upgrade of populated data.
#Capacity and operations
The default six apps each allocate 0.75 vCPU and 1.5 GiB across the service and gateway sidecar: 4.5 vCPU and 9 GiB in total, excluding migration jobs, PostgreSQL and Azure overhead. Each app keeps one replica running. maxReplicas defaults to 1 and can be set from 1 to 10. Configure high availability separately according to your requirements.
Set serviceSize in your configuration to medium (1.25 vCPU/2.5 GiB per app) or large (2 vCPU/4 GiB) if the default small allocation is insufficient. The setting applies to all six apps. More replicas do not increase the memory available to an individual render/export request.
PostgreSQL defaults to Burstable Standard_B2s, 32 GiB and seven-day backups. If allocation fails, consult Azure's deployment error and choose a supported databaseSku and databaseTier together; check the cost before changing size or region. Use PostgreSQL's regional capability information when selecting a size. Blob storage uses Standard LRS with versioning and seven-day soft delete. Size the installation for your workload and adjust capacity as needed. Default ACA ingress timeout and request limits apply to streaming and long-running operations.
Back up the entire state directory, private configuration, database and blobs together. Authority session keys live in encrypted blob storage; signing/encryption keys remain stable across container replacement. Test restores. Signing certificates expire after five years and require planned rotation. Keep secrets out of source control, logs and support attachments.
For an upgrade, extract the next verified release separately, take backups, and rerun its installer with the same configuration and state directory. Schedule maintenance for schema changes. Migrations run before rollout; schema rollback and mixed-version compatibility are not automatic. The installer refuses a resource group associated with different installation state.
#Troubleshooting
| Symptom | Check |
|---|---|
| Image pull denied | Match the registry in release.json with the pull credentials. Registry credentials do not activate a deployment. |
PostgreSQL SkuNotAvailable or version restriction | Inspect the deployment operation and the subscription's regional capabilities and quotas. Provisioning is not Ready. Preserve the owned group and state; agree any size or region change before retrying. |
| Installer fails before an Azure deployment appears | Inspect the local Azure CLI command log under ~/.azure/commands/; redact it before sharing. If a deployment exists, inspect its operations and job execution logs in Azure Portal. |
| Applications are healthy but activation is denied | Check the Client Portal, correct organization, client ID, enabled/expiry status, secret and network access. Credentials created in another environment or removed by a database reset will not activate this deployment. |
| Deployment client identity is lost | The local installation binds its client ID before activation. Rotate a secret on the same client ID when possible. Preserve the state, database and accounting history and contact Grunt before attempting recovery; do not replace the client ID or clear counters. |
Share the release identifier, approximate UTC time and redacted error code with support. Never send private configuration, state, access tokens or keys. The package README covers network ranges and further operational details. Custom domains, an existing VNet, private frontends, zone redundancy and PostgreSQL HA need additional configuration; Kubernetes/AKS manifests are not included.