Container Apps

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Examples on this page target Azure CLI 2.90.0 with the containerapp CLI extension at 1.3.0b5 — commands that are still preview-only in that extension are called out explicitly; every other command shown is generally available in CLI core. Azure Container Apps Express reached general availability on 2026-09-23 (checked 2026-09-26); re-verify all version and feature-availability claims on this page against the Container Apps documentation before relying on them, since this service ships new capabilities frequently.

This page walks through deploying Bookshelf’s catalog-api — a Java/Spring Boot HTTP service — to Azure Container Apps end to end: the environment and app model, the three ways to get code running with a single az containerapp up, ingress and custom domains, revisions and traffic splitting, and the pricing and troubleshooting a reader needs before calling this "production ready." Scaling rules, Container Apps jobs and Dapr are deliberately out of scope here; they are covered in depth on Container Apps: Scaling, Jobs and Dapr. Whether Container Apps is even the right compute choice for a given workload, versus App Service and Azure Functions or AKS Clusters/Deploying Microservices on AKS, is covered on Choosing a Compute Service.

Concepts: environment, app, revision, replica, container

Six terms recur throughout this page and the Azure CLI’s own flag names, nested in nesting order:

Environment

A secure boundary around one or more container apps and jobs, backed by a shared virtual network, DNS suffix, and (for a standard environment) a Log Analytics workspace. Apps in the same environment can reach each other by name and share the same network and logging destination; apps that must never share compute or must be isolated by team/lifecycle go in separate environments. Bookshelf uses one environment, cae-bookshelf, for catalog-api and orders-api.

App (container app)

One deployable unit — a name, a set of revisions, and the ingress/scaling/identity configuration that applies across all of them. catalog-api and orders-api are each their own container app in cae-bookshelf.

Revision

An immutable, versioned snapshot of an app’s template (image, resources, scale rules, env vars). The first revision is created automatically on first deploy; every later change to the template creates a new one. A revision-scope change (image, resources, scale rules) creates a new revision; an application-scope change (secrets, ingress, revision mode) applies to every existing revision without creating a new one.

Replica

One running instance of a revision — the unit that actually scales in and out. A revision with --min-replicas 2 --max-replicas 10 can have anywhere from 2 to 10 replicas running at once, each independently scheduled and independently billed.

Container

A replica runs one or more containers: the main app container plus, in advanced scenarios, sidecar containers (sharing the replica’s disk and network, e.g. a log-forwarding agent next to catalog-api) and init containers (run to completion, in array order, before the main container starts — e.g. a database-migration step ahead of catalog-api). Most container apps use a single container; reach for sidecars/init containers only when containers are genuinely tightly coupled, since the usual microservices pattern is one container app per service rather than one app with several containers.

Diagram showing an Azure Container Apps environment containing container apps

Workload profiles: v2 vs. legacy Consumption-only, Consumption vs. Dedicated

Every Container Apps environment is one of two environment types:

Type What it means

Workload profiles (v2) — the default

Supports a Consumption workload profile (serverless, scale-to-zero, pay-per-use) and any number of Dedicated workload profiles (reserved VM-backed compute, sized by you) side by side in the same environment. Gives access to the fuller networking stack (firewall/NSG support, native private endpoints, user-defined routes).

Consumption-only (v1) — legacy

The older environment type: Consumption plan only, no Dedicated profiles, and a narrower set of networking features. It is no longer the default for new environments; Microsoft’s own guidance is that every new environment should be Workload profiles (v2), including one that only ever runs consumption workloads, because it gives the same consumption behavior plus room to add a Dedicated profile later without recreating the environment.

Within a Workload profiles (v2) environment, every app runs on one of two plans, chosen per workload profile:

Plan Behavior

Consumption

Serverless: apps scale to zero, and billing is per-second resource consumption (see Pricing model). Every v2 environment gets exactly one Consumption profile automatically; it cannot be removed, and it costs nothing when unused.

