Subchapter 6.4
references/code-deployment-container-apps.mdMarkdown8 KBView on GitHub
After IaC deployment creates the Container App with a placeholder image (mcr.microsoft.com/azuredocs/containerapps-helloworld:latest), deploy the actual application code. This is Phase 2 of the two-phase Container Apps deployment pattern.
⛔
--subscription {subscriptionId}on EVERYazcommand (fromcontext.json.azure.subscriptionId). Without it, the CLI uses whatever subscription is currently active — which may have changed since the prepare phase. This applies to ALL commands below.
⛔ Phase 2 is NOT optional. If IaC deployed a Container App with a placeholder image, you MUST execute Phase 2 before health checks. Do NOT leave a placeholder image and tell the user to deploy code manually.
Determine the code deploy path:
| Condition | Path | Steps |
|---|
ACR exists in IaC (services[] has Container Registry) | ACR build + image update | Steps 1–4 below |
| No ACR in IaC, simple app (single Dockerfile or source) | Add ACR to IaC + redeploy, then ACR build | Step 0 + Steps 1–4 |
| No ACR in IaC, no Dockerfile, Oryx-compatible (Node/Python/Go/.NET) | az containerapp update --source . | Step 5 (Oryx shortcut) |
Add container-registry.bicep module (ACR Basic, adminUserEnabled: false) + AcrPull role assignment for CA’s managed identity (role GUID: 7f951dda-4ed3-4680-a7ca-43fe172d538d). Redeploy IaC, wait ~60s for AcrPull role propagation. Log as healing attempt.
⛔ Update prepare-plan.json (add ACR to services[]), scaffold-manifest.json.files[], and costEstimate.
| Step | Command | Notes |
|---|---|---|
| 1. Build image | az acr build --subscription {subscriptionId} -r {acrName} -t {appName}:latest . --no-logs | Builds from workspace root. If buildRequirements.hasBuildKitSyntax, use -f Dockerfile.azure (see BuildKit handling below). ⛔ Build-time env vars: If Dockerfile has ARG NEXT_PUBLIC_* or ARG VITE_*, pass --build-arg NEXT_PUBLIC_API_URL=https://{api-fqdn} to inject the deployed API URL. These vars are baked into the JS bundle at build time — runtime env vars have no effect on client-side code. Get the API FQDN from Phase 1 output: az containerapp show -g {rg} -n {apiApp} --query properties.configuration.ingress.fqdn -o tsv. |
| 2. Update Bicep image | Edit infra/modules/{containerapp}.bicep: replace placeholder with image: '{acrLoginServer}/{appName}:latest'. Add ACR registry config: registries: [{ server: acrLoginServer, identity: 'system' }] | IaC-only — no imperative az containerapp update |
| 3. Redeploy | az deployment sub create --subscription {subscriptionId} --location {location} --template-file infra/main.bicep --parameters @infra/main.parameters.json --parameters administratorLoginPassword=$dbPassword --name app-onboard-code-deploy-{timestamp} --query properties.provisioningState -o tsv | Emit new portal link. ⛔ $dbPassword = existing KV secret, never new/placeholder — see Parameter Pass-Through. |
| 4. Verify revision | az containerapp show --subscription {subscriptionId} -g {rg} -n {ca} --query "{revision:properties.latestReadyRevisionName, image:properties.template.containers[0].image}" -o json | Confirm image is not the placeholder |
⛔
az acr buildfailures count toward thedeploy-result.json.healingAttempts[]counter. After 3 failed builds (even with different root causes), pause and present a diagnosis to the user: “Web image build failed 3 times: [root causes]. Continue healing? (Yes / Cancel).” Each build fix attempt = 1 healing entry. Cross-reference deploy SKILL.md healing loop rule.
⛔ Windows:
az acr buildmay fail withUnicodeEncodeError. Azure CLI log streaming crashes on non-ASCII characters (✓, ✗) using Windowscharmapcodec. Append--no-logsto the build command. Build success/failure is still reported via exit code.
az containerapp update --subscription {subscriptionId} -g {rg} -n {ca} --source . --set-env-vars NODE_ENV=production⛔
az containerapp update --sourceis allowed for code deploy ONLY. This is different fromaz containerapp up --source(which is ⛔ BLOCKED because it creates resources imperatively).update --sourceupdates an EXISTING Container App — no state drift.
After Phase 1, seed real secret values into KV before Phase 2 activates secretRef:
az keyvault secret set --subscription {subscriptionId} --vault-name {kvName} --name {secret-name} --value $generatedValue⛔ Use the SAME generated password passed to
az deployment sub create. Shell variables don’t persist between tool calls — reload fromdeploy-secrets.env(see deploy-safety.md § Deploy Checklist) or pass to BOTH commands in the same block.
az containerapp revision list⛔
az containerapp revision restartdoes NOT re-resolve KV secrets. Container Apps caches KV-backedsecretRefvalues at revision creation time. To pick up updated KV secrets, create a NEW revision by redeploying Bicep (preferred) oraz containerapp update --revision-suffix rev{timestamp}. Do NOT userevision restartfor KV secret rotation — it only restarts the container with the same cached values.
⛔ KV secretRef AND ACR registries require managed identity + roles to exist first. Phase 1 of the two-phase deploy creates the Container App with a placeholder image,
registries: [], andsecrets: []. Both KVsecretRefand ACRregistrieswithidentity: 'system'fail in Phase 1 because the CA’s managed identity doesn’t exist yet (no principalId → AcrPull/KV role assignment fails → “Operation expired”). After Phase 1 completes and RBAC propagates (~60s), Phase 2 redeploys with ACR registries + KV secretRef + real image + correcttargetPort.
Creating Azure-specific config (e.g., config-azure.yml) is fine for non-secret values.
⛔ NEVER bake secrets into config files COPY’d into Docker images.
Secret injection: Config uses placeholders → az containerapp secret set → az containerapp update --set-env-vars KEY=secretref:name → framework env var override.
Go/Viper:
AutomaticEnv()mapsPOSTGRES_PASSWORD→postgres_password(underscores), NOTpostgres.password. WithoutSetEnvKeyReplacer(strings.NewReplacer(".", "_")), env vars can’t override nested keys.
ACR’s az acr build uses the classic Docker builder — it does NOT support BuildKit. When buildRequirements.hasBuildKitSyntax == true: build using the ACR-compatible copy instead: az acr build -f Dockerfile.azure .. The user’s original Dockerfile stays untouched. If Dockerfile.azure doesn’t exist yet, create it by stripping BuildKit syntax from the original Dockerfile per dockerfile-generation.md § ACR Build Compatibility (opens in a new tab).
⛔ On every redeploy, pass the SAME values you originally used — a full desired-state apply, not a patch, so an omitted param reverts to its default and a regenerated secret overwrites the live value. Two params bite:
containerImage — omitting it reverts the Container App to the placeholder image.administratorLoginPassword (DB modules) — passing a new or placeholder value silently RESETS the database admin password, desyncing it from the Key Vault secret the app reads → runtime auth failures (Access denied for user).$dbPassword = az keyvault secret show --subscription {subscriptionId} --vault-name {kvName} --name {db-secret} --query value -o tsv
az deployment sub create ... `
--parameters containerImage='{acrLoginServer}/{appName}:latest' `
--parameters administratorLoginPassword=$dbPassword `
--query properties.provisioningState -o tsv⛔ You MUST read
database-post-deploy.mdfor migration discovery, execution via Container Apps exec, error handling, and PostgreSQL-specific checks.