Azure Container Apps ยท Production Guide

Container Apps in VNet
Exposed to the Internet

A complete step-by-step guide to deploy Azure Container Apps inside a Virtual Network with Application Gateway for secure, production-grade internet exposure.

// Architecture Overview
๐ŸŒ
InternetPublic Traffic
HTTPS 443
๐Ÿ›ก๏ธ
App Gateway+ WAF v2
Azure VNet
Private
๐Ÿ“ฆ
Container AppInternal Ingress
Pull
๐Ÿ—„๏ธ
ACRPrivate Endpoint

Step-by-Step Guide

0 / 9 complete
01
Create Resource Group & Plan the Network
az group create ยท VNet address spaces
Required
โ–ผ

Start by creating a resource group and designing your VNet with dedicated subnets. Proper subnet sizing is critical โ€” Container Apps requires a /23 minimum for the environment subnet.

  • 1
    Recommended address space: Use 10.0.0.0/16 for the VNet, giving you room to grow.
  • 2
    Subnet plan: aca-subnet (10.0.0.0/23) for Container Apps, agw-subnet (10.0.2.0/24) for App Gateway.
  • 3
    The ACA subnet must be delegated to Microsoft.App/environments and cannot be shared with other resources.
Azure CLI (Bash)
# Variables
RG="rg-myapp-prod"
LOCATION="eastus2"
VNET="vnet-myapp"

# Create resource group
az group create \
  --name $RG \
  --location $LOCATION

# Create VNet + ACA subnet (min /23 required)
az network vnet create \
  --resource-group $RG \
  --name $VNET \
  --address-prefix 10.0.0.0/16 \
  --subnet-name "aca-subnet" \
  --subnet-prefix 10.0.0.0/23

# Create App Gateway subnet
az network vnet subnet create \
  --resource-group $RG \
  --vnet-name $VNET \
  --name "agw-subnet" \
  --address-prefix 10.0.2.0/24
โš ๏ธ
The aca-subnet must be empty when you create the Container App Environment. Delegating after the fact is not supported. Plan this before deploying anything into the subnet.
02
Create Azure Container Registry (ACR)
Private image registry with Private Endpoint
Security
โ–ผ

Use ACR with a Private Endpoint so image pulls stay on the private network. Use the Premium SKU to enable private endpoints and geo-replication.

Azure CLI
ACR_NAME="acrMyApp$(shuf -i 1000-9999 -n 1)"

# Create ACR (Premium for private endpoint)
az acr create \
  --resource-group $RG \
  --name $ACR_NAME \
  --sku Premium \
  --public-network-enabled false

# Create private endpoint for ACR
ACR_ID=$(az acr show --name $ACR_NAME -g $RG --query id -o tsv)

az network private-endpoint create \
  --resource-group $RG \
  --name "pe-acr" \
  --vnet-name $VNET \
  --subnet "aca-subnet" \
  --private-connection-resource-id $ACR_ID \
  --group-id registry \
  --connection-name "acr-connection"

# Link private DNS zone
az network private-dns zone create -g $RG \
  -n "privatelink.azurecr.io"

az network private-dns link vnet create -g $RG \
  --zone-name "privatelink.azurecr.io" \
  --name "acr-dns-link" \
  --virtual-network $VNET \
  --registration-enabled false
๐Ÿ’ก
Push your container image to ACR before creating the Container App Environment. Use az acr build to build directly in Azure without needing Docker locally.
03
Create Managed Identity & Assign ACR Permissions
Zero-credential image pulling
Security
โ–ผ

Use a User-Assigned Managed Identity for your Container App to pull from ACR. This avoids storing registry credentials and follows least-privilege principles.

Azure CLI
# Create managed identity
az identity create \
  --resource-group $RG \
  --name "id-myapp"

IDENTITY_ID=$(az identity show -g $RG -n "id-myapp" --query id -o tsv)
IDENTITY_PRINCIPAL=$(az identity show -g $RG -n "id-myapp" --query principalId -o tsv)

# Assign AcrPull role to the identity
az role assignment create \
  --assignee $IDENTITY_PRINCIPAL \
  --role "AcrPull" \
  --scope $ACR_ID
โ„น๏ธ
Role assignments can take 1โ€“5 minutes to propagate in Azure AD. Wait before creating the Container App, or the pull will fail on first deployment.
04
Create Container App Environment (Internal)
VNet-injected environment with internal-only ingress
Required
โ–ผ

Create the Container App Environment with VNet injection and set it to internal mode. This means no public IP is assigned to the environment โ€” traffic must come through App Gateway.

