Skip to content

Azure Deployment

Deploy FraiseQL on Azure using App Service, Container Apps, Container Instances, or Kubernetes Service (AKS). FraiseQL supports both Azure Database for PostgreSQL and Azure SQL Database (SQL Server) as first-class targets.

If your organization runs SQL Server, Azure SQL Database is the native Azure target — no PostgreSQL migration required.

Terminal window
RESOURCE_GROUP=fraiseql-rg
REGION=eastus
SQL_SERVER=fraiseql-sql
# Create resource group
az group create --name $RESOURCE_GROUP --location $REGION
# Create SQL Server (logical server)
az sql server create \
--name $SQL_SERVER \
--resource-group $RESOURCE_GROUP \
--location $REGION \
--admin-user fraiseql_admin \
--admin-password "$(openssl rand -base64 20 | tr -d /+= | head -c20)Aa1!"
# Create database (Serverless for dev, General Purpose for prod)
az sql db create \
--resource-group $RESOURCE_GROUP \
--server $SQL_SERVER \
--name fraiseql_db \
--edition GeneralPurpose \
--compute-model Serverless \
--family Gen5 \
--min-capacity 1 \
--capacity 4 \
--auto-pause-delay 60 # minutes; set -1 to disable for prod

Zero-credential authentication — no password in config files or environment variables:

Terminal window
# Assign system-managed identity to Container App
az containerapp identity assign \
--name fraiseql-app \
--resource-group $RESOURCE_GROUP \
--system-assigned
# Get the managed identity's object ID
IDENTITY_OID=$(az containerapp identity show \
--name fraiseql-app \
--resource-group $RESOURCE_GROUP \
--query principalId -o tsv)
# Set Azure AD admin on the SQL server (required before creating AD users)
az sql server ad-admin create \
--resource-group $RESOURCE_GROUP \
--server $SQL_SERVER \
--display-name "fraiseql-aad-admin" \
--object-id $IDENTITY_OID

Then connect as an Azure AD admin and create the SQL login for the managed identity:

-- Run against fraiseql_db (not master)
CREATE USER [fraiseql-app] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [fraiseql-app];
ALTER ROLE db_datawriter ADD MEMBER [fraiseql-app];
fraiseql.toml
[database]
# Managed Identity — no password
url = "server=fraiseql-sql.database.windows.net;database=fraiseql_db;Authentication=ActiveDirectoryMsi"
pool_min = 2
pool_max = 20
connect_timeout_ms = 5000
ssl_mode = "require"
Terminal window
# Create Container Apps environment
az containerapp env create \
--name fraiseql-env \
--resource-group $RESOURCE_GROUP \
--location $REGION
# Deploy FraiseQL
az containerapp create \
--name fraiseql-app \
--resource-group $RESOURCE_GROUP \
--environment fraiseql-env \
--image ghcr.io/fraiseql/fraiseql:latest \
--env-vars \
"DATABASE_URL=server=fraiseql-sql.database.windows.net;database=fraiseql_db;Authentication=ActiveDirectoryMsi" \
--target-port 8080 \
--ingress external \
--min-replicas 1 \
--max-replicas 10
Terminal window
# Create Log Analytics workspace
az monitor log-analytics workspace create \
--resource-group $RESOURCE_GROUP \
--workspace-name fraiseql-logs
# Link to Container App environment
WORKSPACE_ID=$(az monitor log-analytics workspace show \
--resource-group $RESOURCE_GROUP \
--workspace-name fraiseql-logs \
--query customerId -o tsv)
# Query FraiseQL logs
az monitor log-analytics query \
--workspace $WORKSPACE_ID \
--analytics-query "ContainerAppConsoleLogs_CL | where ContainerName_s == 'fraiseql' | order by TimeGenerated desc | take 50"

