This guide deploys Azure Container Apps inside a Virtual Network with no public endpoint of its own, and publishes it to the internet through Application Gateway with a Web Application Firewall. Work through the steps in order and tick each one off as you go.

Step-by-Step Guide

0 / 9 complete
01

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: a /23 environment subnet works for every Container Apps environment type (workload-profiles environments accept as small as /27).

  • 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, pe-subnet (10.0.3.0/24) for private endpoints.
  • 3
    The ACA subnet is dedicated to the environment: nothing else, not even private endpoints, may be placed in it. Workload-profiles environments (the CLI default) also need it delegated to Microsoft.App/environments.
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

# Delegate the ACA subnet (workload-profiles environments)
az network vnet subnet update \
  --resource-group $RG \
  --vnet-name $VNET \
  --name "aca-subnet" \
  --delegations Microsoft.App/environments

# 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

# Create a subnet for private endpoints (kept out of the ACA subnet)
az network vnet subnet create \
  --resource-group $RG \
  --vnet-name $VNET \
  --name "pe-subnet" \
  --address-prefix 10.0.3.0/24
⚠️
The aca-subnet must be empty when you create the Container App Environment and stay dedicated to it afterwards. Plan this before deploying anything into the VNet.
02

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 "pe-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

# Register the endpoint's A record in the zone
az network private-endpoint dns-zone-group create -g $RG \
  --endpoint-name "pe-acr" \
  --name "acr-zone-group" \
  --private-dns-zone "privatelink.azurecr.io" \
  --zone-name "acr"
💡
Push your container image to ACR before creating the Container App Environment. az acr build builds in Azure without Docker locally, but once public network access is disabled ACR Tasks need a dedicated agent pool in the VNet, so run the first build before locking the registry down.
03

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 Microsoft Entra ID. Wait before creating the Container App, or the pull will fail on first deployment.
04

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

VNet-scoped ingress inside the internal environment
Required
▼

Deploy your container app with external ingress. Because the environment is internal-only, “external” means reachable from the VNet and nothing more: there is no public IP. Ingress set to internal would accept traffic only from other apps in the same environment, and App Gateway (in its own subnet) would be refused.

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 external \
  --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 app FQDN (VNet-only) — use this as the AGW backend
ACA_FQDN=$(az containerapp show \
  -g $RG -n $APP_NAME \
  --query "properties.configuration.ingress.fqdn" -o tsv)

echo "App FQDN: $ACA_FQDN"
💡
Save the app FQDN output: it is the backend pool target in Application Gateway. It looks like myapp.<env-id>.eastus2.azurecontainerapps.io and resolves inside the VNet once step 7 is done.
06

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

Required for App Gateway to resolve the backend
Required
▼

App Gateway needs to resolve the Container App's FQDN. Because the environment is internal-only, create a Private DNS Zone named after the environment's default domain and link it 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)

# The environment's default domain, e.g. abc123xyz.eastus2.azurecontainerapps.io
DNS_SUFFIX=$(az containerapp env show \
  -g $RG -n $ACA_ENV \
  --query "properties.defaultDomain" -o tsv)

# 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

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.

💡
Use Azure Key Vault to store your TLS certificate and reference it from App Gateway. This enables automatic rotation without redeploying the gateway.
09

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