Azure CLI
ACA_ENV="cae-myapp-prod"
ACA_SUBNET_ID=$(az network vnet subnet show \
  -g $RG --vnet-name $VNET --name "aca-subnet" --query id -o tsv)

# Create Log Analytics workspace for monitoring
az monitor log-analytics workspace create \
  -g $RG -n "law-myapp"

LAW_ID=$(az monitor log-analytics workspace show \
  -g $RG -n "law-myapp" --query customerId -o tsv)
LAW_KEY=$(az monitor log-analytics workspace get-shared-keys \
  -g $RG -n "law-myapp" --query primarySharedKey -o tsv)

# Create Container App Environment โ€” INTERNAL mode
az containerapp env create \
  --resource-group $RG \
  --name $ACA_ENV \
  --location $LOCATION \
  --infrastructure-subnet-resource-id $ACA_SUBNET_ID \
  --internal-only true \
  --logs-workspace-id $LAW_ID \
  --logs-workspace-key $LAW_KEY
โš ๏ธ
--internal-only true is the key flag. This prevents Azure from creating a public IP for the environment. All inbound traffic must go through App Gateway on the same VNet. Takes ~10โ€“15 min to provision.
05
Deploy the Container App
With internal ingress enabled
Required
โ–ผ

Deploy your container app with ingress set to internal. This assigns a private IP within the VNet that App Gateway will use as its backend.

Azure CLI
APP_NAME="myapp"
IMAGE="${ACR_NAME}.azurecr.io/myapp:latest"

az containerapp create \
  --resource-group $RG \
  --name $APP_NAME \
  --environment $ACA_ENV \
  --image $IMAGE \
  --target-port 8080 \
  --ingress internal \
  --registry-server "${ACR_NAME}.azurecr.io" \
  --registry-identity $IDENTITY_ID \
  --user-assigned $IDENTITY_ID \
  --cpu 0.5 \
  --memory "1.0Gi" \
  --min-replicas 1 \
  --max-replicas 10

# Get the internal FQDN โ€” use this as AGW backend
ACA_FQDN=$(az containerapp show \
  -g $RG -n $APP_NAME \
  --query "properties.configuration.ingress.fqdn" -o tsv)

echo "Internal FQDN: $ACA_FQDN"
๐Ÿ’ก
Save the internal FQDN output โ€” you'll use it as the backend pool target in Application Gateway. It looks like myapp.internal.<env-id>.eastus2.azurecontainerapps.io.
06
Create Public IP & Application Gateway with WAF
Internet-facing entry point with Web Application Firewall
RequiredSecurity
โ–ผ

Application Gateway v2 with WAF_v2 SKU is the recommended way to expose internal Container Apps. It terminates TLS, forwards requests privately, and protects against OWASP Top 10 threats.

  • A
    Create a Static Public IP (Standard SKU required for AGW v2).
  • B
    Create AGW with WAF_v2 SKU, point backend to the Container App internal FQDN.
  • C
    Upload your TLS certificate, or use App Gateway's managed certificate with a custom domain.
Azure CLI
# 1. Create static public IP
az network public-ip create \
  -g $RG -n "pip-agw-myapp" \
  --sku Standard \
  --allocation-method Static \
  --dns-name "myapp-prod"

# 2. Create Application Gateway (WAF_v2)
az network application-gateway create \
  --resource-group $RG \
  --name "agw-myapp" \
  --location $LOCATION \
  --sku WAF_v2 \
  --capacity 2 \
  --vnet-name $VNET \
  --subnet "agw-subnet" \
  --public-ip-address "pip-agw-myapp" \
  --frontend-port 443 \
  --http-settings-port 443 \
  --http-settings-protocol Https \
  --servers $ACA_FQDN \
  --priority 100

# 3. Enable WAF in Prevention mode
az network application-gateway waf-config set \
  -g $RG \
  --gateway-name "agw-myapp" \
  --enabled true \
  --firewall-mode Prevention \
  --rule-set-type OWASP \
  --rule-set-version 3.2
๐Ÿ”ด
Set WAF to Prevention mode, not Detection, in production. Also configure HTTP-to-HTTPS redirect on port 80 to ensure all traffic is encrypted.
07
Configure Private DNS for ACA Internal FQDN
Required for App Gateway to resolve the backend
Required
โ–ผ

App Gateway needs to resolve the Container App's internal FQDN. Since it's internal-only, you must create a Private DNS Zone linked to your VNet.

