Standalone External (App Gateway in front of internal Container Apps)
Use this scenario when you want the standalone topology and wants to expose a Container App externally via Application Gateway.
Sample parameter file
bicep/infra/sample.standalone-external.bicepparam
What this walkthrough validates
| Capability | Notes |
|---|---|
| Internal Container Apps Environment | Deploys an internal CAE with publicNetworkAccess: Disabled. |
| External entrypoint | Deploys App Gateway v2 with a Public IP to provide inbound access. |
| VNet-reachable backend | Ensures the backend Container App is reachable from inside the VNet so App Gateway probes + forwarding work. |
| HTTPS for labs | Generates a self-signed PFX at deployment time (no Key Vault required). |
If you already have a real certificate, use the Key Vault path (production) instead of auto-generating a self-signed cert.
Key settings (from the sample)
| Setting | Typical value | Notes |
|---|---|---|
deployToggles.applicationGateway |
true |
Deploys Application Gateway. |
deployToggles.applicationGatewayPublicIp |
true |
Creates a public frontend IP. |
appGatewayDefinition.enableHttps |
true |
HTTPS listener (443) + HTTP redirect (80 → 443). |
appGatewayDefinition.createSelfSignedCertificate |
true |
Lab-friendly certificate path using deployment scripts. |
containerAppsList[*].ingressExternal |
true for the backend app |
Required for VNet-reachable ingress (even though the CAE is internal). |
Prerequisites
You need permissions to create resources in the target subscription, and you must be signed in with Azure CLI. This walkthrough uses Azure Developer CLI (azd), so run the commands from the repository root.
Deployment
Create a local working directory and run the commands from there.
mkdir deploy
cd deploy
Initialize the environment.
azd init -e ailz-standalone-external-RANDOM_SUFFIX
Set environment variables.
$env:AZURE_LOCATION = "eastus2"
$env:AZURE_RESOURCE_GROUP = "rg-ailz-standalone-external-RANDOM_SUFFIX"
$env:AZURE_SUBSCRIPTION_ID = "00000000-1111-2222-3333-444444444444"
# Convenience variable used by the commands below
$rg = $env:AZURE_RESOURCE_GROUP
Copy the sample into the active parameter file used by azd.
Copy-Item bicep/infra/sample.standalone-external.bicepparam bicep/infra/main.bicepparam -Force
Provision.
azd provision
Validation
Confirm the backend Container App ingress is VNet-reachable.
az containerapp show --resource-group $rg --name ca-aca-helloworld --query "properties.configuration.ingress" -o jsonc
Expected:
- external should be true (VNet-reachable)
- targetPort should be 80 (matches the hello-world container)
Optional (recommended): validate in-VNet connectivity from the Jump VM.
$vm = (az vm list --resource-group $rg --query "[?contains(name, '-jmp')].name | [0]" -o tsv)
$fqdn = (az containerapp show --resource-group $rg --name ca-aca-helloworld --query "properties.configuration.ingress.fqdn" -o tsv)
az vm run-command invoke -g $rg -n $vm --command-id RunPowerShellScript \
--scripts "Resolve-DnsName -Name '$fqdn' | Out-String" \
"try { (Invoke-WebRequest -UseBasicParsing -TimeoutSec 20 -Uri 'http://$fqdn/').StatusCode } catch { if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { $_.Exception.Message } }" \
--query "value[0].message" -o tsv
Expected:
- DNS output includes a privatelink.* CNAME and a private IP4Address
- Final line prints 200
Check Application Gateway backend health.
$agwName = (az network application-gateway list --resource-group $rg --query "[0].name" -o tsv)
az network application-gateway show-backend-health --resource-group $rg --name $agwName -o jsonc
Expected: backend members show Healthy.
Get the App Gateway public DNS name (cloudapp FQDN).
$pipId = (az network application-gateway show --resource-group $rg --name $agwName --query "frontendIPConfigurations[?publicIPAddress.id!=null][0].publicIPAddress.id" -o tsv)
$pipName = $pipId.Split('/')[-1]
$appGwFqdn = (az network public-ip show --resource-group $rg --name $pipName --query "dnsSettings.fqdn" -o tsv)
$appGwFqdn
Test HTTPS end-to-end.
curl.exe -k "https://$appGwFqdn/"
Expected: 200 OK and the hello-world response body.
Optional: use your own custom domain + trusted certificate
This walkthrough validates the scenario using the Azure Public IP cloudapp.azure.com hostname and a self-signed certificate.
If you want to test (or later move to) a real custom domain like app.example.com, you only need to change the frontend (DNS + HTTPS listener/cert).
1) Create a DNS record for your domain
Point your DNS name to the App Gateway public frontend using one of these:
- CNAME:
app.example.com→<appgw-public-fqdn>.<region>.cloudapp.azure.com - A record:
app.example.com→<App Gateway Public IP address>
You can retrieve the Public IP resource and its FQDN/IP using the validation commands above.
2) Set the HTTPS hostname (SNI)
In your .bicepparam, set:
appGatewayDefinition.httpsHostName = 'app.example.com'
3) Use a certificate that matches your domain
For a real custom domain, use a publicly trusted certificate (CN/SAN includes app.example.com). Supported options:
- Recommended (production): Key Vault
- Set
appGatewayDefinition.httpsKeyVaultSecretIdto the Key Vault secret ID (PFX).- Set
appGatewayDefinition.createSelfSignedCertificate = false.
- Direct PFX upload (no Key Vault)
- Set
appGatewayDefinition.sslCertificatePfxBase64andappGatewayDefinition.sslCertificatePassword.- Set
appGatewayDefinition.createSelfSignedCertificate = false.
- Self-signed (lab only)
- Keep
appGatewayDefinition.createSelfSignedCertificate = true.- Browsers will show warnings unless the cert is trusted on the client.
After changing DNS / hostname / certificate settings, re-run azd provision to apply.
Troubleshooting notes
- If backend health shows
Unhealthywith404, the Container App ingress is usually not VNet-reachable. - If you changed subnet address spaces, ensure
firewallPrivateIpmatches the actual Azure Firewall private IP. - If
userDefinedRoutes=true(forced tunneling), keepappGatewayDefinition.appGatewayInternetRoutingException=true(AppGW v2 requirement).