Data API Builder — Azure Deployment
This skill covers deploying Data API Builder (DAB) to Azure Container Apps with Azure SQL, using Azure Developer CLI (azd) for orchestration.
For local development, see the aspire-data-api-builder or docker-data-api-builder skill.
Core Mental Model
- Always build a custom Docker image with
dab-config.jsonbaked in — never use volume mounts in Azure. - azd orchestrates the full deployment: Bicep provisions resources, a post-provision hook deploys content.
- ACR hosts all custom images (DAB, web, Inspector).
- Connection strings use secrets — never plain-text env vars for credentials.
TrustServerCertificate=trueis required in all Azure SQL connection strings.
Anti-Patterns (NEVER DO)
- ❌ Azure Files share mount for
dab-config.json - ❌ Azure Storage Account for config files
- ❌ Volume mounts in Azure Container Apps for DAB config
- ❌ Any external file storage for DAB config in cloud
- ✅ Custom Docker image with config embedded via
COPY
Why custom image? Config is versioned with the image — immutable, reproducible, no extra infrastructure, faster startup, fewer failure modes.
Architecture
azd up
├── Bicep provisions:
│ Azure SQL Server + Database
│ Azure Container Registry (ACR)
│ Container Apps Environment
│ DAB Container App (port 5000)
│ SQL Commander Container App (port 8080)
│ Web Container App (port 80)
│ MCP Inspector Container App (port 80)
│
└── post-provision.ps1:
1. Add client IP to SQL firewall
2. Build + deploy dacpac schema
3. Build custom DAB image (with CORS) → push to ACR
4. Update DAB container app
5. Build web image → push to ACR
6. Update web container app
7. Build Inspector image → push to ACR
8. Update Inspector container app
File Structure
project/
├── azure.yaml # azd entry point
├── azure/
│ ├── main.bicep # Subscription-scoped entry
│ ├── resources.bicep # All Azure resources
│ └── post-provision.ps1 # Content deployment hook
├── api/
│ ├── dab-config.json # DAB configuration
│ └── Dockerfile # Custom DAB image
└── database/
├── database.sqlproj
├── Tables/*.sql
└── Scripts/PostDeployment.sql
Templates
Dockerfile (DAB Custom Image)
FROM mcr.microsoft.com/azure-databases/data-api-builder:1.7.83-rc
COPY dab-config.json /App/dab-config.json
Two lines. Config baked in. No volume mounts.
azure.yaml
name: my-project
metadata:
template: my-project
infra:
provider: bicep
path: azure
module: main
hooks:
postprovision:
shell: pwsh
continueOnError: false
interactive: true
run: ./azure/post-provision.ps1
main.bicep (Subscription Scope)
targetScope = 'subscription'
@minLength(1)
@maxLength(64)
param environmentName string
param location string
@secure()
param sqlAdminPassword string
param sqlAdminUser string = 'sqladmin'
var tags = { 'azd-env-name': environmentName }
param resourceToken string = ''
var effectiveToken = empty(resourceToken)
? uniqueString(subscription().id, environmentName, location)
: resourceToken
resource rg 'Microsoft.Resources/resourceGroups@2024-03-01' = {
name: 'rg-${environmentName}-${effectiveToken}'
location: location
tags: tags
}
module resources 'resources.bicep' = {
scope: rg
name: 'resources'
params: {
location: location
tags: tags
resourceToken: effectiveToken
sqlAdminUser: sqlAdminUser
sqlAdminPassword: sqlAdminPassword
}
}
// Outputs become env vars for post-provision hook
output AZURE_RESOURCE_GROUP string = rg.name
output AZURE_SQL_SERVER_NAME string = resources.outputs.sqlServerName
output AZURE_SQL_SERVER_FQDN string = resources.outputs.sqlServerFqdn
output AZURE_SQL_DATABASE string = 'sql-db'
output AZURE_SQL_ADMIN_USER string = sqlAdminUser
output AZURE_ACR_NAME string = resources.outputs.acrName
output AZURE_CONTAINER_APP_API_NAME string = resources.outputs.dabAppName
output AZURE_CONTAINER_APP_API_FQDN string = resources.outputs.dabFqdn
resources.bicep — DAB Container App
The DAB container app definition within resources.bicep:
param location string
param tags object
param resourceToken string
param sqlAdminUser string
@secure()
param sqlAdminPassword string
// ── SQL Server + Database ──
resource sqlServer 'Microsoft.Sql/servers@2023-08-01-preview' = {
name: 'sql-server-${resourceToken}'
location: location
tags: tags
properties: {
administratorLogin: sqlAdminUser
administratorLoginPassword: sqlAdminPassword
}
}
resource sqlFirewallAzure 'Microsoft.Sql/servers/firewallRules@2023-08-01-preview' = {
parent: sqlServer
name: 'AllowAzureServices'
properties: {
startIpAddress: '0.0.0.0'
endIpAddress: '0.0.0.0'
}
}
resource sqlDb 'Microsoft.Sql/servers/databases@2023-08-01-preview' = {
parent: sqlServer
name: 'sql-db'
location: location
tags: tags
sku: { name: 'Basic', tier: 'Basic' }
}
// ── Container Registry ──
resource acr 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' = {
name: 'acr${resourceToken}'
location: location
tags: tags
sku: { name: 'Basic' }
properties: { adminUserEnabled: true }
}
// ── Container Apps Environment ──
resource cae 'Microsoft.App/managedEnvironments@2024-03-01' = {
name: 'environment-${resourceToken}'
location: location
tags: tags
properties: {}
}
// ── DAB Container App ──
var sqlConnString = 'Server=tcp:${sqlServer.properties.fullyQualifiedDomainName},1433;Database=sql-db;User Id=${sqlAdminUser};Password=${sqlAdminPassword};Encrypt=true;TrustServerCertificate=true'
resource dabApp 'Microsoft.App/containerApps@2024-03-01' = {
name: 'data-api-${resourceToken}'
location: location
tags: tags
properties: {
managedEnvironmentId: cae.id
configuration: {
ingress: {
external: true
targetPort: 5000
}
registries: [
{
server: acr.properties.loginServer
username: acr.listCredentials().username
passwordSecretRef: 'acr-password'
}
]
secrets: [
{ name: 'db-conn', value: sqlConnString }
{ name: 'acr-password', value: acr.listCredentials().passwords[0].value }
]
}
template: {
containers: [
{
name: 'dab-api'
image: 'mcr.microsoft.com/azure-databases/data-api-builder:1.7.83-rc'
resources: { cpu: json('0.5'), memory: '1Gi' }
env: [
{ name: 'MSSQL_CONNECTION_STRING', secretRef: 'db-conn' }
]
}
]
scale: { minReplicas: 1, maxReplicas: 1 }
}
}
}
// ── Outputs ──
output sqlServerName string = sqlServer.name
output sqlServerFqdn string = sqlServer.properties.fullyQualifiedDomainName
output acrName string = acr.name
output dabAppName string = dabApp.name
output dabFqdn string = dabApp.properties.configuration.ingress.fqdn
Note: Bicep provisions with a placeholder image. The post-provision hook replaces it with the custom ACR image.
Connection Strings
Azure SQL (SQL Auth)
Server=tcp:<server>.database.windows.net,1433;Database=sql-db;User Id=sqladmin;Password=<password>;Encrypt=true;TrustServerCertificate=true
Azure SQL (Managed Identity)
Server=tcp:<server>.database.windows.net,1433;Database=sql-db;Authentication=Active Directory Default;Encrypt=true;TrustServerCertificate=true
CRITICAL:
TrustServerCertificate=trueis required — DAB and SQL Commander will not connect without it.
CORS Configuration
DAB's dab-config.json needs the web app's Azure origin in its CORS config. Use a placeholder that gets replaced during image build:
dab-config.json (with placeholder)
"cors": {
"origins": ["http://localhost:5173", "__WEB_URL_AZURE__"],
"allow-credentials": false
}
Replacement in post-provision.ps1
$webOrigin = "https://$webFqdn"
$dabConfig = Get-Content -Path "api/dab-config.json" -Raw
$dabConfig = $dabConfig.Replace("__WEB_URL_AZURE__", $webOrigin)
$dabConfig | Out-File -FilePath "api/dab-config.json" -Encoding utf8 -Force
This replacement happens before
az acr build, so the correct origin is baked into the image.
Post-Provision Hook Pattern
The post-provision.ps1 script runs after Bicep provisions all resources. It handles content that can't be done declaratively:
$ErrorActionPreference = "Stop"
# Variables from Bicep outputs (set by azd as env vars)
$resourceGroup = $env:AZURE_RESOURCE_GROUP
$sqlServerName = $env:AZURE_SQL_SERVER_NAME
$sqlServerFqdn = $env:AZURE_SQL_SERVER_FQDN
$sqlDb = $env:AZURE_SQL_DATABASE
$sqlAdminUser = $env:AZURE_SQL_ADMIN_USER
$sqlAdminPassword = $env:AZURE_SQL_ADMIN_PASSWORD
$acrName = $env:AZURE_ACR_NAME
$dabAppName = $env:AZURE_CONTAINER_APP_API_NAME
$dabFqdn = $env:AZURE_CONTAINER_APP_API_FQDN
$sqlConn = "Server=tcp:$sqlServerFqdn,1433;Database=$sqlDb;User Id=$sqlAdminUser;Password=$sqlAdminPassword;Encrypt=true;TrustServerCertificate=true"
# ── 1. Open SQL firewall for local machine ──
$myIp = (Invoke-WebRequest -Uri "https://api.ipify.org" -UseBasicParsing).Content
az sql server firewall-rule create `
--resource-group $resourceGroup `
--server $sqlServerName `
--name "azd-deploy-client" `
--start-ip-address $myIp `
--end-ip-address $myIp 2>$null | Out-Null
# ── 2. Deploy database schema (dacpac) ──
dotnet build database/database.sqlproj -c Release
sqlpackage /Action:Publish `
/SourceFile:database/bin/Release/database.dacpac `
/TargetConnectionString:"$sqlConn" `
/p:BlockOnPossibleDataLoss=false
# ── 3. Build custom DAB image with CORS → push to ACR ──
$apiDeployDir = "api-deploy-temp"
Copy-Item -Path "api" -Destination $apiDeployDir -Recurse -Force
$webOrigin = "https://$env:AZURE_WEB_APP_FQDN"
$dabConfig = Get-Content -Path "$apiDeployDir/dab-config.json" -Raw
$dabConfig = $dabConfig.Replace("__WEB_URL_AZURE__", $webOrigin)
$dabConfig | Out-File -FilePath "$apiDeployDir/dab-config.json" -Encoding utf8 -Force
az acr build --registry $acrName --image dab-api:latest --file "$apiDeployDir/Dockerfile" $apiDeployDir/
Remove-Item $apiDeployDir -Recurse -Force
# ── 4. Update DAB container app with custom image ──
az containerapp update `
--name $dabAppName `
--resource-group $resourceGroup `
--image "$acrName.azurecr.io/dab-api:latest"
Steps 5-8 handle web app, Inspector — see
azure-mcp-inspectorandazure-sql-commanderskills.
Deployment Commands
Full deployment (recommended)
azd up
This runs Bicep provisioning + post-provision hook in one command.
Re-deploy DAB only (after config change)
# Replace CORS placeholder
$dabConfig = Get-Content -Path "api/dab-config.json" -Raw
$dabConfig = $dabConfig.Replace("__WEB_URL_AZURE__", $webOrigin)
$dabConfig | Out-File -FilePath "api-deploy-temp/dab-config.json" -Encoding utf8
# Build and push
az acr build --registry <acr-name> --image dab-api:latest --file api/Dockerfile api/
# Update container
az containerapp update --name <app-name> --resource-group <rg> --image <acr>.azurecr.io/dab-api:latest
Re-deploy schema only
dotnet build database/database.sqlproj -c Release
sqlpackage /Action:Publish `
/SourceFile:database/bin/Release/database.dacpac `
/TargetConnectionString:"Server=tcp:<fqdn>,1433;Database=sql-db;User Id=sqladmin;Password=<pw>;Encrypt=true;TrustServerCertificate=true" `
/p:BlockOnPossibleDataLoss=false
Verification
After deployment, verify DAB is healthy:
$r = Invoke-WebRequest -Uri "https://<dab-fqdn>/health" -UseBasicParsing
$r.StatusCode # Should be 200
Also check:
- REST:
https://<dab-fqdn>/api/<entity> - GraphQL:
https://<dab-fqdn>/graphql - MCP:
https://<dab-fqdn>/mcp - Swagger:
https://<dab-fqdn>/swagger
Common Issues
DAB container fails to start
- Check connection string secret is correct in Container Apps
- Verify Azure SQL firewall allows Azure services (
0.0.0.0rule) - Check
TrustServerCertificate=trueis in the connection string
Schema not found (404 on entities)
- Database schema must be deployed before the DAB image is built
- Post-provision runs dacpac first, then builds the DAB image — this order matters
CORS errors from web app
- The
__WEB_URL_AZURE__placeholder must be replaced beforeaz acr build - Verify the replacement happened: check post-provision output for "CORS origin set to..."
- After adding a new web origin, rebuild and push the DAB image
ACR build fails
- Verify ACR admin is enabled:
az acr show --name <acr> --query adminUserEnabled - Check Dockerfile path is correct relative to build context
Container app shows old image
- Azure caches images. After pushing, update the container:
az containerapp update --name <app> --resource-group <rg> --image <acr>.azurecr.io/dab-api:latest
Key Differences: Local vs Azure
| Aspect | Local (Docker/Aspire) | Azure |
|---|---|---|
| Config mount | Bind mount :ro |
Baked into image |
| Connection | Server=sql-2025 (service name) |
Server=tcp:<fqdn>,1433 |
| CORS | * or localhost |
Specific https:// origin |
| Secrets | .env file |
Container Apps secrets |
| Schema deploy | sqlpackage to localhost |
sqlpackage to Azure SQL FQDN |
| Image source | Docker Hub / MCR direct | ACR (custom image) |
Related Skills
aspire-data-api-builder— Local dev with .NET Aspiredocker-data-api-builder— Local dev with Docker Composedata-api-builder-config— DAB config file referencedata-api-builder-cli— DAB CLI commandsazure-sql-commander— Deploy SQL Commander to Azureazure-mcp-inspector— Deploy MCP Inspector to Azure