Dedicated

Apps run on reserved virtual machine instances that you size (general purpose, memory-optimized, or GPU), sized and scaled per profile rather than per app. A Dedicated plan management fee applies to the environment as soon as it has one Dedicated workload profile, on top of the per-instance compute cost.

catalog-api and orders-api both run on the environment’s default Consumption profile; cae-bookshelf never needs a Dedicated profile in this scenario.

Managed VNet vs. custom (bring-your-own) VNet

Creating an environment without specifying a subnet provisions a managed virtual network for it automatically, with a small, fixed set of network controls — adequate for cae-bookshelf’s public-facing APIs. Supplying `--infrastructure-subnet-resource-id against an existing subnet instead gives fine-grained control: custom DNS, network security groups, user-defined routes, and (with --internal-only true) an environment with no public inbound IP at all, reachable only through an internal load balancer inside that VNet. Choose a custom VNet when catalog-api must sit behind existing network security controls or peer with other Azure networks; the managed VNet is the right default otherwise.

Container Apps Express vs. a standard environment

Container Apps Express is a separate, streamlined environment mode purpose-built for HTTP web apps and APIs: no environment to size or network to configure up front, sub-second scale-from-zero, and a dedicated portal at containerapps.azure.com. It trades that simplicity for a deliberately narrower feature set — as of this writing it does not support custom domains, Key Vault secret references, system-assigned managed identities, Dapr, Container Apps jobs, custom KEDA scale rules, multiple revisions and traffic splitting, workload profiles, or GPU compute, and it only runs linux/amd64 HTTP workloads. It does support internal or external HTTP ingress, virtual network egress, and user-assigned managed identities — both for the app’s runtime calls and for pulling images from Azure Container Registry. Billing is the same Consumption per-second model and free grant described in Pricing model, with no separate environment fee.

# Create an express environment (no subnet or profile sizing needed)
az containerapp env create \
  --name cae-bookshelf-express \
  --resource-group "$RG" \
  --environment-mode express \
  --logs-destination none

# Express supports user-assigned (not system-assigned) identities for image pulls;
# id-catalog-api holds AcrPull on $ACR as set up in the ACR section below
IDENTITY_ID=$(az identity show --name id-catalog-api --resource-group "$RG" --query id --output tsv)

az containerapp create \
  --name catalog-api \
  --resource-group "$RG" \
  --environment cae-bookshelf-express \
  --image "$ACR.azurecr.io/catalog-api:1.0.0" \
  --user-assigned "$IDENTITY_ID" \
  --registry-server "$ACR.azurecr.io" \
  --registry-identity "$IDENTITY_ID" \
  --target-port 8080 \
  --ingress external \
  --min-replicas 0 \
  --max-replicas 3

Pulling from ACR with a user-assigned identity (Pulling from ACR with managed identity) works the same way in Express, so that is not what rules it out for Bookshelf. The blockers are elsewhere: catalog-api needs a custom domain (Custom domains and free managed certificates) and Key Vault secret references (Identity and Access), notifier scales on a custom Service Bus KEDA rule and runs as a Container Apps job, and the blue-green rollouts in Revision modes and traffic splitting rely on multiple revisions and traffic splitting — none of which Express supports today. So the rest of this page builds cae-bookshelf as a standard Workload profiles (v2) environment. Reach for Express instead when a workload is a plain HTTP app/API/prototype that does not need those features and where the fastest possible cold start matters more than full configurability — and remember that an existing environment might be selected for automatic migration to Express by Microsoft, with 25—​30 days' notice and an opt-out, if its detected feature usage is Express-compatible.

Creating the environment

In the Azure portal

  1. Search for Container Apps Environments and select Create.

  2. On the Basics tab, choose the subscription and $RG, name the environment cae-bookshelf, and pick the region.

  3. On the Workload profiles tab, keep the default Consumption profile; only add a Dedicated profile if a specific app needs reserved compute or GPU.

  4. On the Networking tab, keep Use managed virtual network for the managed-VNet default, or select Use custom virtual network and pick an existing subnet for the custom-VNet path described above; toggle Internal if the environment must have no public inbound IP.

  5. On the Monitoring tab, keep the default new Log Analytics workspace (or point at an existing one).

  6. Select Review + create, then Create.

With the Azure CLI

export RG=rg-bookshelf
export LOCATION=eastus2
export ACR=acrbookshelf
export ENV=cae-bookshelf
export APP=catalog-api

az group create --name "$RG" --location "$LOCATION"

az extension add --name containerapp --upgrade
az provider register --namespace Microsoft.App
az provider register --namespace Microsoft.OperationalInsights

# Standard, managed-VNet Workload profiles (v2) environment with the default Consumption profile
az containerapp env create \
  --name "$ENV" \
  --resource-group "$RG" \
  --location "$LOCATION"

Deploying with az containerapp up

az containerapp up is a single command that creates whatever it needs (resource group, registry, environment, Log Analytics workspace, app) and works three ways.

From an existing image

az containerapp up \
  --name "$APP" \
  --resource-group "$RG" \
  --environment "$ENV" \
  --image "$ACR.azurecr.io/catalog-api:1.0.0" \
  --ingress external \
  --target-port 8080

From local source (--source ., cloud build to ACR)

Run from catalog-api’s own project directory. With a `Dockerfile present, up builds from it (and, if the Dockerfile has an EXPOSE instruction, infers --ingress/--target-port from it); without one, it uses Azure’s cloud build (Buildpacks) for supported stacks, which includes Java.