Terminal window
# Set variables
RESOURCE_GROUP=fraiseql-rg
REGION=eastus
REGISTRY_NAME=fraiseqlregistry
# Create resource group
az group create \
--name $RESOURCE_GROUP \
--location $REGION
# Create container registry
az acr create \
--resource-group $RESOURCE_GROUP \
--name $REGISTRY_NAME \
--sku Standard
# Build and push Docker image
az acr build \
--registry $REGISTRY_NAME \
--image fraiseql:latest .
# Get login credentials
az acr credential show \
--name $REGISTRY_NAME
Terminal window
# Create PostgreSQL server
az postgres server create \
--resource-group $RESOURCE_GROUP \
--name fraiseql-prod \
--location $REGION \
--admin-user fraiseql \
--admin-password "$(openssl rand -base64 32)" \
--sku-name B_Gen5_1 \
--storage-size 51200 \
--backup-retention 30 \
--geo-redundant-backup Enabled
# Create database
az postgres db create \
--resource-group $RESOURCE_GROUP \
--server-name fraiseql-prod \
--name fraiseql
# Configure firewall to allow Azure services
az postgres server firewall-rule create \
--resource-group $RESOURCE_GROUP \
--server-name fraiseql-prod \
--name AllowAzureServices \
--start-ip-address 0.0.0.0 \
--end-ip-address 0.0.0.0
# Get connection string
az postgres server show-connection-string \
--server-name fraiseql-prod \
--admin-user fraiseql
Terminal window
# Create App Service Plan
az appservice plan create \
--name fraiseql-plan \
--resource-group $RESOURCE_GROUP \
--is-linux \
--sku P1V2 \
--number-of-workers 3
Terminal window
# Create web app
az webapp create \
--resource-group $RESOURCE_GROUP \
--plan fraiseql-plan \
--name fraiseql-prod \
--deployment-container-image-name \
$REGISTRY_NAME.azurecr.io/fraiseql:latest
# Configure container registry
az webapp config container set \
--name fraiseql-prod \
--resource-group $RESOURCE_GROUP \
--docker-custom-image-name $REGISTRY_NAME.azurecr.io/fraiseql:latest \
--docker-registry-server-url https://$REGISTRY_NAME.azurecr.io \
--docker-registry-server-user $(az acr credential show -n $REGISTRY_NAME --query username -o tsv) \
--docker-registry-server-password $(az acr credential show -n $REGISTRY_NAME --query 'passwords[0].value' -o tsv)
Terminal window
# Set application settings
az webapp config appsettings set \
--resource-group $RESOURCE_GROUP \
--name fraiseql-prod \
--settings \
ENVIRONMENT=production \
LOG_LEVEL=info \
LOG_FORMAT=json \
WEBSITES_ENABLE_APP_SERVICE_STORAGE=false \
DOCKER_ENABLE_CI=true \
DOCKER_REGISTRY_SERVER_URL=https://$REGISTRY_NAME.azurecr.io
# Set secrets (from Key Vault)
az keyvault secret set \
--vault-name fraiseql-kv \
--name DatabaseUrl \
--value "postgresql://fraiseql:password@fraiseql-prod.postgres.database.azure.com:5432/fraiseql"
az keyvault secret set \
--vault-name fraiseql-kv \
--name JwtSecret \
--value "$(openssl rand -base64 32)"
Terminal window
# Enable health check
az webapp config set \
--resource-group $RESOURCE_GROUP \
--name fraiseql-prod \
--generic-configurations '{"HealthCheckPath": "/health/ready"}'
Terminal window
# Create auto-scale settings
az monitor autoscale create \
--resource-group $RESOURCE_GROUP \
--resource fraiseql-prod \
--resource-type "Microsoft.Web/sites" \
--name fraiseql-autoscale \
--min-count 3 \
--max-count 10 \
--count 3
# Add scale-up rule (CPU > 70%)
az monitor autoscale rule create \
--resource-group $RESOURCE_GROUP \
--autoscale-name fraiseql-autoscale \
--condition "Percentage CPU > 70 avg 5m" \
--scale out 1
# Add scale-down rule (CPU < 30%)
az monitor autoscale rule create \
--resource-group $RESOURCE_GROUP \
--autoscale-name fraiseql-autoscale \
--condition "Percentage CPU < 30 avg 5m" \
--scale in 1
Terminal window
# Create Key Vault
az keyvault create \
--resource-group $RESOURCE_GROUP \
--name fraiseql-kv \
--location $REGION
# Create secrets
az keyvault secret set \
--vault-name fraiseql-kv \
--name database-url \
--value "postgresql://user:pass@host/db"
az keyvault secret set \
--vault-name fraiseql-kv \
--name jwt-secret \
--value "$(openssl rand -base64 32)"
az keyvault secret set \
--vault-name fraiseql-kv \
--name cors-origins \
--value "https://app.example.com"
Terminal window
# Create managed identity
az identity create \
--resource-group $RESOURCE_GROUP \
--name fraiseql-identity
# Get identity ID
IDENTITY_ID=$(az identity show \
--resource-group $RESOURCE_GROUP \
--name fraiseql-identity \
--query id -o tsv)
# Assign to App Service
az webapp identity assign \
--resource-group $RESOURCE_GROUP \
--name fraiseql-prod \
--identities $IDENTITY_ID
# Grant Key Vault access
az keyvault set-policy \
--name fraiseql-kv \
--object-id $(az identity show \
--resource-group $RESOURCE_GROUP \
--name fraiseql-identity \
--query principalId -o tsv) \
--secret-permissions get

