02 - First Deploy to Azure Container Apps¶
In this step, you provision the core Azure resources, build your image in Azure Container Registry, and deploy your first revision to Azure Container Apps.
Infrastructure Context
Service: Container Apps (Consumption) | Network: VNet integrated | VNet: ✅
This tutorial assumes a production-ready Container Apps deployment with a custom VNet, ACR with managed identity pull, and private endpoints for backend services.
flowchart TD
INET[Internet] -->|HTTPS| CA["Container App\nConsumption\nLinux Node 18 LTS"]
subgraph VNET["VNet 10.0.0.0/16"]
subgraph ENV_SUB["Environment Subnet 10.0.0.0/23\nDelegation: Microsoft.App/environments"]
CAE[Container Apps Environment]
CA
end
subgraph PE_SUB["Private Endpoint Subnet 10.0.2.0/24"]
PE_ACR[PE: ACR]
PE_KV[PE: Key Vault]
PE_ST[PE: Storage]
end
end
PE_ACR --> ACR[Azure Container Registry]
PE_KV --> KV[Key Vault]
PE_ST --> ST[Storage Account]
subgraph DNS[Private DNS Zones]
DNS_ACR[privatelink.azurecr.io]
DNS_KV[privatelink.vaultcore.azure.net]
DNS_ST[privatelink.blob.core.windows.net]
end
PE_ACR -.-> DNS_ACR
PE_KV -.-> DNS_KV
PE_ST -.-> DNS_ST
CA -.->|System-Assigned MI| ENTRA[Microsoft Entra ID]
CAE --> LOG[Log Analytics]
CA --> AI[Application Insights]
style CA fill:#107c10,color:#fff
style VNET fill:#E8F5E9,stroke:#4CAF50
style DNS fill:#E3F2FD Deployment Workflow¶
graph LR
BICEP[Bicep Deploy] --> ACR[ACR Build]
ACR --> UPDATE[Container App Update]
UPDATE --> VERIFY[Verify URL] Prerequisites¶
- Completed 01 - Run Locally with Docker
- Azure CLI logged in
- Bicep template at
infra/main.bicep
Step-by-step¶
-
Set standard variables
-
Create a resource group
-
Deploy infrastructure (environment, Log Analytics, ACR, Container App)
az deployment group create \ --name "$DEPLOYMENT_NAME" \ --resource-group "$RG" \ --template-file infra/main.bicep \ --parameters baseName="$BASE_NAME" location="$LOCATION"Expected output
This command takes 2-3 minutes to complete. When successful, it returns a JSON object containing the deployment details.
{ "id": "/subscriptions/<subscription-id>/resourceGroups/rg-nodejs-guide/providers/Microsoft.Resources/deployments/main", "name": "main", "properties": { "provisioningState": "Succeeded", "outputs": { "containerAppName": { "type": "String", "value": "ca-nodejs-guide-<unique-suffix>" }, "containerAppUrl": { "type": "String", "value": "https://ca-nodejs-guide-<unique-suffix>.<hash>.<region>.azurecontainerapps.io" } } } }Initial revision health can appear unhealthy
The Bicep template creates the Container App before your custom image is built and pushed. Until you complete Step 5 and update the app image, the initial revision may show as unhealthy. This is expected.
-
Capture generated resource names from Bicep outputs
APP_NAME=$(az deployment group show \ --name "$DEPLOYMENT_NAME" \ --resource-group "$RG" \ --query "properties.outputs.containerAppName.value" \ --output tsv) ENVIRONMENT_NAME=$(az deployment group show \ --name "$DEPLOYMENT_NAME" \ --resource-group "$RG" \ --query "properties.outputs.containerAppEnvName.value" \ --output tsv) ACR_NAME=$(az deployment group show \ --name "$DEPLOYMENT_NAME" \ --resource-group "$RG" \ --query "properties.outputs.containerRegistryName.value" \ --output tsv) ACR_LOGIN_SERVER=$(az deployment group show \ --name "$DEPLOYMENT_NAME" \ --resource-group "$RG" \ --query "properties.outputs.containerRegistryLoginServer.value" \ --output tsv) APP_URL=$(az deployment group show \ --name "$DEPLOYMENT_NAME" \ --resource-group "$RG" \ --query "properties.outputs.containerAppUrl.value" \ --output tsv) -
Build and push container image with ACR Tasks
Expected output (az acr build)
The build output shows the Docker build progress. The last few lines should look like this:
-
Verify deployment state and URL
az containerapp show \ --name "$APP_NAME" \ --resource-group "$RG" \ --query "{state:properties.provisioningState,url:properties.configuration.ingress.fqdn}"Expected output
Verify the
/healthendpoint withcurl: -
Deploy an update (creates a new revision)
az acr build --registry "$ACR_NAME" --image "$BASE_NAME:v2" ./apps/nodejs az containerapp update \ --name "$APP_NAME" \ --resource-group "$RG" \ --image "$ACR_LOGIN_SERVER/$BASE_NAME:v2"Expected output
Confirm revision status — you should now see two revisions (the original v1 and the new v2). In single-revision mode, the old revision is retained but inactive:
az containerapp revision list \ --name "$APP_NAME" \ --resource-group "$RG" \ --query "[].{name:name,active:properties.active,trafficWeight:properties.trafficWeight,replicas:properties.replicas,healthState:properties.healthState,runningState:properties.runningState}"Expected output (revision list)
What to validate¶
- Image exists in ACR:
$BASE_NAME:v1and$BASE_NAME:v2 - App endpoint responds with HTTP 200 for
/health - A new revision appears after
az containerapp update
Advanced Topics¶
- Move to internal ingress for private APIs and pair with VNet integration.
- Add workload profiles and min/max replicas for predictable performance.
- Use managed identity-based ACR pull for stronger credential hygiene.
CLI Alternative (No Bicep)¶
Use these commands to deploy without Bicep templates. This creates the same resources imperatively.
Step 1: Set variables¶
RG="rg-express-containerapp"
APP_NAME="ca-express-demo"
BASE_NAME="express-app"
ENVIRONMENT_NAME="cae-express-demo"
ACR_NAME="crexpressdemo"
LOG_NAME="log-express-demo"
LOCATION="koreacentral"
Expected output
Step 2: Create resource group¶
Expected output
Step 3: Create Log Analytics workspace¶
az monitor log-analytics workspace create --resource-group "$RG" --workspace-name "$LOG_NAME" --location "$LOCATION"
LOG_ID=$(az monitor log-analytics workspace show --resource-group "$RG" --workspace-name "$LOG_NAME" --query customerId --output tsv)
Expected output
{
"id": "/subscriptions/<subscription-id>/resourceGroups/rg-express-containerapp/providers/Microsoft.OperationalInsights/workspaces/log-express-demo",
"location": "koreacentral",
"name": "log-express-demo",
"properties": {
"customerId": "11111111-2222-3333-4444-555555555555",
"provisioningState": "Succeeded"
}
}
Step 4: Create Azure Container Registry¶
Expected output
{
"id": "/subscriptions/<subscription-id>/resourceGroups/rg-express-containerapp/providers/Microsoft.ContainerRegistry/registries/crexpressdemo",
"location": "koreacentral",
"loginServer": "crexpressdemo.azurecr.io",
"name": "crexpressdemo",
"provisioningState": "Succeeded",
"sku": {
"name": "Basic"
}
}
Step 5: Create Container Apps environment¶
az containerapp env create --resource-group "$RG" --name "$ENVIRONMENT_NAME" --location "$LOCATION" --logs-workspace-id "$LOG_ID"
Expected output
Step 6: Build and push image with ACR Tasks¶
Expected output
Step 7: Create Container App¶
az containerapp create --resource-group "$RG" --name "$APP_NAME" --environment "$ENVIRONMENT_NAME" --image "$ACR_NAME.azurecr.io/$BASE_NAME:v1" --target-port 8000 --ingress external --query "properties.configuration.ingress.fqdn"
Step 8: Verify deployment¶
APP_FQDN=$(az containerapp show --resource-group "$RG" --name "$APP_NAME" --query "properties.configuration.ingress.fqdn" --output tsv)
az containerapp show --resource-group "$RG" --name "$APP_NAME" --query "{state:properties.provisioningState,fqdn:properties.configuration.ingress.fqdn,image:properties.template.containers[0].image}"
curl "https://$APP_FQDN/health"