az containerapp up \
  --name "$APP" \
  --resource-group "$RG" \
  --environment "$ENV" \
  --source . \
  --ingress external \
  --target-port 8080

This builds the image, pushes it to a Container Registry (creating one if --registry-server isn’t given — see Azure Container Registry for a dedicated, pre-existing $ACR instead), and deploys the app. If the command hangs waiting on the build agent, re-running it is the documented workaround for a known transient build-agent error; add --debug for more detail on any other failure.

From a GitHub repository (--repo, generated GitHub Actions workflow)

az containerapp up \
  --name "$APP" \
  --resource-group "$RG" \
  --environment "$ENV" \
  --repo "https://github.com/<org>/bookshelf-catalog-api" \
  --ingress external \
  --target-port 8080

This path requires a Dockerfile in the repository, creates (or reuses) a service principal to let the generated workflow deploy on Azure’s behalf, and commits a GitHub Actions workflow file that rebuilds and redeploys on every push. Because rerunning up this way creates another workflow, redeploy by pushing to the repository afterward, not by rerunning the command; edit the generated workflow file directly for anything beyond its defaults. If Bookshelf already manages CI/CD with workload identity federation (see Identity and Access), pass an existing service principal explicitly instead of letting up create one:

az containerapp up \
  --name "$APP" \
  --resource-group "$RG" \
  --environment "$ENV" \
  --repo "https://github.com/<org>/bookshelf-catalog-api" \
  --service-principal-client-id "$SP_CLIENT_ID" \
  --service-principal-client-secret "$SP_CLIENT_SECRET" \
  --service-principal-tenant-id "$TENANT_ID" \
  --ingress external \
  --target-port 8080

up intentionally only sets a handful of defaults (image, ingress, target port). Anything past that — Dapr, scaling rules, secrets, transport protocol — is configured afterward with az containerapp update and the other az containerapp subcommands used throughout the rest of this page; up is not a synonym for update, and rerunning it against the same --resource-group/--environment redeploys rather than reconfigures.

az containerapp create / update / show / logs show / exec

