I removed one setting from a container app. Azure Container Apps built a new revision, marked it Healthy and gave it all the traffic. The app answered model: not-configured. No failed deployment, no error, everything green, and the app was broken.
This explainer shows what "Healthy" really checks, why a wrong ingress port skips every check, and the readiness probe that keeps a broken release away from users. Everything below comes from a real run in my own Azure subscription.
Key points
- Healthy means the probes pass. Without probes of your own, Container Apps adds default TCP probes on the ingress target port. They only ask whether something accepts a connection.
- Ingress is not part of a revision. The target port, traffic rules and secrets are app-level configuration: a change creates no revision and goes live for every revision at once, with no gate in between.
- Readiness decides traffic, liveness restarts. Put what a replica needs to do its job into readiness; keep liveness to the process itself, and never put the database into it.
- Debug from the outside in. Revision list, then console and system logs, then Log Analytics for history; deactivate a bad revision instead of deleting it.
What Healthy checks
Removing an environment variable changes the revision's template, so Container Apps builds a new revision. A revision is a frozen snapshot of your container settings: the image, the environment variables, the scale rules. It runs as one or more replicas, the running copies.
In single revision mode (the default), the old revision keeps all the traffic until the new one is ready: provisioned, scaled up, and past its startup and readiness probes. That is the gate. I had defined no probes, so with MODEL_NAME gone, the new revision still got 100 % of the traffic.
The reason: with ingress enabled, Container Apps adds default probes for every type you don't define. They are TCP checks on the ingress target port:
| Probe | Default check |
|---|---|
| Startup | TCP on the target port, every second, up to 240 failures |
| Readiness | TCP on the target port, every 5 seconds |
| Liveness | TCP on the target port |
The app reads the setting with a fallback, so it started, the port opened, and the gate let it through:
MODEL_NAME = os.getenv("MODEL_NAME", "not-configured")
To find a change like that, compare the last good revision with the new one:
az containerapp revision show -n ai-api -g rg-demo \--revision ai-api--v2 \--query "properties.template.containers[0].env" -o table
For v1 this printed MODEL_NAME gpt-5.4-mini. For v2 it printed nothing.
The wrong port
The app listens on port 8000. I changed the ingress target port to 3000:
az containerapp ingress update -n ai-api -g rg-demo \--target-port 3000
A wrong port sounds like a failed deployment, but there is no new revision and no check. Ingress is application-scope configuration, like traffic rules and secrets: it sits in front of every revision and changes all of them at once. The very next request got a 503 with Connection refused.
The console log shows the mismatch in one line: gunicorn is Listening at: http://0.0.0.0:8000, while ingress sends to 3000.
A restart doesn't help. The new replica's default startup probe now knocks on port 3000 and fails every second (Probe of StartUp failed with status code: 1 in the system log). The revision list still said Healthy, because the old replica kept running, and every request still failed.
Readiness and liveness
You make Healthy mean something by giving the gate better questions:
| Probe | Question | When it fails |
|---|---|---|
| Readiness | Can this replica take requests right now? | the replica gets no traffic, but keeps running |
| Liveness | Is the process still alive? | Container Apps restarts the replica |
| Startup | Has the app started? | runs only while the app starts; then readiness and liveness take over |
Readiness gets its own endpoint. This one returns 503 while MODEL_NAME is missing:
@app.route("/health/ready")def ready():if MODEL_NAME == "not-configured":return {"status": "not ready"}, 503return {"status": "ready"}
And the probes in the container template (HTTP probes count a status from 200 to 399 as success):
probes:- type: ReadinesshttpGet: { path: /health/ready, port: 8000 }periodSeconds: 5- type: LivenesshttpGet: { path: /health, port: 8000 }periodSeconds: 10
The trap: if your liveness probe also checks the database, and the database is down for a minute, every replica restarts, again and again. A short blip becomes an outage, with cold starts on top. Put the database check into readiness instead: during the outage the replicas stop taking traffic, nothing restarts, and they come back by themselves when the database does.
When it breaks anyway
- The revision list: which revision is active, is it healthy, where does the traffic go.
- The logs: console logs are what your app prints; system logs are what the platform saw (probes, image pulls, restarts).
az containerapp logs showstreams both (--type system; system logs only come as JSON). - History in Log Analytics: the environment's workspace keeps them, a few minutes behind.
- Compare the failing revision with the last good one.
- Deactivate, don't delete: it stops the replicas, inactive revisions cost nothing, and the evidence stays.
A restart is not a fix: it starts the same image with the same configuration.
az containerapp revision list -n ai-api -g rg-demo -o tableaz containerapp logs show -n ai-api -g rg-demo --type systemaz containerapp revision deactivate -n ai-api -g rg-demo \--revision ai-api--v5
What happened when I ran it
Tested on October 19, 2026 in my own Azure subscription (westeurope, Azure CLI 2.88.0); everything was deleted afterwards. The app is the Flask API from the Microsoft Learn exercise, plus the /health/ready endpoint above. My first attempt in germanywestcentral failed at the environment: AKSCapacityHeavyUsage, "Creating a new cluster is unavailable at this time in region germanywestcentral". In westeurope the environment took 18.6 minutes, the cloud build 69 seconds, the app 43 seconds.
The broken revision, live:
> az containerapp revision list -n ai-api -g rg-demo --all -o tableCreatedTime Active Replicas TrafficWeight HealthState ProvisioningState Name------------------------- -------- ---------- --------------- ------------- ------------------- ---------------2026-10-19T15:32:45+00:00 False 0 0 Healthy Provisioned ai-api--f5uwknl2026-10-19T15:33:07+00:00 True 1 0 Healthy Provisioned ai-api--v12026-10-19T15:34:06+00:00 True 1 100 Healthy Provisioned ai-api--v2> (irm https://$FQDN/).modelname----not-configured
Each az containerapp update took 18 to 22 seconds and returned before the new revision was ready: asked for the latest ready revision, it still printed the old one.
The wrong port:
> curl.exe -sS -i -m 30 https://$FQDN/healthHTTP/1.1 503 Service Unavailableupstream connect error or disconnect/reset before headers. retried and the latestreset reason: remote connection failure, transport failure reason: delayed connecterror: Connection refused
The same mistake with the readiness probe (revision v5, four minutes later): v5 never became ready, the app kept answering gpt-5.4-mini from v4, and the system log showed the gate at work:
Probe of Readiness failed with status code: 503Probe of Readiness failed with status code: 503Probe of Readiness failed with status code: 503
Log Analytics, queried two and a half minutes after the last call:
RevisionName_s TableName Calls Failed---------------- ------------- ------- --------ai-api--v4 PrimaryResult 70 0ai-api--v5 PrimaryResult 53 53
(The first row of the revision list, ai-api--f5uwknl, is a placeholder revision the CLI created while it set up the managed identity for the image pull.)
One more detail: in single revision mode, revision list showed v5 with a traffic weight of 100 while it was not ready. Requests still went to v4, the latest ready revision, and the first one took 24 seconds, because v4 had scaled to zero in the meantime.
Left out of the video
Tags vs digests for production images, start, stop and scale to zero, CPU and memory sizing (on Consumption from 0.25 vCPU / 0.5 GiB up to 2 vCPU / 4 GiB per replica), structured logging fields and multiple revision mode with traffic splitting. They are in the Microsoft Learn module below.





