Container Apps + ACR requires two-phase deployment (circular dependency: CA needs ACR image, ACR needs CA identity for AcrPull):
Phase 1: Deploy Container App with placeholder image (mcr.microsoft.com/azuredocs/containerapps-helloworld:latest). ⛔ No registries block, no KV secretRef. The placeholder image is pulled from MCR (public). Use registries: [] and secrets: []. RBAC role assignments (AcrPull, KV Secrets User) ARE created in Phase 1 — they don’t affect the placeholder deployment and need 1–2 minutes to propagate before Phase 2.
Phase 2: Build + push app image to ACR, redeploy with real image + registries + KV secretRef entries. RBAC is already propagated from Phase 1.
⛔ Placeholder image listens on port 80, not your app’s port. Set targetPort conditionally: var effectivePort = containerImage == 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' ? 80 : appPort. Mismatched ports cause “Operation expired” (health probe can’t reach container).
⛔ containerImage param must exist in BOTH main.bicep AND the container app module. Phase 2 passes --parameters containerImage='...' via CLI — if main.bicep lacks the param, the override is silently ignored and the placeholder persists.
⛔ Do NOT set revisionSuffix. Omit it entirely — ARM auto-generates unique revision names. Hardcoding revisionSuffix: 'v1' causes Phase 2 redeploy to fail with “revision with suffix v1 already exists.”
⛔ Output BOTH id and customerId from the log-analytics module. Container Apps Environment needs the GUID customerId. App Insights needs the ARM resource ID. Do NOT use split(workspaceId, '/')[8] — that extracts the workspace name, not the GUID.
bicep
// log-analytics.bicep outputs:output id string = logAnalyticsWorkspace.id // ARM resource IDoutput customerId string = logAnalyticsWorkspace.properties.customerId // GUIDoutput sharedKey string = logAnalyticsWorkspace.listKeys().primarySharedKey// container-app-environment.bicep:param workspaceCustomerId string // GUID, NOT resource ID// ⛔ MUST nest under appLogsConfiguration.destination='log-analytics' — a bare top-level logAnalyticsConfiguration fails deploy (ManagedEnvironmentInvalidSchema). This nesting is the ONLY valid location at EVERY API version (the flat shape was never valid — NOT version drift, so do not chase API-version pins). This is the CA's only log path (no diagnostic-settings module).properties: { appLogsConfiguration: { destination: 'log-analytics' logAnalyticsConfiguration: { customerId: workspaceCustomerId sharedKey: workspaceSharedKey } }}// ❌ WRONG: customerId: split(workspaceId, '/')[8]
⛔ Container resource limits: Use decimal format for memory: '0.5Gi', '1Gi', '2Gi' — NOT Kubernetes-style '512Mi'. CPU must be type string: '0.25', '0.5', '1'. Valid combos: 0.25/0.5Gi, 0.5/1Gi, 0.75/1.5Gi, 1/2Gi, 1.25/2.5Gi, 1.5/3Gi, 1.75/3.5Gi, 2/4Gi.
⛔ ACR module:retentionPolicy is Premium-only. For Basic/Standard ACR, omit retentionPolicy entirely — ARM rejects it.
⛔ Container Apps does NOT support @Microsoft.KeyVault(SecretUri=...) syntax. That is App Service-only. Container Apps uses secretRef with managed identity.
Correct pattern — Container Apps secrets from Key Vault:
❌ WRONG — environment().suffixes.keyvaultDns produces double-dot URL:keyVaultUrl: 'https://${kvName}${environment().suffixes.keyvaultDns}/secrets/...'
That function returns .vault.azure.net (WITH leading dot) → kv-name..vault.azure.net → ContainerAppSecretKeyVaultUrlInvalid.
✅ Use keyVault.name + .vault.azure.net (hardcoded domain) or keyVaultModule.outputs.vaultUri.
⛔ Every secrets[].keyVaultUrl in a Container App MUST have a matching Microsoft.KeyVault/vaults/secrets child resource in the KV module. If the CA references sshpass via secretRef, the KV module must create that secret. Missing secrets → SecretNotFound at Phase 2 deploy.
bicep
resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { identity: { type: 'SystemAssigned' } properties: { configuration: { secrets: [ { name: 'db-connection-string' // ⛔ Do NOT replace vault.azure.net with environment().suffixes.keyvaultDns — it adds a leading dot → double-dot URL #disable-next-line no-hardcoded-env-urls keyVaultUrl: 'https://${keyVault.name}.vault.azure.net/secrets/db-connection-string' identity: 'system' // Uses the CA's system-assigned managed identity } ] } template: { containers: [{ env: [ { name: 'DATABASE_URL' secretRef: 'db-connection-string' // References the secret defined above } ] }] } }}
⛔ Never use conditional logic (??, ternary, empty(), union()) to mix plain and secret env vars in a single Bicep loop or array. ARM evaluates ALL property paths in conditional expressions — envVar.secretRef errors on items that don’t have that property, producing InvalidTemplate. Instead, define plain and secret env vars as separate arrays and concatenate:
⛔ Subnets MUST be defined inline in VNet properties.subnets[], NOT as separate Microsoft.Network/virtualNetworks/subnets child resources. Separate child resources cause InUseSubnetCannotBeDeleted on redeploy when NICs are attached.