• English icon
  • German icon
Explainer

Azure Container Apps explained: environments, revisions, secrets

· Series: Containers on Azure

azureazure-container-appscontainersrevisionsdevops

You ship a new image to Azure Container Apps, and for a moment two versions of the app run side by side: the old one keeps every request until the new one is ready. Then you rotate an API key, and the app keeps using the old one. Both come from the same idea, the revision.

This explainer walks through a full deployment: what a Container Apps environment is, how to deploy with the CLI and with YAML, how settings and secrets reach the container, how it pulls from a private registry without a password, and how to check that a deployment really works. One picture helps: the environment is a city, every container app is a metro line, and every revision is a train, built once from one image and one set of settings and never changed.

Key points

  • The environment is the boundary. Apps in one environment share a virtual network and, by default, one Log Analytics workspace. Use separate environments for development, test and production.
  • Every template change builds a new revision. In single revision mode (the default), the old revision keeps all the traffic until the new one is provisioned, scaled up and has passed its health probes.
  • Secrets are application-scope. Changing a secret builds no new revision; the running containers keep the value they read at start until the revision restarts.
  • Pull with a managed identity, check from the outside in. AcrPull on Azure Container Registry, no password; then show, logs, revisions and replicas.

The environment

An environment is the boundary your container apps live in. Apps in the same environment share one virtual network and send their logs to the same place, by default one Log Analytics workspace. That makes it the natural home for services that belong together, such as an API and the background worker that feeds it. It is also how you separate stages: development, test and production each get their own environment, so a broken test revision never touches production.

Inside the environment, every app decides who may reach it:

IngressWho can call the app
Externalcallers outside the environment (the internet, for a public environment) and apps inside it
Internalonly other apps in the same environment; outside callers get a 404

Either way, the container listens on its target port, and Container Apps serves it over HTTPS on port 443.

az containerapp env create \
--name aca-env-demo \
--resource-group rg-aca-demo \
--location centralus

New environments use workload profiles by default, with a Consumption profile that scales to zero: you pay for what your apps use.

Deploy and update

The fastest first deployment is az containerapp up: one command can create the environment and everything around it, deploy the image and print the app's address. Great for a first try. For real environments, create the environment first and deploy into it with az containerapp create: the same image, port and ingress, but nothing is created behind your back.

az containerapp create -n ai-api -g rg-aca-demo \
--environment aca-env-demo \
--image mcr.microsoft.com/k8se/quickstart:latest \
--target-port 80 --ingress external

Either way, Container Apps builds the first revision: a snapshot of the app with its image, settings and scale rules. A revision never changes. Shipping version two creates a new one next to the running one:

az containerapp update -n ai-api -g rg-aca-demo \
--image myregistry.azurecr.io/ai-api:v2 \
--revision-suffix v2

Single revision mode (the default): the old revision keeps all the traffic while the new one starts, scales up and passes its health probes. Only then does the traffic switch, and the old revision is shut down. If the new revision never gets ready, nothing switches and users stay on the old version. Multiple revision mode lets you decide yourself and split the traffic, for example 90 % to 10 %.

YAML for teams: keep the whole app in a YAML file in source control and review changes like code. With --yaml, the file is the full truth: the CLI ignores every other flag on that command.

properties:
configuration:
ingress: { external: true, targetPort: 8000 }
template:
containers:
- name: ai-api
image: myregistry.azurecr.io/ai-api:v2

Settings and secrets

The same image should run in test and in production; what differs is configuration. Non-sensitive settings such as a log level or a feature flag go into environment variables. --set-env-vars adds or changes one and keeps the rest.

Sensitive values such as an API key become secrets. A secret belongs to the app, not to a revision, and is stored encrypted. An environment variable points at it with secretref: followed by the secret's name, and the name must match exactly, including its case:

az containerapp secret set -n ai-api -g rg-aca-demo \
--secrets embeddings-api-key=<value>
az containerapp update -n ai-api -g rg-aca-demo \
--set-env-vars \
EMBEDDINGS_API_KEY=secretref:embeddings-api-key

In YAML it's secretRef, and the file only holds the name, never the value:

properties:
template:
containers:
- name: ai-api
env:
- name: LOG_LEVEL
value: info
- name: EMBEDDINGS_API_KEY
secretRef: embeddings-api-key

The code doesn't know any of this. It just reads an environment variable:

import os
key = os.environ["EMBEDDINGS_API_KEY"]

Two parts of a container app. This is the idea behind the opening question:

PartContainsA change …
Template (properties.template)image, environment variables, scale rulesbuilds a new revision
Configuration (properties.configuration)secrets, ingress, registries, revision modeapplies to every revision, builds none

