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 .NET 8"]
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
???+ example "Expected output"
{
"id": "/subscriptions/<subscription-id>/resourceGroups/rg-dotnet-guide",
"location": "koreacentral",
"name": "rg-dotnet-guide",
"properties": {
"provisioningState": "Succeeded"
}
}
- 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"
???+ example "Expected output" This command takes 2-3 minutes to complete. When successful, it returns a JSON object containing the deployment details.
```json
{
"id": "/subscriptions/<subscription-id>/resourceGroups/rg-dotnet-guide/providers/Microsoft.Resources/deployments/main",
"name": "main",
"properties": {
"provisioningState": "Succeeded",
"outputs": {
"containerAppName": { "type": "String", "value": "ca-dotnet-guide-<unique-suffix>" },
"containerAppUrl": { "type": "String", "value": "https://ca-dotnet-guide-<unique-suffix>.<env-suffix>.koreacentral.azurecontainerapps.io" }
}
}
}
```
!!! note "Unique suffix"
The `<unique-suffix>` is generated by `uniqueString(resourceGroup().id)` in Bicep to ensure globally unique resource names.
!!! note "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)
???+ example "Expected output" These commands capture the values silently. You can verify them by running:
```bash
echo "APP_NAME=$APP_NAME"
echo "ENVIRONMENT_NAME=$ENVIRONMENT_NAME"
echo "ACR_NAME=$ACR_NAME"
echo "ACR_LOGIN_SERVER=$ACR_LOGIN_SERVER"
echo "APP_URL=$APP_URL"
```
Output:
```text
APP_NAME=ca-dotnet-guide-<unique-suffix>
ENVIRONMENT_NAME=cae-dotnet-guide-<unique-suffix>
ACR_NAME=crdotnetguide<unique-suffix>
ACR_LOGIN_SERVER=crdotnetguide<unique-suffix>.azurecr.io
APP_URL=https://ca-dotnet-guide-<unique-suffix>.<env-suffix>.koreacentral.azurecontainerapps.io
```
- Build and push container image with ACR Tasks
???+ example "Expected output (az acr build)" The build output shows the multi-stage Docker build progress. The last few lines should look like this:
```text
Step 13/13 : ENTRYPOINT ["dotnet", "AzureContainerApps.dll"]
---> Running in abc123
---> def456
Successfully built def456
Successfully tagged dotnet-guide:latest
```
Update the Container App to use the new image:
az containerapp update \
--name "$APP_NAME" \
--resource-group "$RG" \
--image "$ACR_LOGIN_SERVER/dotnet-guide:latest"
???+ example "Expected output (az containerapp update)"
{
"latestRevision": "<your-app-name>--xxxxxxx",
"name": "<your-app-name>",
"provisioningState": "Succeeded"
}
- Verify deployment state and URL
az containerapp show \
--name "$APP_NAME" \
--resource-group "$RG" \
--query "{state:properties.provisioningState,url:properties.configuration.ingress.fqdn}"
???+ example "Expected output"
{
"state": "Succeeded",
"url": "ca-dotnet-guide-<unique-suffix>.<env-suffix>.koreacentral.azurecontainerapps.io"
}
Verify the /health endpoint with curl:
???+ example "Expected output (health check)"
```json
{"status":"healthy","timestamp":"2026-04-04T16:13:19.2964050Z"}
```
- Deploy an update (creates a new revision)
az acr build --registry "$ACR_NAME" --image "dotnet-guide:v2" ./apps/dotnet-aspnetcore
az containerapp update \
--name "$APP_NAME" \
--resource-group "$RG" \
--image "$ACR_LOGIN_SERVER/dotnet-guide:v2"
???+ example "Expected output"
```json
{
"name": "<your-app-name>",
"provisioningState": "Succeeded",
"latestRevisionName": "<your-app-name>--0000002"
}
```
Confirm revision status — you should now see two revisions. 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,healthState:properties.healthState,runningState:properties.runningState}"
???+ example "Expected output (revision list)"
[
{
"name": "<your-app-name>--0000001",
"active": false,
"trafficWeight": 0,
"healthState": "Healthy",
"runningState": "Running"
},
{
"name": "<your-app-name>--0000002",
"active": true,
"trafficWeight": 100,
"healthState": "Healthy",
"runningState": "Running"
}
]
What to validate¶
- Image exists in ACR:
dotnet-guide:latestanddotnet-guide:v2 - App endpoint responds with HTTP 200 for
/healthwith JSON payload - A new revision appears after
az containerapp update - Kestrel is listening on the expected port (default 8000)
Advanced Topics¶
- Build optimization: Use Docker layer caching in ACR Tasks to speed up subsequent builds.
- Revision names: Customize revision names for better traceability using
--revision-suffix. - Private connectivity: Use internal ingress for APIs that don't need public exposure.
CLI Alternative (No Bicep)¶
Use these commands to deploy without Bicep templates. This creates the same resources imperatively.
Step 1: Set variables¶
RG="rg-dotnet-containerapp"
LOCATION="koreacentral"
APP_NAME="ca-dotnet-demo"
BASE_NAME="dotnet-app"
ENVIRONMENT_NAME="cae-dotnet-demo"
ACR_NAME="crdotnetdemo"
LOG_NAME="log-dotnet-demo"
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
Expected output
Step 4: Create Azure Container Registry¶
Expected output
Step 5: Create Container Apps environment¶
LOG_ID=$(az monitor log-analytics workspace show --resource-group $RG --workspace-name $LOG_NAME --query customerId --output tsv)
LOG_KEY=$(az monitor log-analytics workspace get-shared-keys --resource-group $RG --workspace-name $LOG_NAME --query primarySharedKey --output tsv)
az containerapp env create --resource-group $RG --name $ENVIRONMENT_NAME --location $LOCATION --logs-workspace-id $LOG_ID --logs-workspace-key $LOG_KEY
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¶
FQDN=$(az containerapp show --resource-group $RG --name $APP_NAME --query "properties.configuration.ingress.fqdn" --output tsv)
curl https://$FQDN/health