Skip to content

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

Step-by-step

  1. Set standard variables
RG="rg-dotnet-guide"
BASE_NAME="dotnet-guide"
LOCATION="koreacentral"
DEPLOYMENT_NAME="main"
  1. Create a resource group
az group create --name "$RG" --location "$LOCATION"

???+ example "Expected output"

{
  "id": "/subscriptions/<subscription-id>/resourceGroups/rg-dotnet-guide",
  "location": "koreacentral",
  "name": "rg-dotnet-guide",
  "properties": {
    "provisioningState": "Succeeded"
  }
}

  1. 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.

  1. 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
    ```
  1. Build and push container image with ACR Tasks
az acr build \
   --registry "$ACR_NAME" \
   --image "dotnet-guide:latest" \
   ./apps/dotnet-aspnetcore

???+ 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"
}

  1. 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:

curl "$APP_URL/health"

???+ example "Expected output (health check)"
    ```json
    {"status":"healthy","timestamp":"2026-04-04T16:13:19.2964050Z"}
    ```
  1. 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:latest and dotnet-guide:v2
  • App endpoint responds with HTTP 200 for /health with 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"
Expected output
Variables initialized for the .NET Container Apps deployment.

Step 2: Create resource group

az group create --name $RG --location $LOCATION
Expected output
{
  "id": "/subscriptions/<subscription-id>/resourceGroups/rg-dotnet-containerapp",
  "location": "koreacentral",
  "name": "rg-dotnet-containerapp",
  "properties": {
    "provisioningState": "Succeeded"
  }
}

Step 3: Create Log Analytics workspace

az monitor log-analytics workspace create --resource-group $RG --workspace-name $LOG_NAME --location $LOCATION
Expected output
{
  "customerId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
  "id": "/subscriptions/<subscription-id>/resourceGroups/rg-dotnet-containerapp/providers/Microsoft.OperationalInsights/workspaces/log-dotnet-demo",
  "name": "log-dotnet-demo",
  "provisioningState": "Succeeded"
}

Step 4: Create Azure Container Registry

az acr create --resource-group $RG --name $ACR_NAME --sku Basic
Expected output
{
  "id": "/subscriptions/<subscription-id>/resourceGroups/rg-dotnet-containerapp/providers/Microsoft.ContainerRegistry/registries/crdotnetdemo",
  "loginServer": "crdotnetdemo.azurecr.io",
  "name": "crdotnetdemo",
  "provisioningState": "Succeeded"
}

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
{
  "id": "/subscriptions/<subscription-id>/resourceGroups/rg-dotnet-containerapp/providers/Microsoft.App/managedEnvironments/cae-dotnet-demo",
  "name": "cae-dotnet-demo",
  "provisioningState": "Succeeded"
}

Step 6: Build and push image with ACR Tasks

az acr build --registry $ACR_NAME --image $BASE_NAME:v1 ./apps/dotnet-aspnetcore
Expected output
Packing source code into tar to upload...
Queued a build with ID: cg1
Run ID: cg1 was successful after 1m 05s

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"
Expected output
"ca-dotnet-demo.mistyfield-1a2b3c4d.koreacentral.azurecontainerapps.io"

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
Expected output
{"status":"healthy","timestamp":"2026-04-09T09:24:33.5123345Z"}

See Also

Sources