So when you update a secret, no new revision appears. The running containers read their secrets when they started, so they keep the old value. To pick up the new one, restart the revision or deploy a new one:

az containerapp revision restart -n ai-api \
-g rg-aca-demo --revision ai-api--v2

If the secret lives in Azure Key Vault, reference it through a managed identity instead. Point at the secret without a version, and Container Apps fetches a new version within 30 minutes and restarts the active revisions that use it.

Pull from a private registry

Production images usually live in a private registry such as Azure Container Registry, so the app needs permission to pull. A username and a password work with almost any registry, but they are one more secret to store and rotate, in every app that pulls. With Azure Container Registry, a managed identity is the better way: the app gets its own identity in Microsoft Entra ID, the registry trusts that identity, and no password exists anywhere.

az containerapp create -n ai-api -g rg-aca-demo \
--environment aca-env-demo \
--image myregistry.azurecr.io/ai-api:v1 \
--registry-server myregistry.azurecr.io \
--registry-identity system \
--ingress external --target-port 8000

The CLI uses the app's identity and, where it can, assigns the AcrPull role on the registry. AcrPull is the least it needs: it can pull images, never push them. When several apps pull from the same registry, Microsoft recommends a user-assigned identity: it exists before the app, and many apps can share it.

Tags: Container Apps checks the registry for the image every time a container starts. If a tag like latest moves, a restart or a scale-out can quietly run different code, so use a unique tag for every release. After any registry change, deploy a revision and watch it start before you trust it.

Check that it works

A command that finished without an error is not a working app. Check from the outside in:

  1. The app. az containerapp show returns the configuration. Query the ingress for the address, check that it's external if you expect callers from outside, then call a health endpoint.
  2. Console logs are what the container writes: a missing variable, a crash, a wrong port. By default, logs show prints the last 20 lines from one replica of the latest revision; --follow streams them live.
  3. System logs (--type system) are what the platform writes: image pulls, probes, restarts. When the container never even started, look there.
  4. Revisions. revision list shows which revisions are active and their running state: running, activating, degraded or failed. After every update a new one should appear.
  5. Replicas are the running copies of a revision. An empty list doesn't always mean a crash: by default an app scales between zero and ten replicas, so with no traffic it simply has none. The next request starts one and waits for it; if that cold start hurts, set the minimum replicas to one.
az containerapp show -n ai-api -g rg-aca-demo \
--query properties.configuration.ingress.fqdn
az containerapp logs show -n ai-api -g rg-aca-demo \
--follow --tail 30
az containerapp logs show -n ai-api -g rg-aca-demo \
--type system
az containerapp revision list -n ai-api -g rg-aca-demo
az containerapp replica list -n ai-api -g rg-aca-demo

Five quick fixes

SymptomFix
You rotated a secret and nothing changedrestart the revision
The app works from the inside, not from the internetits ingress is internal
The revision never gets readythe target port doesn't match the port the container listens on
A flag had no effectyou also passed a YAML file, and the file wins
The same tag runs different code after a restartgive every release its own tag

Further reading

Video transcript

You ship a new image to Azure Container Apps. For a moment, two versions of your app run side by side, and the old one keeps every request, until the new one is ready. Then you rotate an API key, and your app keeps using the old one. Both come from the same idea. Once you see it, the rest of Container Apps makes sense.

In this video, you will see what a Container Apps environment is, how to deploy with the CLI and with YAML, how settings and secrets reach your container, how it pulls from a private registry without a password, and how to check that a deployment really works. Picture a city with a metro. The environment is the city. Every container app is a line. And every revision is a train: built once, from one image and one set of settings, and never changed.

So what is an environment? It's the boundary your container apps live in. Apps in the same environment share one virtual network, and they send their logs to the same place, by default one Log Analytics workspace. That makes it the natural home for services that belong together, like an API and the background worker that feeds it. It is also how you separate stages. Development, test and production each get their own environment, so a broken test revision never touches production. Inside the city, every app decides who may reach it. External ingress opens it to the outside: the internet, for a public environment. Internal ingress keeps it inside: only other apps in the same environment can call it. Either way, your container listens on its target port, and Container Apps serves it over HTTPS, on port 443. You can create the environment yourself, with az containerapp env create, a name, a resource group and a region. New environments use workload profiles by default, with a Consumption profile that scales to zero, and you pay for what your apps use.

