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 Python 3.11"]

    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 TD
    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-myapp"
BASE_NAME="myapp"
LOCATION="koreacentral"
DEPLOYMENT_NAME="main"
  1. Create a resource group
az group create --name "$RG" --location "$LOCATION"
Command Why it is used
az group create ... Creates the isolated resource group used by the example.

???+ example "Expected output" json { "id": "/subscriptions/<subscription-id>/resourceGroups/rg-myapp", "location": "koreacentral", "name": "rg-myapp", "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"
Command Why it is used
az deployment group create ... Deploys the Bicep or ARM template into the target resource group.

???+ 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-myapp/providers/Microsoft.Resources/deployments/main",
      "name": "main",
      "properties": {
        "provisioningState": "Succeeded",
        "outputs": {
          "containerAppName": { "type": "String", "value": "ca-myapp-<unique-suffix>" },
          "containerAppUrl": { "type": "String", "value": "https://ca-myapp-<unique-suffix>.<hash>.<region>.azurecontainerapps.io" }
       }
     }
    }
    ```

!!! 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)

ACA_ENV_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)
| Command | Purpose |
|---|---|
| `APP_NAME=$(az deployment group show --name "$DEPLOYMENT_NAME" --resource-group "$RG" --query "properties.outputs.containerAppName.value" --output tsv)` | Captures the generated Container App name from the deployment outputs so later tutorial steps target the exact Python app that was provisioned. |
| `ACA_ENV_NAME=$(az deployment group show --name "$DEPLOYMENT_NAME" --resource-group "$RG" --query "properties.outputs.containerAppEnvName.value" --output tsv)` | Captures the generated Container Apps environment name so environment-scoped steps stay aligned with the deployment. |
| `ACR_NAME=$(az deployment group show --name "$DEPLOYMENT_NAME" --resource-group "$RG" --query "properties.outputs.containerRegistryName.value" --output tsv)` | Captures the registry name used by the Python image build and push steps. |
| `ACR_LOGIN_SERVER=$(az deployment group show --name "$DEPLOYMENT_NAME" --resource-group "$RG" --query "properties.outputs.containerRegistryLoginServer.value" --output tsv)` | Captures the registry login server used to assemble the final image reference. |
| `APP_URL=$(az deployment group show --name "$DEPLOYMENT_NAME" --resource-group "$RG" --query "properties.outputs.containerAppUrl.value" --output tsv)` | Captures the app URL emitted by the deployment so it can be compared with later ingress verification. |

???+ example "Expected output" These commands capture the values silently. You can verify them by running:

   ```bash
   echo "APP_NAME=$APP_NAME"
   echo "ACA_ENV_NAME=$ACA_ENV_NAME"
   echo "ACR_NAME=$ACR_NAME"
   echo "ACR_LOGIN_SERVER=$ACR_LOGIN_SERVER"
   echo "APP_URL=$APP_URL"
   ```

   Output:
    ```text
    APP_NAME=ca-myapp-<unique-suffix>
    ACA_ENV_NAME=cae-myapp-<unique-suffix>
    ACR_NAME=<acr-name>
    ACR_LOGIN_SERVER=<acr-name>.azurecr.io
    APP_URL=https://ca-myapp-<unique-suffix>.<hash>.<region>.azurecontainerapps.io
    ```
  1. Build and push container image with ACR Tasks
az acr build \
   --registry "$ACR_NAME" \
   --image "$BASE_NAME:v1" \
   ./apps/python
Command Why it is used
az acr build ... Builds and pushes the container image to Azure Container Registry.

???+ example "Expected output (az acr build)" The build output shows the Docker build progress. The last few lines should look like this:

   ```text
   Step 7/7 : CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--chdir", "src", "app:app"]
    ---> Running in abc123
    ---> def456
   Successfully built def456
   Successfully tagged myapp:v1
   ```
az containerapp update \
   --name "$APP_NAME" \
   --resource-group "$RG" \
   --image "$ACR_LOGIN_SERVER/$BASE_NAME:v1"
Command Why it is used
az containerapp update ... Updates the existing Container App configuration without recreating the app.

???+ example "Expected output (az containerapp update)"

{
   "latestRevision": "<your-app-name>--xxxxxxx",
   "name": "ca-myapp-<unique-suffix>",
  "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}"
Command Why it is used
az containerapp show ... Reads the Container App configuration so the documented setting can be verified.

???+ example "Expected output"

{
  "state": "Succeeded",
  "url": "ca-myapp-<unique-suffix>.<hash>.<region>.azurecontainerapps.io"
}

Verify the /health endpoint with curl:

curl "$APP_URL/health"

???+ example "Expected output (health check)"
    ```json
    {"status":"healthy","timestamp":"2024-01-15T10:30:00.000000+00:00"}
    ```

Verify ingress configuration details:
```bash
az containerapp ingress show \
  --name "$APP_NAME" \
  --resource-group "$RG"