In your FraiseQL code, use managed identity:

from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
credential = DefaultAzureCredential()
client = SecretClient(
vault_url="https://fraiseql-kv.vault.azure.net/",
credential=credential
)
database_url = client.get_secret("database-url").value
jwt_secret = client.get_secret("jwt-secret").value

Or set App Service app settings to reference Key Vault:

Terminal window
az webapp config appsettings set \
--resource-group $RESOURCE_GROUP \
--name fraiseql-prod \
--settings \
DATABASE_URL="@Microsoft.KeyVault(SecretUri=https://fraiseql-kv.vault.azure.net/secrets/database-url/)" \
JWT_SECRET="@Microsoft.KeyVault(SecretUri=https://fraiseql-kv.vault.azure.net/secrets/jwt-secret/)"

For complex workloads requiring Kubernetes:

Terminal window
# Create AKS cluster
az aks create \
--resource-group $RESOURCE_GROUP \
--name fraiseql-aks \
--node-count 3 \
--vm-set-type VirtualMachineScaleSets \
--load-balancer-sku standard \
--enable-managed-identity \
--network-plugin azure \
--enable-addons monitoring,azure-policy \
--enable-app-routing \
--docker-bridge-address 172.17.0.1/16 \
--service-principal ... \
--client-secret ...
# Get credentials
az aks get-credentials \
--resource-group $RESOURCE_GROUP \
--name fraiseql-aks \
--overwrite-existing

Follow the Kubernetes deployment guide with Azure-specific steps:

fraiseql-aks-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: fraiseql
spec:
replicas: 3
template:
spec:
serviceAccountName: fraiseql
containers:
- name: fraiseql
image: fraiseqlregistry.azurecr.io/fraiseql:latest
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 2000m
memory: 2Gi
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: fraiseql-secrets
key: database-url
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: fraiseql-secrets
key: jwt-secret
---
apiVersion: v1
kind: Service
metadata:
name: fraiseql
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8000
selector:
app: fraiseql

Deploy:

Terminal window
# Create secrets
kubectl create secret generic fraiseql-secrets \
--from-literal=database-url="postgresql://user:pass@host/db" \
--from-literal=jwt-secret="$(openssl rand -base64 32)"
# Deploy
kubectl apply -f fraiseql-aks-deployment.yaml
# Check status
kubectl get services
kubectl get pods
Terminal window
# Create Application Gateway
az network application-gateway create \
--name fraiseql-gateway \
--location $REGION \
--resource-group $RESOURCE_GROUP \
--vnet-name fraiseql-vnet \
--subnet gateway-subnet \
--capacity 2 \
--sku Standard_v2 \
--http-settings-cookie-based-affinity Disabled \
--frontend-port 443 \
--http-settings-port 8000 \
--http-settings-protocol Http

For zero-downtime deployments:

