Operational configuration
- Lesson 14
- 45 min
- Module 5: Deploy and clean up
Lesson 14 of 15 · Docker and Cloud Essentials for AI DevelopersOperational configuration
1. Understand containers
2. Build the application image
3. Networking, configuration and persistence
4. Cloud building blocks
5. Deploy and clean up
What you will be able to do
A managed platform runs your container, but it does not know your app. Tell it how to check the app, how large it may be, how far it may grow, and which settings it must have. Then a bad version stops before it reaches anyone.
Before you startWhat to know and to have ready
What you need to know
- Liveness and readiness,
/healthand/ready, from Publish safely: Liveness and readiness in Build and Deploy Your First API. REQUIRE_API_KEYand theSettings errorlines, from Runtime configuration: Required and optional settings and A file of settings.- Why
/readyfails when the database is down, from Persistent data: Your turn: the database stops. - Scale to zero and the requests that you did not send, from Cost awareness: Scale to zero, and the requests you did not send.
What you need on your computer
- Live route: the container app
ticket-apiinrg-ticket-api-practice, its registry name and its key in your terminal, from Registry and deployment: The live route: create the deployment. If you closed the terminal, setRGandACRagain, as in that section. - Simulated route: the local simulation running, from Registry and deployment: The simulated route: the same steps on your computer. You also need the local registry with
ticket-api:1.1.0forlinux/amd64, from Registry and deployment: Build for the cloud’s CPU. deploy/containerapp.yamlin the project. If it is missing, copy it from the snapshotend-of-m05.
Tested with Docker Desktop 4.81.0 (Docker Engine 29.6.1) on macOS, Apple silicon. The Azure steps were run with Azure CLI 2.88.0 and the containerapp extension 1.3.0b5, in region canadacentral. The Windows and Linux steps are not tested.
What the platform knows now
Section titled “What the platform knows now”After Registry and deployment, Amira asks Azure how it checks her app:
az containerapp show -g $RG -n ticket-api --query "properties.template.containers[0].probes" -o jsonThe answer (live) is an empty list. With no probes, the platform only checks that something accepts connections on port 8000 (a TCP check). A process that is stuck, or a classifier that cannot classify, still passes that check.
“Serverless” does not mean “no configuration”. The platform manages the servers. It does not know what “working” means for your app, how much memory it needs, or which settings it must have. You tell it, in five parts:
| Part | Question it answers | The ticket API’s answer |
|---|---|---|
| Ingress | Who can reach the app, and on which port? | External, HTTPS, target port 8000 |
| Probes | How does the platform know that the app works? | /health for startup and liveness, /ready for readiness |
| Resources | How large is one replica? | 0.25 vCPU, 0.5 GiB |
| Scale | How many replicas, at least and at most? | 0 to 2 |
| Required settings | What must be set, or the app must not start? | API_KEY from the secret, REQUIRE_API_KEY=true, SHOW_DOCS=false |
Ingress: one way in
Section titled “Ingress: one way in”Ingress is the platform’s front door: the part that takes requests from outside and sends them to your container. --ingress external --target-port 8000 from the last lesson set it. Azure gives the app an HTTPS address with a certificate, and it sends each request to port 8000 in a container. The app itself speaks plain HTTP, as on your computer.
Plain http:// is not served. Amira checks it (live):
curl -s -o /dev/null -w "%{http_code} -> %{redirect_url}\n" http://ticket-api.nicewater-a307f95a.canadacentral.azurecontainerapps.io/health301 -> https://ticket-api.nicewater-a307f95a.canadacentral.azurecontainerapps.io/health301 is a permanent redirect to the HTTPS address. A key sent over plain HTTP could be read on the way, so the platform never answers there.
Three probes, two endpoints
Section titled “Three probes, two endpoints”A probe is a request that the platform sends to the app again and again, to decide what to do with a replica. Container Apps has three kinds:
| Probe | Question | If it fails | The ticket API |
|---|---|---|---|
| Startup | Has the app finished starting? | Wait. After too many failures, restart the replica. | /health, every 2 s, up to 30 times |
| Liveness | Is the process still working? | Restart the replica. | /health, every 10 s, 3 failures |
| Readiness | Can it do its job now? | Send it no requests, but do not restart it. | /ready, every 10 s, 3 failures |
Why /ready only for readiness? Remember Module 3: when the database is down, /ready gives 503, and classification still works. A restart cannot repair a database. If liveness used /ready, a database outage would restart the app again and again, and stop classification too. So liveness asks “is the process alive?” (/health), and readiness asks “can it work now?” (/ready).
The startup probe protects a slow start. It allows up to 30 × 2 s = 60 s. Liveness checks start only after the startup probe passes, so a slow start is not counted as a dead process.
The settings in one file
Section titled “The settings in one file”Amira does not set probes with many flags. She keeps every operational setting in one file, deploy/containerapp.yaml, and reviews it in Git like code:
# The container settings of the ticket API on Azure Container Apps.# Apply with:# az containerapp update --resource-group rg-ticket-api-practice \# --name ticket-api --yaml deploy/containerapp.yaml# Replace REGISTRY_NAME with the name of your registry first.# The secret api-key must already exist on the app (az containerapp secret set).properties: template: containers: - name: ticket-api image: REGISTRY_NAME.azurecr.io/ticket-api:1.1.0 resources: cpu: 0.25 memory: 0.5Gi env: - name: API_KEY secretRef: api-key - name: REQUIRE_API_KEY value: "true" - name: SHOW_DOCS value: "false" probes: - type: Startup httpGet: path: /health port: 8000 initialDelaySeconds: 1 periodSeconds: 2 failureThreshold: 30 - type: Liveness httpGet: path: /health port: 8000 periodSeconds: 10 failureThreshold: 3 - type: Readiness httpGet: path: /ready port: 8000 periodSeconds: 10 failureThreshold: 3 scale: minReplicas: 0 maxReplicas: 2The file holds no secret. secretRef: api-key names the app’s secret, and the value stays in Azure. You can commit this file.
Resources. On the Consumption plan, CPU and memory come in fixed pairs. 0.25 vCPU with 0.5 GiB is the smallest pair. The ticket API used about 57 MiB in the simulation, so 0.5 GiB leaves room.
Scale. A replica is one running copy of the app (see Cost awareness). minReplicas: 0 lets the app scale to zero: no replicas, and no compute charges, when nobody calls it for 300 s. maxReplicas: 2 is a limit on cost and on damage. If a program sends thousands of requests, the platform starts at most two replicas, not ten (the default maximum is 10).
Required settings. REQUIRE_API_KEY: "true" makes a missing key a failed start, not an open API. If the secret is missing, the app stops with Settings error: REQUIRE_API_KEY is true, but API_KEY is not set, the new revision never becomes ready, and the old one keeps serving. That is the safe failure.
Worked example: apply the file (live)
Section titled “Worked example: apply the file (live)”Amira writes her registry name into the image line, then applies the file:
sed -i '' "s/REGISTRY_NAME/$ACR/" deploy/containerapp.yamlaz containerapp update -g $RG -n ticket-api --yaml deploy/containerapp.yaml --query "{rev:properties.latestRevisionName, prov:properties.provisioningState}" -o table(Get-Content deploy/containerapp.yaml) -replace 'REGISTRY_NAME', $ACR | Set-Content deploy/containerapp.yamlaz containerapp update -g $RG -n ticket-api --yaml deploy/containerapp.yaml --query "{rev:properties.latestRevisionName, prov:properties.provisioningState}" -o tablesed -i "s/REGISTRY_NAME/$ACR/" deploy/containerapp.yamlaz containerapp update -g $RG -n ticket-api --yaml deploy/containerapp.yaml --query "{rev:properties.latestRevisionName, prov:properties.provisioningState}" -o tableThe registry name is not a secret, so it may be in the committed file. The output (live):
Rev Prov------------------- ---------ticket-api--0000001 SucceededA new revision, ticket-api--0000001. A revision is a fixed copy of the app’s image and settings. Every change to the template (the image, the settings, the probes) makes a new one. She checks that Azure has what the file says:
az containerapp show -g $RG -n ticket-api --query "properties.template.containers[0].{probes:probes[].type, env:env[].name}" -o json{ "env": ["API_KEY", "REQUIRE_API_KEY", "SHOW_DOCS"], "probes": ["Liveness", "Readiness", "Startup"]}Now the revisions, right after the update and 20 s later:
az containerapp revision list -g $RG -n ticket-api --query "[].{name:name, active:properties.active, replicas:properties.replicas, health:properties.healthState, traffic:properties.trafficWeight}" -o tableName Active Replicas Health Traffic------------------- -------- ---------- -------- ---------ticket-api--nz14cir True 1 Healthy 0ticket-api--0000001 True 1 None 100
Name Active Replicas Health Traffic------------------- -------- ---------- -------- ---------ticket-api--nz14cir True 1 Healthy 0ticket-api--0000001 True 1 Healthy 100The app is in single revision mode (az containerapp show … --query properties.configuration.activeRevisionsMode says Single). Only one revision gets the traffic. The platform starts the new one next to the old one, and removes the old one after the new one is ready. The lesson’s next failure shows what happens when “ready” never comes.
Worked example: the same ideas in the simulation
Section titled “Worked example: the same ideas in the simulation”The simulation has no probes and no revisions, but it has three of the five parts. Amira looks at them in the running container.
A health check. Docker runs the healthcheck command in the container every 10 s. It uses Python’s urllib, because the slim image has no curl:
docker inspect ticket-api-cloud-sim-api-1 --format "{{json .State.Health}}"{"Status":"healthy","FailingStreak":0,"Log":[{"Start":"2026-10-07T16:50:36.679051923Z","End":"2026-10-07T16:50:37.721244007Z","ExitCode":0,"Output":""}]}Docker’s health check is like a liveness probe that only reports: docker ps shows (unhealthy), but Docker does not restart the container for it. The checks show in the app’s log as requests from 127.0.0.1, because they run inside the container, like Azure’s probes.
The size limits.
docker stats --no-stream ticket-api-cloud-sim-api-1CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS5567fda8c533 ticket-api-cloud-sim-api-1 2.14% 56.92MiB / 512MiB 11.12% 7.96kB / 6.1kB 0B / 0B 6/ 512MiB is mem_limit: 512m. If the app used more, the kernel would stop it. cpus: 0.25 does not show here: docker inspect shows it as NanoCpus 250000000, a quarter of one CPU.
A restart policy, and a required setting. restart: on-failure restarts the container when the app stops with an error. Predict: if Amira stops the app cleanly, from inside, does Docker start it again?
docker exec ticket-api-cloud-sim-api-1 sh -c 'kill 1'docker ps -a --filter name=ticket-api-cloud-sim --format "{{.Names}} {{.Status}}"ticket-api-cloud-sim-api-1 Exited (0) 2 seconds agoNo. kill 1 sends the stop signal to process 1, the app. It shuts down cleanly, with exit code 0, and on-failure restarts only after an error. Now a real failure: she empties the key file, so the required setting is missing, and restarts the service. A Compose secret is mounted from the file, so the container sees the change.
cp secrets/api_key.txt secrets/api_key.txt.bak: > secrets/api_key.txtdocker compose -f deploy/local-cloud.yaml up -ddocker compose -f deploy/local-cloud.yaml logs --tail 3 apidocker ps -a --filter name=ticket-api-cloud-sim --format "{{.Status}}"The output (simulated; docker ps run a few times, a second apart):
api-1 |api-1 |api-1 | Settings error: REQUIRE_API_KEY is true, but API_KEY is not setRestarting (1) Less than a second agoUp Less than a second (health: starting)Up 2 seconds (health: starting)Restarting (1) Less than a second agoThe app stops with exit code 1, and Docker starts it again, and again. docker inspect ticket-api-cloud-sim-api-1 --format "{{.RestartCount}}" counts the restarts. In two test runs, it grew by one every 4 to 10 s: with a quarter of a CPU, the app needs a few seconds to reach the settings check. The API never answers without a key: nobody gets an open API. She puts the key back:
cp secrets/api_key.txt.bak secrets/api_key.txtrm secrets/api_key.txt.bakAt the next restart, the app finds the key. About 25 s later, docker ps said Up 34 seconds (healthy), with no command from her. This is a small version of what Azure does with a revision that cannot start. There is one difference: Docker has only one container, so the API is down while it restarts. Azure keeps the old revision serving.
Guided practice: give the platform its instructions
Section titled “Guided practice: give the platform its instructions”- Read
deploy/containerapp.yaml. For each probe, write indocs/cloud-design.mdits path and what the platform does when it fails. - Live route: put your registry name in the file, and apply it with
az containerapp update --yaml. Expected: a new revision,ticket-api--0000001, andSucceeded. - Live route: list the revisions until the new one is
Healthywith traffic100. - Live route: check the probes and the setting names with
az containerapp show. Expected:Liveness,Readiness,Startup, andAPI_KEY,REQUIRE_API_KEY,SHOW_DOCS. - Simulated route: check the health status with
docker inspect … --format "{{json .State.Health}}". Expected:"Status":"healthy". - Simulated route: check the memory limit with
docker stats --no-stream. Expected:/ 512MiB. - Simulated route: empty the key file and watch the restarts, as in the worked example. Then put the key back, and wait for
(healthy). - Both routes: check that
/docsgives404, becauseSHOW_DOCSisfalse.
Show the expected notes for step 1
| Probe | Path | When it fails |
|---|---|---|
| Startup | /health, every 2 s |
The platform waits, up to 30 failures (60 s). Then it restarts the replica. Liveness and readiness wait for it. |
| Liveness | /health, every 10 s |
After 3 failures, the platform restarts the replica. |
| Readiness | /ready, every 10 s |
After 3 failures, the platform sends the replica no requests, but it does not restart it. |
/ready is not a liveness check, because it fails when the database is down, and a restart cannot repair a database.
Your turn: docker kill and the restart policy
Section titled “Your turn: docker kill and the restart policy”The worked example stopped the app from inside. Now stop it from outside, with docker kill ticket-api-cloud-sim-api-1. docker kill ends the process at once, and the exit code is not 0.
Predict: does restart: on-failure start the container again? Run it, then check with docker ps -a. Afterwards, start the simulation again with docker compose -f deploy/local-cloud.yaml up -d.
Show the answer
No. The output (simulated):
ticket-api-cloud-sim-api-1 Exited (137) 3 seconds agoExit code 137 means “ended by the signal SIGKILL” (128 + 9). It is a failure code, but Docker does not restart the container. A stop that you ask for, with docker stop or docker kill, is a manual stop, and Docker never restarts a container after a manual stop. A restart policy is for failures that nobody asked for. Azure Container Apps is different: a replica that stops is always started again, and the liveness probe can also restart a replica that runs but does not answer.
Common mistake
The deployment succeeded, but the new version never runs
Text description of the diagram
In single revision mode, a new revision gets the traffic only when it is ready. Revision 0000002 had an image for the wrong CPU, so its image pull failed again and again, and it stayed Activating. All this time, revision 0000001 served the requests. When Amira deployed the correct image, revision 0000003 started, became ready and took the traffic, and the platform removed 0000001.
Symptom (live): Amira built an image only for linux/arm64, with the tag 1.1.0-arm64, and deployed it. The command said Succeeded:
docker buildx build --platform linux/arm64 -t $ACR.azurecr.io/ticket-api:1.1.0-arm64 --push .az containerapp update -g $RG -n ticket-api --image $ACR.azurecr.io/ticket-api:1.1.0-arm64 --query "{rev:properties.latestRevisionName, prov:properties.provisioningState}" -o tableRev Prov------------------- ---------ticket-api--0000002 SucceededBut 60 s later, the new revision was still not running, and the old one had the traffic:
Name Active Replicas Health Running Traffic------------------- -------- ---------- -------- ---------- ---------ticket-api--0000001 True 1 Healthy Running 0ticket-api--0000002 True 1 None Activating 100/health still gave 200: the old revision answered. (The Traffic column shows the target, 100 for the new revision. The requests still went to the old one, because the new one was not ready.)
Diagnosis: the system logs. Use --type system without --format text (with it, the command says ERROR: --type: only json logs supported for system logs):
az containerapp logs show -g $RG -n ticket-api --type system --tail 15The output (live; one JSON object per line, shown here as time, Reason and Msg):
2026-10-07 16:18:24 PullingImage | Pulling image 'ticketapi4821.azurecr.io/ticket-api:1.1.0-arm64'2026-10-07 16:18:34 ImagePullUnauthorized | Pull image: ticketapi4821.azurecr.io/ticket-api:1.1.0-arm64 failed with unauthorized exception.2026-10-07 16:18:34 ContainerTerminated | Container 'ticket-api' was terminated with exit code '' and reason 'ImagePullFailure'2026-10-07 16:18:54 PullingImage | Pulling image 'ticketapi4821.azurecr.io/ticket-api:1.1.0-arm64'2026-10-07 16:19:03 ImagePullUnauthorized | Pull image: ticketapi4821.azurecr.io/ticket-api:1.1.0-arm64 failed with unauthorized exception.2026-10-07 16:19:03 ContainerTerminated | Container 'ticket-api' was terminated with exit code '' and reason 'ImagePullFailure'Cause: the message says “unauthorized”, but the identity and its role were correct: the tag 1.1.0 from the same repository pulled without a problem. The image had no linux/amd64 variant. Azure reported the missing platform as a failed pull. The simulated route shows the real cause, in clear words, with the same image:
docker pull --platform linux/amd64 127.0.0.1:5000/ticket-api:1.1.0-arm64Error response from daemon: no matching manifest for linux/amd64 in the manifest list entries: no match for platform in manifest: not foundFix:
- Before you deploy, check the platform:
docker buildx imagetools inspect $ACR.azurecr.io/ticket-api:<tag>must showlinux/amd64. - Deploy the last good image again:
az containerapp update -g $RG -n ticket-api --image $ACR.azurecr.io/ticket-api:1.1.0 --query properties.latestRevisionName -o tsv. It printedticket-api--0000003. - List the revisions until the new one is
Healthywith traffic100. After 45 s:
Name Active Replicas Health Running Traffic------------------- -------- ---------- -------- -------------- ---------ticket-api--0000001 True 1 Healthy Deprovisioning 0ticket-api--0000003 True 1 Healthy Running 100For a real AI service: “the command succeeded” only means that the platform accepted your request. A model server with a large image, or a model file that fails to load, fails the same way: after the command, during the start. Always check the revision’s health and run the smoke checks after a deployment. And when a pull error makes no sense, check the image’s platform first.
Check your understanding
Why does the ticket API use /health for liveness and /ready for readiness, and not /ready for both?
Show the answer
A failed liveness probe restarts the replica. A failed readiness probe only stops the requests to it. /ready fails when a dependency, such as the history database, is down. A restart cannot repair the database. With /ready as liveness, a database outage would restart the app again and again. Classification, which still works, would stop too. /health fails only when the process itself does not answer, and then a restart can help.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
Predict: you deploy a new revision, but you forgot to create the secret api-key. REQUIRE_API_KEY is true. What do the users of the API see?
Show the answer
They see no change: the old revision keeps serving. The new revision cannot start with a missing key: the app stops with Settings error: REQUIRE_API_KEY is true, but API_KEY is not set, the startup probe never passes, and the platform never moves the traffic to it. Without REQUIRE_API_KEY=true, the new revision would start without a key, and the API would be open to everybody. A required setting turns a dangerous mistake into a failed deployment. (In the test, a missing secret was not tried live. The simulation shows the same settings error and the restarts.)
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
The help desk lead says: it is serverless, so it costs nothing and needs no settings. Give two settings from deploy/containerapp.yaml that show why both parts are wrong.
Show the answer
minReplicas: 0 is a setting, and it is what makes the compute cost zero when nobody calls the API. With minReplicas: 1, a replica runs (and charges) all month. maxReplicas: 2 limits the cost when many requests arrive: the default allows 10 replicas. The probes, the size (cpu: 0.25, memory: 0.5Gi) and REQUIRE_API_KEY are also settings that the platform cannot guess. Serverless means that someone else manages the servers. You still decide the cost limits, the checks and the required settings. And the registry and the logs charge even when the app has no replicas.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Recap
- Serverless still needs configuration: ingress, probes, resources, scale limits and required settings.
- Ingress gives one HTTPS address. Plain HTTP gets
301to HTTPS. - Startup and liveness use
/health. Readiness uses/ready, so a database outage stops traffic but never restarts the app. - 0.25 vCPU with 0.5 GiB is the smallest pair.
minReplicas: 0lets it scale to zero.maxReplicas: 2limits cost and damage. deploy/containerapp.yamlkeeps these settings in Git, with no secret in it.az containerapp update --yamlapplies it and makes a new revision.- In single revision mode, a new revision gets the traffic only when it is ready. “Succeeded” is not proof: check the revision and the system logs.
- The simulation shows the same ideas: a health check, the limits in
docker stats, and a restart policy that restarts after a failure, not after a manual stop.
The next lesson proves that the deployment works, reads its logs, and then deletes everything with a checked inventory.
Next: Verify and remove