The lower-level commands up wraps, used directly whenever more control is needed than `up’s defaults give:

# Create explicitly (equivalent building blocks that `up` assembles for you)
az containerapp create \
  --name "$APP" \
  --resource-group "$RG" \
  --environment "$ENV" \
  --image "$ACR.azurecr.io/catalog-api:1.0.0" \
  --target-port 8080 \
  --ingress external \
  --cpu 0.5 --memory 1.0Gi \
  --min-replicas 1 --max-replicas 3

# Update settings on an existing app (creates a new revision if any changed setting is revision-scope)
az containerapp update \
  --name "$APP" \
  --resource-group "$RG" \
  --image "$ACR.azurecr.io/catalog-api:1.1.0" \
  --cpu 1.0 --memory 2.0Gi \
  --min-replicas 2 --max-replicas 5

# Inspect the app's current configuration
az containerapp show --name "$APP" --resource-group "$RG" --output yaml

# Stream console logs (last 50 lines, following)
az containerapp logs show \
  --name "$APP" \
  --resource-group "$RG" \
  --type console \
  --tail 50 \
  --follow

# Open an interactive shell in a running replica, for interactive debugging
az containerapp exec --name "$APP" --resource-group "$RG"

CPU and memory combinations

On the Consumption plan, the total vCPU and memory allocated across every container in a replica must add up to one of a fixed table of pairs — 0.25 vCPU with 0.5 GiB up to 4.0 vCPU with 8.0 GiB, in matching 0.25 vCPU / 0.5 GiB steps (0.5/1.0, 0.75/1.5, 1.0/2.0, and so on); a Consumption-only (legacy v1) environment additionally caps this at 2.0 vCPU / 4.0 GiB. Set it with --cpu/--memory on create/update as shown above; a Dedicated workload profile instead lets catalog-api request any size the profile’s own VM SKU can accommodate.

The portal flow: Basics, Container, Ingress, and the blades after create

In the Azure portal

  1. Search for Container Apps and select Create.

  2. Basics: subscription, $RG, container app name catalog-api, region, and the Container Apps Environment (cae-bookshelf, created above or created inline here).

  3. Container: Image source — Azure Container Registry (pick $ACR and the catalog-api repository/tag), Docker Hub or other registries, or Quickstart image for a placeholder; set CPU and memory to one of the combinations above.

  4. Ingress: enable ingress, choose Accepting traffic from anywhere (external) or Limited to Container Apps Environment (internal), set Target port to 8080, and leave Ingress type as HTTP unless a raw TCP protocol is needed.

  5. Review + create, then Create.

  6. Once created, the app’s own blade exposes further settings as separate left-navigation entries, each covered below or on the sibling scaling/jobs/Dapr page: Scale (min/max replicas and scale rules — Container Apps: Scaling, Jobs and Dapr), Secrets (name/value pairs referenced from env — see Key Vault and Secrets and Runtime Configuration for how Bookshelf actually sources these), Identity (system-/user-assigned managed identity — Identity and Access), Custom domains (Custom domains and free managed certificates), and Revisions (Revision modes and traffic splitting).

Ingress

External vs. internal, target port, transport

Two settings, at two different levels, determine reachability:

  • The environment’s accessibility level (fixed at creation: public, or --internal-only true for no public inbound IP at all).

  • The app’s own ingress visibility — external (published at whichever boundary the environment has: the public internet on a public environment, the internal load balancer on an internal one) or internal (reachable only from other apps in the same environment).

az containerapp ingress enable \
  --name "$APP" \
  --resource-group "$RG" \
  --type external \
  --target-port 8080 \
  --transport auto

--transport accepts auto (the default — negotiates HTTP/1.1 or HTTP/2 automatically, also covering WebSocket and gRPC), http, http2, or tcp for a non-HTTP protocol. --target-port must match the port catalog-api’s embedded Tomcat/Netty server actually listens on (`server.port in Spring Boot).

IP restrictions, CORS, session affinity