Azure CLI
# Get the internal IP of the Container App environment
ACA_ENV_IP=$(az containerapp env show \
  -g $RG -n $ACA_ENV \
  --query "properties.staticIp" -o tsv)

# Extract DNS suffix from FQDN
# e.g. internal.abc123xyz.eastus2.azurecontainerapps.io
DNS_SUFFIX=$(echo $ACA_FQDN | sed 's/^[^.]*\.//')

# Create private DNS zone
az network private-dns zone create \
  -g $RG -n $DNS_SUFFIX

# Link to VNet
az network private-dns link vnet create \
  -g $RG \
  --zone-name $DNS_SUFFIX \
  --name "aca-dns-link" \
  --virtual-network $VNET \
  --registration-enabled false

# Create wildcard A record โ†’ ACA environment IP
az network private-dns record-set a add-record \
  -g $RG \
  --zone-name $DNS_SUFFIX \
  --record-set-name "*" \
  --ipv4-address $ACA_ENV_IP
โ„น๏ธ
The wildcard * A record covers all container apps in the environment. New apps you deploy will automatically resolve correctly without DNS updates.
08
Configure TLS Certificate & Custom Domain
End-to-end HTTPS with managed or custom cert
Security
โ–ผ

For production, configure end-to-end TLS. App Gateway terminates the public TLS, then re-encrypts to the Container App backend using the internal certificate.

Point your domain's DNS A record to the App Gateway public IP
Upload a PFX certificate (or use Key Vault reference) to App Gateway listener
Configure HTTPS listener on port 443 with your domain SNI
Add HTTP-to-HTTPS redirect rule on port 80
Configure backend HTTP settings to use HTTPS with "Pick hostname from backend target" enabled
Add trusted root cert for backend TLS verification (if using self-signed on ACA)
๐Ÿ’ก
Use Azure Key Vault to store your TLS certificate and reference it from App Gateway. This enables automatic rotation without redeploying the gateway.
09
Lock Down NSGs, Scaling & Monitoring
Network Security Groups ยท Autoscale ยท Diagnostics
SecurityBest Practice
โ–ผ

Harden the deployment with NSGs, configure autoscaling rules, and enable diagnostic logs for observability.

NSG Rules for ACA Subnet:
DirectionPortSourcePurpose
Inbound443AGW SubnetAllow App Gateway โ†’ Container App
Inbound*AzureLoadBalancerRequired health probes
Inbound*VirtualNetworkInternal VNet traffic
Outbound443*ACR, Key Vault, Azure APIs
Deny All*InternetBlock direct internet inbound
Azure CLI โ€” Autoscale + Diagnostics
# Enable Container App HTTP scaling rule
az containerapp update \
  -g $RG -n $APP_NAME \
  --scale-rule-name "http-rule" \
  --scale-rule-type http \
  --scale-rule-http-concurrency 50

# Enable diagnostic logs on Container App
ACA_ID=$(az containerapp show -g $RG -n $APP_NAME --query id -o tsv)
LAW_FULL_ID=$(az monitor log-analytics workspace show \
  -g $RG -n "law-myapp" --query id -o tsv)

az monitor diagnostic-settings create \
  --resource $ACA_ID \
  --name "diag-aca" \
  --workspace $LAW_FULL_ID \
  --logs '[{"category":"ContainerAppConsoleLogs","enabled":true}]' \
  --metrics '[{"category":"AllMetrics","enabled":true}]'

# Enable AGW diagnostic logs
AGW_ID=$(az network application-gateway show \
  -g $RG -n "agw-myapp" --query id -o tsv)

az monitor diagnostic-settings create \
  --resource $AGW_ID \
  --name "diag-agw" \
  --workspace $LAW_FULL_ID \
  --logs '[{"category":"ApplicationGatewayAccessLog","enabled":true},{"category":"ApplicationGatewayFirewallLog","enabled":true}]'

๐Ÿš€ Pre-Production Checklist

Container App is set to internal ingress only โ€” no direct public access
App Gateway WAF is in Prevention mode with OWASP 3.2
Private DNS zone linked to VNet with wildcard A record
NSG on ACA subnet blocks direct internet inbound
ACR uses Private Endpoint โ€” public access disabled
TLS certificate valid and HTTPS enforced end-to-end
Diagnostic logs flowing to Log Analytics
Managed Identity used โ€” no stored credentials anywhere
Min replicas โ‰ฅ 1 to avoid cold starts on first request
HTTP-to-HTTPS redirect configured on port 80