Terminal window
# Create staging slot
az webapp deployment slot create \
--resource-group $RESOURCE_GROUP \
--name fraiseql-prod \
--slot staging
# Deploy to staging
az webapp config container set \
--name fraiseql-prod \
--resource-group $RESOURCE_GROUP \
--slot staging \
--docker-custom-image-name $REGISTRY_NAME.azurecr.io/fraiseql:staging
# Swap to production (when ready)
az webapp deployment slot swap \
--resource-group $RESOURCE_GROUP \
--name fraiseql-prod \
--slot staging
azure-pipelines.yml
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: Build
jobs:
- job: BuildAndPush
steps:
- task: Docker@2
inputs:
containerRegistry: 'fraiseql-acr'
repository: 'fraiseql'
command: 'buildAndPush'
Dockerfile: 'Dockerfile'
tags: '$(Build.BuildId)'
- stage: Deploy
dependsOn: Build
jobs:
- deployment: DeployToAppService
environment: production
strategy:
runOnce:
deploy:
steps:
- task: AzureAppServiceSettings@1
inputs:
azureSubscription: 'Azure Subscription'
appName: 'fraiseql-prod'
resourceGroupName: $RESOURCE_GROUP
- task: AzureRmWebAppDeployment@4
inputs:
azureSubscription: 'Azure Subscription'
appType: 'webAppContainer'
WebAppName: 'fraiseql-prod'
Terminal window
# Create action group for alerts
az monitor action-group create \
--name fraiseql-alerts \
--resource-group $RESOURCE_GROUP
# Create metric alert (high CPU)
az monitor metrics alert create \
--name fraiseql-high-cpu \
--resource-group $RESOURCE_GROUP \
--scopes /subscriptions/{subid}/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Web/sites/fraiseql-prod \
--description "Alert when CPU > 70%" \
--condition "avg Percentage CPU > 70" \
--window-size 5m \
--evaluation-frequency 1m \
--action fraiseql-alerts
Terminal window
# Create Application Insights
az monitor app-insights component create \
--app fraiseql-insights \
--location $REGION \
--resource-group $RESOURCE_GROUP
# Link to App Service
az webapp config appsettings set \
--resource-group $RESOURCE_GROUP \
--name fraiseql-prod \
--settings APPINSIGHTS_INSTRUMENTATIONKEY=$(az monitor app-insights component show \
--app fraiseql-insights \
--resource-group $RESOURCE_GROUP \
--query instrumentationKey -o tsv)
Terminal window
# Create Log Analytics workspace
az monitor log-analytics workspace create \
--resource-group $RESOURCE_GROUP \
--workspace-name fraiseql-logs
# View logs
az monitor log-analytics query \
--workspace fraiseql-logs \
--analytics-query "ContainerLog | where time > ago(1h)"
Terminal window
# View automatic backups
az postgres server backup show \
--resource-group $RESOURCE_GROUP \
--server-name fraiseql-prod
# Restore from backup
az postgres server restore \
--resource-group $RESOURCE_GROUP \
--name fraiseql-restored \
--source-server fraiseql-prod \
--restore-point-in-time "2024-01-15T12:00:00"
# Enable geo-redundant backups (already done in creation)
# Allows restore in different region if primary region fails
Terminal window
# Create read replica in different region
az postgres server replica create \
--name fraiseql-replica \
--resource-group $RESOURCE_GROUP \
--source-server fraiseql-prod \
--location westus
# Promote replica to standalone (if primary fails)
az postgres server promote-replica \
--name fraiseql-replica \
--resource-group $RESOURCE_GROUP
Terminal window
# Purchase reserved instances (1-3 year commitment)
# Typical savings: 30-60%
az reservations reservation list

Spot Instances (for non-critical workloads)

Section titled “Spot Instances (for non-critical workloads)”
Terminal window
# Use spot instances in AKS
az aks nodepool add \
--resource-group $RESOURCE_GROUP \
--cluster-name fraiseql-aks \
--name spotnodepool \
--priority Spot \
--eviction-policy Delete \
--max-surge 33 \
--max-unavailable 33
Terminal window
# View logs
az webapp log tail \
--resource-group $RESOURCE_GROUP \
--name fraiseql-prod
# View deployment logs
az webapp deployment log show \
--resource-group $RESOURCE_GROUP \
--name fraiseql-prod
# Restart app
az webapp restart \
--resource-group $RESOURCE_GROUP \
--name fraiseql-prod
Terminal window
# Test connection
psql "postgresql://fraiseql@fraiseql-prod:password@fraiseql-prod.postgres.database.azure.com:5432/fraiseql"
# Check firewall rules
az postgres server firewall-rule list \
--resource-group $RESOURCE_GROUP \
--server-name fraiseql-prod
Terminal window
# Check image in registry
az acr repository list \
--name $REGISTRY_NAME
# View image tags
az acr repository show-tags \
--name $REGISTRY_NAME \
--repository fraiseql
  • Use Premium tier App Service Plan (for better SLA)
  • Enable geographic redundancy for databases
  • Configure Application Insights monitoring
  • Set up Log Analytics for log aggregation
  • Enable Key Vault for secrets management
  • Configure auto-scaling based on metrics
  • Set up deployment slots for zero-downtime deploys
  • Enable Azure Backup for App Service
  • Configure firewall rules appropriately
  • Set up Azure DevOps CI/CD pipeline
  • Test failover and recovery procedures

CI/CD with Azure DevOps

Set up Azure DevOps pipelines for automatic deployments to App Service. Deployment Overview

Custom Dashboards

Create monitoring dashboards in Azure Monitor for visibility into your deployment. Scaling Guide

Security Center

Review and apply Azure Security Center recommendations for FraiseQL. Deployment Overview

Troubleshooting

Diagnose App Service startup failures, database connectivity, and container issues. Troubleshooting Guide