```

| Command | Why it is used |
|---|---|
| `az containerapp ingress show ...` | Reads ingress configuration such as exposure, target port, transport, and affinity. |

???+ example "Expected output (ingress configuration)"
    ```json
    {
      "allowInsecure": false,
      "external": true,
      "fqdn": "<your-app-name>.<hash>.<region>.azurecontainerapps.io",
      "targetPort": 8000,
      "transport": "Auto",
      "traffic": [
        { "latestRevision": true, "weight": 100 }
      ]
    }
    ```
  1. Deploy an update (creates a new revision)
az acr build --registry "$ACR_NAME" --image "$BASE_NAME:v2" ./apps/python

az containerapp update \
   --name "$APP_NAME" \
   --resource-group "$RG" \
   --image "$ACR_LOGIN_SERVER/$BASE_NAME:v2"
Command Why it is used
az acr build --registry ... Builds and pushes the container image to Azure Container Registry.
???+ example "Expected output"
    ```json
    {
      "name": "<your-app-name>",
      "provisioningState": "Succeeded",
      "latestRevisionName": "<your-app-name>--xxxxxxx"
    }
    ```

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}"
Command Why it is used
az containerapp revision list ... Lists revisions so rollout state, traffic, and health can be verified.

???+ example "Expected output (revision list)"

[
  {
    "name": "<your-app-name>--0000001",
    "active": false,
    "trafficWeight": 0,
    "replicas": 0,
    "healthState": "Healthy",
    "runningState": "Running"
  },
  {
    "name": "<your-app-name>--0000002",
    "active": true,
    "trafficWeight": 100,
    "replicas": 1,
    "healthState": "Healthy",
    "runningState": "Running"
  }
]

What to validate

  • Image exists in ACR: $BASE_NAME:v1 and $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-flask-containerapp"
APP_NAME="ca-flask-demo"
BASE_NAME="flask-app"
ACA_ENV_NAME="cae-flask-demo"
ACR_NAME="crflaskdemo"
LOG_NAME="log-flask-demo"
LOCATION="koreacentral"
Expected output
Variables set for RG=rg-flask-containerapp, APP_NAME=ca-flask-demo, and LOCATION=koreacentral.

Step 2: Create resource group

az group create --name "$RG" --location "$LOCATION"
Command Why it is used
az group create ... Creates the isolated resource group used by the example.
Expected output
{
  "id": "/subscriptions/<subscription-id>/resourceGroups/rg-flask-containerapp",
  "location": "koreacentral",
  "name": "rg-flask-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"

LOG_ID=$(az monitor log-analytics workspace show --resource-group "$RG" --workspace-name "$LOG_NAME" --query customerId --output tsv)
Command Why it is used
az monitor log-analytics ... Creates or inspects Azure Monitor alerts, diagnostic settings, or metrics.
Expected output
{
  "id": "/subscriptions/<subscription-id>/resourceGroups/rg-flask-containerapp/providers/Microsoft.OperationalInsights/workspaces/log-flask-demo",
  "location": "koreacentral",
  "name": "log-flask-demo",
  "properties": {
    "customerId": "11111111-2222-3333-4444-555555555555",
    "provisioningState": "Succeeded"
  }
}

Step 4: Create Azure Container Registry

az acr create --resource-group "$RG" --name "$ACR_NAME" --sku Basic
Command Why it is used
az acr create --resource-group ... Creates Azure Container Registry for container image storage.
Expected output
{
  "id": "/subscriptions/<subscription-id>/resourceGroups/rg-flask-containerapp/providers/Microsoft.ContainerRegistry/registries/crflaskdemo",
  "location": "koreacentral",
  "loginServer": "crflaskdemo.azurecr.io",
  "name": "crflaskdemo",
  "provisioningState": "Succeeded",
  "sku": {
    "name": "Basic"
  }
}

Step 5: Create Container Apps environment

az containerapp env create --resource-group "$RG" --name "$ACA_ENV_NAME" --location "$LOCATION" --logs-workspace-id "$LOG_ID"
Command Why it is used
az containerapp env create ... Creates the managed Container Apps environment that hosts the app replicas.
Expected output
{
  "id": "/subscriptions/<subscription-id>/resourceGroups/rg-flask-containerapp/providers/Microsoft.App/managedEnvironments/cae-flask-demo",
  "location": "koreacentral",
  "name": "cae-flask-demo",
  "properties": {
    "provisioningState": "Succeeded"
  }
}

Step 6: Build and push image with ACR Tasks

az acr build --registry "$ACR_NAME" --image "$BASE_NAME:v1" ./apps/python
Command Why it is used
az acr build --registry ... Builds and pushes the container image to Azure Container Registry.
Expected output
Queued a build with ID: acb_default_1700000000000
Successfully built image: flask-app:v1
Successfully pushed image: crflaskdemo.azurecr.io/flask-app:v1

Step 7: Create Container App

az containerapp create --resource-group "$RG" --name "$APP_NAME" --environment "$ACA_ENV_NAME" --image "$ACR_NAME.azurecr.io/$BASE_NAME:v1" --target-port 8000 --ingress external --query "properties.configuration.ingress.fqdn"
Command Why it is used
az containerapp create --resource-group ... Creates the Container App with the documented image, ingress, scale, and environment settings.
Expected output
"<container-app-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"
Command Why it is used
az containerapp show --resource-group ... Reads the Container App configuration so the documented setting can be verified.
Expected output
{
  "state": "Succeeded",
  "fqdn": "<container-app-fqdn>",
  "image": "crflaskdemo.azurecr.io/flask-app:v1"
}
Expected output (health check)
{"status":"healthy","timestamp":"2026-04-09T10:30:00.000000+00:00"}

Step 9: Verify deployment in Azure Portal

Container App | Overview | Stop Refresh Delete | Essentials | Resource group Status Running Application Url | Container Apps Environment Environment type Workload profiles Log Analytics | Get started Properties Monitoring | left nav Application Networking Settings Monitoring Containers Scale Volumes Ingress Logs

[Observed] ca-sample-d38538. Container App. Overview. Stop. Refresh. Delete. Essentials. Resource group. Status. Running. Location. Korea Central. Subscription. Visual Studio Enterprise Subscription. Subscription ID. Aspire Dashboard. Not yet active. Tags. Add tags. Application Url. Container Apps Environme.... Environment type. Workload profiles. Log Analytics. Development stack. Generic. View Cost. JSON View. Get started. Properties. Monitoring. Application. Revisions and replicas. Containers. Scale. Volumes. Networking. Ingress. Custom domains. CORS. Security. Settings. Monitoring. Log stream. Logs. Console. Alerts. Metrics.

[Inferred] The Status field value of Running appears consistent with the state:properties.provisioningState of Succeeded returned by the CLI verification in Step 8: Verify deployment. The Application Url field appears to surface the same properties.configuration.ingress.fqdn value queried in Step 7: Create Container App. The Container Apps Environme... link appears to map to the $ACA_ENV_NAME value supplied to --environment in Step 7: Create Container App. The left-navigation entries Containers, Scale, Ingress, and Logs are consistent with the artifacts created across Step 5: Create Container Apps environment and Step 7: Create Container App.

[Not Proven] Additional deployment detail and runtime health detail are not visible on this blade.

See Also

Sources