Now the first deployment. The fastest way is az containerapp up. One command can create the environment and everything around it, deploy the image, and print the app's address. Great for a first try. For real environments, you usually create the environment first, and then deploy into it with az containerapp create. The same image, port and ingress, but nothing is created behind your back. Either way, Container Apps builds the first revision. A revision is a snapshot of your app: the image, its settings, its scale rules. And it never changes. So what happens when you ship version two? az containerapp update with the new image doesn't touch the running revision. It builds a new one, next to it. By default, an app runs in single revision mode. The old revision keeps all the traffic, while the new one starts, scales up, and passes its health probes. Only then does the traffic switch, and the old revision is shut down. If the new revision never gets ready, nothing switches. Your users stay on version one. In multiple revision mode, you decide yourself, and you can split the traffic, for example ninety percent to ten. Flags on the command line are fine for a test. For a team, keep the whole app in a YAML file, in source control, and review changes like code. With the yaml option, the file is the full truth: the CLI ignores every other flag on that command.

The same image should run in test and in production. What differs is configuration. Settings that aren't sensitive, like a log level or a feature flag, go into environment variables. Set env vars adds or changes one, and keeps the rest. Sensitive values, like an API key, become secrets. A secret belongs to the app, not to a revision, and it is stored encrypted. To use it, an environment variable points at it with secretref, followed by the secret's name. The name must match exactly, including its case. In YAML, it's secretRef. The file only holds the name, never the value. Your code doesn't know any of this. It just reads an environment variable. And here is the idea from the start. A container app has two parts. The template is the train: the image, the environment variables, the scale rules. Change anything in it, and you get a new revision. The configuration is the line: secrets, ingress, registries, the revision mode. Change it, and every revision sees the change, but no new revision is built. So you update a secret. No new revision appears. The running containers read their secrets when they started, so they keep the old value. To pick up the new one, restart the revision, or deploy a new one. If the secret lives in Azure Key Vault, reference it instead, through a managed identity. Point at the secret without a version, and Container Apps fetches a new version within thirty minutes, and restarts the active revisions that use it.

Production images usually live in a private registry, like Azure Container Registry. So the app needs permission to pull. The simple way is a username and a password. It works with almost any registry, but it's one more secret to store and rotate, in every app that pulls. With Azure Container Registry, there is a better way: a managed identity. The app gets its own identity in Microsoft Entra ID, and the registry trusts that identity. No password exists anywhere. On create, pass the registry server and registry identity system. The CLI uses the app's identity, and where it can, it assigns the AcrPull role on the registry for you. AcrPull is the least it needs: it can pull images, but never push them. When several apps pull from the same registry, Microsoft recommends a user-assigned identity: it exists before the app, and many apps can share it. One more detail. Container Apps checks the registry for the image every time a container starts. So if a tag like latest moves, a restart or a scale-out can quietly run different code. Use a unique tag for every release. And after any registry change, deploy a revision and watch it start, before you trust it.

The command finished without an error. Is the app actually working? Start with the app itself. az containerapp show returns its configuration. Query the ingress for the address, and check that it's external if you expect callers from outside. Then call a health endpoint. If the call fails, read the logs. Console logs are what your container writes: a missing variable, a crash, a wrong port. By default, logs show prints the last twenty lines, from one replica of the latest revision. Follow streams them live. System logs are what the platform writes: image pulls, probes, restarts. When the container never even started, look there. Next, the revisions. Revision list shows which revisions are active, and their running state: running, activating, degraded, or failed. After every update, a new one should appear. Finally, the replicas: the running copies of a revision. An empty list doesn't always mean a crash. By default, an app scales between zero and ten replicas, so with no traffic, it simply has none. The next request starts one, and has to wait for it. If that cold start hurts, set the minimum replicas to one.

Five mistakes that cost people hours. One: you rotated a secret, and nothing changed. Restart the revision. Two: the app works from the inside, but not from the internet. Its ingress is internal. Three: the revision never gets ready. The target port doesn't match the port your container listens on. Four: a flag you passed had no effect. You also passed a YAML file, and the file wins. Five: the same tag runs different code after a restart. Give every release its own tag.

To sum it up. The environment is the boundary: one network, one place for logs. Every change to the template builds a new revision, and traffic moves only when it's ready. Settings go into environment variables, sensitive values into secrets, and a changed secret needs a restart. A managed identity with AcrPull pulls your images without a password. And show, logs, revisions and replicas tell you whether it really works.

New image, new train. New secret, restart the train. If this was useful, subscribe for more practical .NET and Azure.

Need this in your own Azure environment?

I build, migrate and run .NET platforms on Azure, with Azure DevOps and Terraform. If your team wants this set up properly, see what I offer.

See services