# Allow only Bookshelf's office network; every other source is denied
az containerapp ingress access-restriction set \
  --name "$APP" \
  --resource-group "$RG" \
  --rule-name allow-office \
  --ip-address 203.0.113.0/24 \
  --action Allow

# Enable CORS for the Bookshelf web front end
az containerapp ingress cors enable \
  --name "$APP" \
  --resource-group "$RG" \
  --allowed-origins "https://shop.bookshelf.example" \
  --allowed-methods "GET,POST" \
  --allowed-headers "*"

# Sticky sessions: pin a client to the replica that handled its first request
az containerapp ingress sticky-sessions set \
  --name "$APP" \
  --resource-group "$RG" \
  --affinity sticky

Session affinity and CORS only apply to the app’s main HTTP ingress port, not to any additional TCP ports; IP restrictions accept both allow and deny rules evaluated in order, with an implicit deny-all once at least one allow rule exists.

Custom domains and free managed certificates

Container Apps issues and auto-renews a free TLS certificate for a custom domain, provided the app stays publicly reachable from ingress and the DNS records below stay in place.

Domain type Record type What it points to

Apex (bookshelf.example)

A

The environment’s static IP (az containerapp env show --query properties.staticIp).

Subdomain (api.bookshelf.example)

CNAME

The app’s generated FQDN (az containerapp show --query properties.configuration.ingress.fqdn) — not an intermediate CNAME such as a CDN or traffic manager, which blocks certificate issuance.

Either way, a TXT record at asuid (apex) or asuid.<subdomain> (subdomain) holding the app’s domain verification ID is required to prove ownership.

In the Azure portal

  1. Open catalog-api → Networking → Custom domains → Add custom domain.

  2. Choose Managed certificate, enter the domain, and select the matching Hostname record type (A or CNAME).

  3. Create the DNS records shown in the Domain validation section with Bookshelf’s DNS provider, then select Validate and Add.

With the Azure CLI

export DOMAIN=api.bookshelf.example

# Domain verification ID and the value the CNAME/A record must point to
az containerapp show --name "$APP" --resource-group "$RG" \
  --query properties.customDomainVerificationId --output tsv
az containerapp show --name "$APP" --resource-group "$RG" \
  --query properties.configuration.ingress.fqdn --output tsv

# After the DNS records above are in place:
az containerapp hostname add \
  --hostname "$DOMAIN" \
  --resource-group "$RG" \
  --name "$APP"

az containerapp hostname bind \
  --hostname "$DOMAIN" \
  --resource-group "$RG" \
  --name "$APP" \
  --environment "$ENV" \
  --validation-method CNAME

Use --validation-method HTTP instead of CNAME for an apex domain (A record). Binding can take several minutes while the certificate is issued; the app must stay in a running state throughout, or the domain-ownership challenge fails with a timeout.

Revision modes and traffic splitting

A revision is immutable and versioned; a revision-scope change (image, resources, scale rules) always creates a new one, while an application-scope change (secrets, ingress, revision mode itself) applies across every existing revision without creating one. Three revision modes govern how many revisions can be active for traffic at once and how traffic reaches them:

Single (default)

One active revision at a time. A new revision only takes over traffic once it has provisioned, scaled to the previous revision’s replica count, and passed its startup/readiness probes — so deploys are zero-downtime, and a failed new revision simply never receives traffic.

Multiple

More than one revision can be active simultaneously, with the app’s ingress splitting traffic between them by percentage (traffic splitting) and/or exposing a fixed, stable URL per revision label that can be moved between revisions independent of the percentage split. Revision labels (az containerapp revision label add) are GA and are most useful in this mode.

Labels (deployment labels, preview)

A separate third mode where named labels such as stage and prod — rather than individual revisions — are the unit you deploy to and route by. Each new revision is created for a target label, traffic is split between labels, promoting a release means pointing prod at the revision stage already runs, any revision no longer referenced by a label is shut down automatically, and each label keeps a history that makes rollback a matter of pointing it back at an earlier revision.

