Containers on App Service: Env Vars and Secrets
Run a container on Azure App Service the right way. Image pull with managed identity, app settings, Key Vault references, deployment slots, and when App Service beats Container Apps for AI workloads.
When App Service is the right fit
Azure App Service is the most managed way to run a container that serves HTTP traffic. You hand Azure a container image, it runs the container behind a load balancer, gives you HTTPS, deployment slots, autoscaling, and a domain β done.
Compared to Container Apps, App Service is older and simpler: one container per app, traditional autoscale on CPU/memory or custom rules, no scale-to-zero on Linux container plans. Compared to AKS, it hides Kubernetes entirely.
For AI-200: the exam tests how App Service supplies environment variables and secrets to your container, and how it pulls images from ACR without passwords using managed identity.
App settings β how App Service supplies environment variables
In App Service, app settings become environment variables inside your container at startup. Set them via portal, CLI, or ARM/Bicep:
az webapp config appsettings set \
--name roo-inference \
--resource-group roo-prod \
--settings \
LOG_LEVEL=info \
MODEL_NAME=phi-4-mini \
INFERENCE_BATCH_SIZE=8 \
WEBSITES_PORT=8000
Inside the container, your code reads them with os.environ.get('MODEL_NAME') (Python), process.env.MODEL_NAME (Node.js), and so on.
| Setting | Purpose |
|---|---|
WEBSITES_PORT | The port your container listens on (App Service will route traffic to it) |
WEBSITES_ENABLE_APP_SERVICE_STORAGE | Whether to mount the App Service file system into the container |
DOCKER_REGISTRY_SERVER_URL | Custom registry URL (when not using managed identity for ACR) |
| Anything you define | Your appβs configuration |
Watch out: app settings are not encrypted at rest by default
App Service app settings are stored as part of the site configuration and are encrypted at rest, but theyβre visible to anyone with Contributor on the App Service resource. Do not put long-lived passwords or API keys directly in app settings.
For secrets, use Key Vault references (next section) β the secret stays in Key Vault, App Service looks it up at startup, and the value never appears in app settings.
Secrets β Key Vault references
The recommended pattern is: secrets live in Key Vault; App Service references them by URI; the container sees only the resolved value at startup.
# Reference syntax in app settings:
DB_CONNECTION_STRING=@Microsoft.KeyVault(SecretUri=https://roo-kv.vault.azure.net/secrets/DbConnString)
OPENAI_API_KEY=@Microsoft.KeyVault(SecretUri=https://roo-kv.vault.azure.net/secrets/OpenAIKey)
Three things must be true for a Key Vault reference to resolve:
- App Service has a system-assigned or user-assigned managed identity enabled
- That identity has Get permission on the secret (Key Vault RBAC:
Key Vault Secrets User) - App Service can reach Key Vault on the network (public, private endpoint, or VNet integration with allowed IP)
If any condition fails, the app setting resolves to the literal reference string β your code starts up, reads OPENAI_API_KEY=@Microsoft.KeyVault(...), and authentication to OpenAI fails. Always test with az webapp config appsettings list to confirm Microsoft.KeyVault.SecretReferenceState=Resolved.
| Feature | Direct app setting | Key Vault reference | Read from Key Vault in code |
|---|---|---|---|
| Where the secret lives | App Service config | Key Vault | Key Vault |
| Rotation | Manual: edit setting + restart | Edit Key Vault β restart app | Edit Key Vault β no restart needed if you re-fetch |
| Visible in portal config | Yes (resolved value) | Yes (reference URI), value masked | No (URI only if you store one) |
| Audit trail | App Service activity log | Key Vault access policy logs + App Service logs | Key Vault logs every read |
| Best for | Non-secret config | Most production secrets | Highly sensitive secrets, frequent rotation, per-request retrieval |
Pulling the image from ACR β use managed identity
Older docs show DOCKER_REGISTRY_SERVER_USERNAME / DOCKER_REGISTRY_SERVER_PASSWORD for ACR auth. Donβt use them. The modern pattern is:
# 1. Enable system-assigned managed identity on the web app
az webapp identity assign --name roo-inference --resource-group roo-prod
# 2. Grant the identity AcrPull on the registry
PRINCIPAL_ID=$(az webapp identity show -n roo-inference -g roo-prod --query principalId -o tsv)
az role assignment create \
--assignee $PRINCIPAL_ID \
--role AcrPull \
--scope $(az acr show -n roo --query id -o tsv)
# 3. Configure App Service to use managed identity for the pull
az webapp config set \
--name roo-inference \
--resource-group roo-prod \
--generic-configurations '{"acrUseManagedIdentityCreds": true}'
# 4. Set the image
az webapp config container set \
--name roo-inference \
--resource-group roo-prod \
--container-image-name roo.azurecr.io/roo-vision:v3.4.1
No password ever leaves Azure. The web app pulls the image as itself.
Deployment slots β zero-downtime updates
Slots are independent App Service environments that share the same compute plan. The classic pattern:
# Create a staging slot
az webapp deployment slot create -n roo-inference -g roo-prod --slot staging
# Deploy a new image to staging
az webapp config container set -n roo-inference -g roo-prod \
--slot staging --container-image-name roo.azurecr.io/roo-vision:v3.5.0
# Smoke test against the staging URL: https://roo-inference-staging.azurewebsites.net
# Swap β production now points to the new image, staging holds the old one
az webapp deployment slot swap -n roo-inference -g roo-prod \
--slot staging --target-slot production
# If something's wrong β swap back. Old image is right there in staging.
az webapp deployment slot swap -n roo-inference -g roo-prod \
--slot staging --target-slot production
Slot-specific app settings (Slot setting checkbox) stay with the slot during a swap β useful when staging and production hit different OpenAI endpoints or Key Vault references.
Real-world example: Priya's blue-green for the menu Q&A bot
BeanCraft Coffee deploys to App Service three times a week. Priyaβs pipeline:
- Build image in ACR Tasks β tag
:v2.<run-id> - Update staging slot to the new tag
- Run smoke tests (synthetic shoppers asking 20 standard questions)
- Swap staging β production
- Watch error rate for 10 minutes; if it spikes, swap back
Total downtime per deploy: zero. Roll-back time: under 30 seconds. App Service slots make this trivial; doing the same on a single VM is a weekend project.
Sidecar containers (Linux only)
For Linux container apps, App Service supports running a small set of additional containers alongside your main image. Common uses:
| Sidecar | Why |
|---|---|
| OpenTelemetry collector | Receive traces from your main app on localhost, batch-export to Application Insights |
| Local model proxy (e.g., Phi-4 ONNX runtime) | Run a small model in-process for low-latency inference, separate from the main API |
| Authentication proxy | OAuth2 proxy in front of an internal API |
Sidecars share the App Serviceβs network namespace β your main app reaches them on localhost. They share the App Serviceβs identity for outbound calls.
Key terms
Knowledge check
Mira's container reads its OpenAI API key from environment variable `OPENAI_API_KEY`. The security team wants the actual key stored only in Key Vault, with rotation visible in audit logs. Which approach satisfies both β the container still reads `OPENAI_API_KEY` AND the secret stays in Key Vault?
Theo deploys a new version of the clinical AI container to App Service. He wants to validate it against real-world traffic before exposing it to clinicians, and to roll back instantly if anything breaks. Which feature should he use?
Lin enabled a system-assigned managed identity on a web app and configured the image to pull from ACR. Pulls still fail. Which step did Lin most likely miss?