Deploy SupportPilot to Azure
Azure Container Apps gives you serverless containers with built-in HTTPS ingress and autoscaling. Azure Database for PostgreSQL stores checkpoints, Key Vault holds secrets, and a managed identity ties it together without passwords in config.
Prerequisites
- An Azure subscription and the Azure CLI.
- Permission to assign roles (Owner or User Access Administrator) on the resource group.
Step-by-step
Log in, register providers, set variables
az login
az extension add --name containerapp --upgrade
az provider register --namespace Microsoft.App
az provider register --namespace Microsoft.OperationalInsights
export RG=supportpilot-rg LOC=eastus
export SUFFIX=$RANDOM # globally-unique names
export ACR=supportpilot$SUFFIX KV=sp-kv-$SUFFIX PG=sp-pg-$SUFFIX
az group create -n $RG -l $LOC
Build the image in ACR
az acr create -g $RG -n $ACR --sku Basic
az acr build -r $ACR -t supportpilot:v1 . # cloud build, no local Docker needed
PostgreSQL Flexible Server
export DB_PASS=$(openssl rand -hex 16)
az postgres flexible-server create -g $RG -n $PG -l $LOC \
--admin-user agent --admin-password "$DB_PASS" \
--tier Burstable --sku-name Standard_B1ms --storage-size 32 --version 16 \
--public-access 0.0.0.0 # allow Azure services only
az postgres flexible-server db create -g $RG -s $PG -d agent
For production, deploy the server with private access (VNet integration) and put the Container Apps environment in the same VNet.
Key Vault and secrets
az keyvault create -g $RG -n $KV -l $LOC --enable-rbac-authorization true
# give yourself permission to write secrets
az role assignment create --assignee $(az ad signed-in-user show --query id -o tsv) \
--role "Key Vault Secrets Officer" --scope $(az keyvault show -n $KV --query id -o tsv)
az keyvault secret set --vault-name $KV -n anthropic-api-key --value "$ANTHROPIC_API_KEY"
az keyvault secret set --vault-name $KV -n database-url \
--value "postgresql://agent:$DB_PASS@$PG.postgres.database.azure.com:5432/agent?sslmode=require"
Managed identity with least privilege
az identity create -g $RG -n supportpilot-id
export ID_RES=$(az identity show -g $RG -n supportpilot-id --query id -o tsv)
export ID_PRINCIPAL=$(az identity show -g $RG -n supportpilot-id --query principalId -o tsv)
az role assignment create --assignee-object-id $ID_PRINCIPAL --assignee-principal-type ServicePrincipal \
--role AcrPull --scope $(az acr show -n $ACR --query id -o tsv)
az role assignment create --assignee-object-id $ID_PRINCIPAL --assignee-principal-type ServicePrincipal \
--role "Key Vault Secrets User" --scope $(az keyvault show -n $KV --query id -o tsv)
The app pulls images and reads secrets as this identity — no registry passwords or secret values stored in the app config.
Create the Container Apps environment and app
az containerapp env create -g $RG -n supportpilot-env -l $LOC
export KV_URI=$(az keyvault show -n $KV --query properties.vaultUri -o tsv)
az containerapp create -g $RG -n supportpilot --environment supportpilot-env \
--image $ACR.azurecr.io/supportpilot:v1 \
--registry-server $ACR.azurecr.io --registry-identity $ID_RES \
--user-assigned $ID_RES \
--target-port 8080 --ingress external \
--cpu 0.5 --memory 1.0Gi \
--min-replicas 1 --max-replicas 10 \
--scale-rule-name http-load --scale-rule-type http --scale-rule-http-concurrency 40 \
--secrets "anthropic-key=keyvaultref:${KV_URI}secrets/anthropic-api-key,identityref:$ID_RES" \
"db-url=keyvaultref:${KV_URI}secrets/database-url,identityref:$ID_RES" \
--env-vars MODEL=anthropic:claude-sonnet-5-5 \
ANTHROPIC_API_KEY=secretref:anthropic-key \
DATABASE_URL=secretref:db-url
keyvaultref:secrets are fetched by the managed identity and refreshed automatically.- The HTTP scale rule adds replicas when concurrent requests per replica exceed 40; set
--min-replicas 0to scale to zero in dev. - Ingress gives you a managed HTTPS endpoint out of the box.
Smoke test
URL=https://$(az containerapp show -g $RG -n supportpilot --query properties.configuration.ingress.fqdn -o tsv)
curl -s $URL/healthz
curl -s $URL/chat -H 'content-type: application/json' \
-d '{"thread_id":"az-1","message":"Can I get a refund on a gift card?"}'
az containerapp logs show -g $RG -n supportpilot --follow
CI/CD with GitHub Actions (OIDC)
Create an Entra app registration with a federated credential for your repo's main branch, then grant it Contributor on the resource group and AcrPush on the registry.
APP_ID=$(az ad app create --display-name supportpilot-gh --query appId -o tsv)
az ad sp create --id $APP_ID
az ad app federated-credential create --id $APP_ID --parameters '{
"name": "gh-main", "issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:YOUR_ORG/YOUR_REPO:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"]}'
az role assignment create --assignee $APP_ID --role Contributor --scope $(az group show -n $RG --query id -o tsv)
az role assignment create --assignee $APP_ID --role AcrPush --scope $(az acr show -n $ACR --query id -o tsv)
name: deploy-azure
on:
push:
branches: [main]
permissions:
contents: read
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install -r requirements-dev.txt && pytest -q
- uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
- run: |
az acr build -r ${{ vars.ACR_NAME }} -t supportpilot:${{ github.sha }} .
az containerapp update -g supportpilot-rg -n supportpilot \
--image ${{ vars.ACR_NAME }}.azurecr.io/supportpilot:${{ github.sha }}
Each update creates a new revision. Use multiple-revision mode with traffic splitting for blue/green or canary releases.
Using Azure-hosted models
Use Azure OpenAI or models from Azure AI Foundry (which also offers Claude) so traffic stays in your tenant:
# requirements.txt: add langchain-openai
# MODEL=azure_openai:<your deployment name>
az containerapp update -g $RG -n supportpilot \
--set-env-vars MODEL=azure_openai:gpt-deployment \
AZURE_OPENAI_ENDPOINT=https://YOUR-RESOURCE.openai.azure.com/ \
OPENAI_API_VERSION=YOUR_API_VERSION
Prefer keyless auth: grant the managed identity the Cognitive Services OpenAI User role and pass an Entra token provider to the model client instead of an API key.
Alternatives on Azure
| Option | When to choose it |
|---|---|
| Container Apps (this guide) | Default for containerised agent APIs: serverless, KEDA autoscaling, revisions. |
| Azure AI Foundry Agent Service | Managed agent runtime with built-in threads, tools and evaluation, tightly integrated with Azure models. |
| App Service (Linux containers) | Teams already standardised on App Service; deployment slots. |
| AKS | Kubernetes platform, GPUs, complex networking. |
| Container Apps jobs | Scheduled/batch agents and migrations. |
GCP vs AWS vs Azure cheat-sheet
| Concern | GCP | AWS | Azure |
|---|---|---|---|
| Serverless containers | Cloud Run | ECS Fargate (+ ALB) | Container Apps |
| Registry | Artifact Registry | ECR | ACR |
| Postgres | Cloud SQL / AlloyDB | RDS / Aurora | PostgreSQL Flexible Server |
| Secrets | Secret Manager | Secrets Manager | Key Vault |
| Workload identity | Service account | IAM task role | Managed identity |
| Managed models | Vertex AI | Bedrock | Azure OpenAI / AI Foundry |
| Managed agent runtime | Vertex AI Agent Engine | Bedrock AgentCore | AI Foundry Agent Service |
| Vector search | Vertex AI Vector Search | OpenSearch / S3 Vectors | Azure AI Search |
| Logs / traces | Cloud Logging / Trace | CloudWatch / X-Ray | Log Analytics / App Insights |
| CI auth from GitHub | Workload Identity Federation | IAM OIDC provider | Entra federated credential |
Clean up
az group delete -n $RG --yes --no-wait # deletes everything in the resource group
az keyvault purge -n $KV # Key Vault is soft-deleted; purge to reuse the name