flowchart LR C["Client request"] --> I["catalog-api ingress\n(multiple revision mode)"] I -- 80% --> R2["Revision: catalog-api--rev2\n(replicas x3)"] I -- 20% --> R1["Revision: catalog-api--rev1\n(replicas x2)"] I -. label: prod .-> R2 I -. label: canary .-> R1

Switching modes and splitting traffic

# Switch to multiple revision mode (an application-scope change: no new revision is created)
az containerapp revision set-mode --name "$APP" --resource-group "$RG" --mode multiple

# List active revisions and their current traffic weight
az containerapp revision list --name "$APP" --resource-group "$RG" --output table

# Split traffic 80/20 between the latest revision and a named earlier one
az containerapp ingress traffic set \
  --name "$APP" \
  --resource-group "$RG" \
  --revision-weight latest=80 catalog-api--rev1=20

# Blue/green cutover: move 100% of traffic to the new revision once validated
az containerapp ingress traffic set \
  --name "$APP" \
  --resource-group "$RG" \
  --revision-weight latest=100

# Revision label (GA, multiple mode): give a revision a stable, percentage-independent URL
az containerapp revision label add \
  --name "$APP" \
  --resource-group "$RG" \
  --revision catalog-api--rev2 \
  --label prod

A label’s URL stays the same as the label is moved between revisions, and labels work independently of the percentage split — Bookshelf can point a canary label at a brand-new revision for a specific test audience while the app’s main traffic split is still 100% on the previous one.

Deployment labels mode (preview)

In the Azure portal

  1. Open catalog-api and, under Application, select Revisions and replicas.

  2. Select Deployment mode, choose Deployment labels, and select Apply. Container Apps creates a first label named label-1.

  3. Under Application, select Labels → New label, pick the label to base it on, name it (for example stage), and select Duplicate.

  4. Use Show history on a label to review which revisions it pointed to, and point it back at an earlier revision to roll back.

With the Azure CLI

Labels mode needs the containerapp extension (the core CLI’s revision set-mode only accepts single and multiple):

az extension add --name containerapp --upgrade --allow-preview true

# Switch catalog-api to labels mode; new revisions go to the stage label by default
az containerapp revision set-mode --name "$APP" --resource-group "$RG" --mode labels --target-label stage

# Deploy a new image as a new revision behind the stage label
az containerapp update \
  --name "$APP" \
  --resource-group "$RG" \
  --image "$ACR.azurecr.io/catalog-api:1.1.0" \
  --target-label stage

# Promote: point the prod label at the revision stage is running
az containerapp revision label add \
  --name "$APP" \
  --resource-group "$RG" \
  --revision catalog-api--rev3 \
  --label prod

# Inspect a label's history (for rollback) and the current traffic per label
az containerapp label-history show --name "$APP" --resource-group "$RG" --label prod
az containerapp ingress traffic show --name "$APP" --resource-group "$RG"

Pulling from ACR with managed identity

catalog-api pulls its image from $ACR using a user-assigned managed identity rather than the registry’s admin username/password, following the same pattern as every other Bookshelf-to-Azure credential covered on Identity and Access:

az identity create --name id-catalog-api --resource-group "$RG" --location "$LOCATION"

IDENTITY_ID=$(az identity show --name id-catalog-api --resource-group "$RG" --query id --output tsv)
IDENTITY_PRINCIPAL_ID=$(az identity show --name id-catalog-api --resource-group "$RG" --query principalId --output tsv)
ACR_ID=$(az acr show --name "$ACR" --resource-group "$RG" --query id --output tsv)

az role assignment create \
  --assignee-object-id "$IDENTITY_PRINCIPAL_ID" \
  --assignee-principal-type ServicePrincipal \
  --role AcrPull \
  --scope "$ACR_ID"

az containerapp identity assign \
  --name "$APP" \
  --resource-group "$RG" \
  --user-assigned "$IDENTITY_ID"

az containerapp registry set \
  --name "$APP" \
  --resource-group "$RG" \
  --server "$ACR.azurecr.io" \
  --identity "$IDENTITY_ID"

Logs in Log Analytics

A standard environment sends every app’s stdout/stderr, scaling events, Dapr sidecar logs (when Dapr is enabled) and system-level events to one shared Log Analytics workspace, queryable with the same Kusto Query Language used throughout Azure Monitor:

az monitor log-analytics query \
  --workspace "$(az containerapp env show --name "$ENV" --resource-group "$RG" --query properties.appLogsConfiguration.logAnalyticsConfiguration.customerId --output tsv)" \
  --analytics-query "ContainerAppConsoleLogs_CL | where ContainerAppName_s == 'catalog-api' | order by TimeGenerated desc | take 50"

Pricing model

On the Consumption plan, billing has two components. Resource consumption is metered in vCPU-seconds and GiB-seconds — the vCPU and memory actually allocated to running replicas, per second — with the first 180,000 vCPU-seconds and 360,000 GiB-seconds free each calendar month, per subscription. Requests are metered separately, with the first 2 million HTTP requests free each calendar month per subscription (health-probe traffic doesn’t count); a scaled-to-zero revision incurs no resource charges at all. A replica that is running but genuinely idle — fully started, no in-flight requests, under roughly 0.01 vCPU and 1,000 bytes/second of network traffic — bills at a reduced idle rate rather than the active rate, which matters for catalog-api configured with --min-replicas greater than zero for fast cold-start avoidance. On the Dedicated plan, billing is a fixed monthly Dedicated plan management fee for the environment (charged only once at least one Dedicated workload profile exists) plus a variable per-instance cost that scales with the profile’s allocated vCPU/RAM and instance count. Exact current per-unit rates and free-grant amounts vary by currency and region and are published at the Container Apps pricing page; treat any specific number here as indicative, not contractual.

Troubleshooting

A revision stuck with a Provisioning status and a Processing running status indefinitely, or one that takes more than ten minutes to reach Provisioned but ends up Degraded with "0/1 replicas ready," is almost always a health-probe misconfiguration — the default liveness/readiness/startup probes hit the ingress target port over TCP, so a Java app with a slow Spring context startup typically needs a longer probe initial delay rather than a genuine fix to the app itself. A deploy that fails outright is usually an image-pull failure: confirm the managed identity has AcrPull on $ACR (Pulling from ACR with managed identity), or that the environment’s outbound network path/DNS can reach the registry if a custom VNet is in play. System-level events — as opposed to the app’s own console output — surface with:

az containerapp logs show --name "$APP" --resource-group "$RG" --type system --tail 50

A revision that never receives traffic despite looking healthy is, in multiple revision mode, almost always a traffic-splitting weight that never got updated after the new revision was created — check with az containerapp ingress traffic show. Requests to an internal environment that return HTTP 404 with "Container App is stopped or does not exist," even though the replica is healthy and TLS succeeds, mean the app’s own ingress visibility is set to internal rather than external; on an internal environment, external app ingress still doesn’t reach the public internet; it only reaches the environment’s internal load balancer. Finally, a Container Apps environment left idle (no running apps or jobs) or stuck in a failed state (typically a broken VNet or Azure Policy configuration) for more than 90 continuous days is automatically deleted by the platform — a caveat to keep in mind for a demo or staging environment left running unattended, not a substitute for the explicit cleanup below.

Clean up

az group delete --name "$RG" --yes --no-wait

This removes cae-bookshelf, catalog-api, orders-api, and every other resource created in $RG on this page. As noted above, an idle or failed environment is eventually deleted automatically after 90 days, but that is a platform safety net, not a reason to skip deleting a demo resource group deliberately once it’s no longer needed.

References