Azure Container Apps has quietly become Microsoft’s answer to the “I just want to deploy a container without thinking about servers” problem. It sits somewhere between Azure Functions and full-blown Azure Kubernetes Service, wrapping KEDA and Dapr under the hood so you get autoscaling, scale-to-zero, and microservice plumbing without writing a single line of YAML for a Kubernetes cluster you’ll never see. As of September 2026, the platform has matured well past its early “Cloud Run knockoff” reputation, with Confidential Compute now generally available, new regions live in Germany, Korea, and Chile, and a Dedicated GPU workload profile for teams running inference workloads next to their app tier.
This tutorial walks through deploying a real containerized API to Azure Container Apps (ACA) from a cold start: no existing Azure resources, no prior ACA experience assumed beyond basic Docker familiarity. By the end, you will have a working, autoscaling, HTTPS-secured container app with a custom scale rule, environment variables pulled from a secret store, and a monitoring dashboard, plus a clear picture of when ACA is the right call versus AWS Fargate, Google Cloud Run, or a full Kubernetes cluster.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
What Azure Container Apps Actually Is (and Isn’t)
Azure Container Apps is a serverless container hosting service built on top of Kubernetes primitives that Microsoft manages entirely on your behalf. You never see a node, a kubelet, or a control plane. Instead, you define a Container Apps Environment, deploy one or more container apps into it, and Azure handles scheduling, networking, and scaling using KEDA (Kubernetes Event-Driven Autoscaling) and Dapr (Distributed Application Runtime) as built-in platform components rather than add-ons you install yourself.
If you’ve already worked through our Google Cloud Run setup tutorial, most of ACA’s conceptual model will feel familiar, container in, HTTPS endpoint out, scale on demand, even though the underlying implementation and pricing differ meaningfully. The distinction that matters most in practice is between the two workload profile types. Consumption profiles bill per-second for active vCPU and memory, support 0.25 to 4 vCPU and 0.5 to 8 GiB of memory per replica, and are available in every ACA region. Dedicated (v2) profiles run on reserved VM-based node pools, including D-series general purpose nodes (4 to 32 vCPU), E-series memory-optimized nodes, and NC-series nodes with A100 GPUs for teams that need predictable performance or GPU access rather than pay-per-second billing. Most teams start on Consumption and only move workloads to Dedicated when they need VNET-injected networking at scale or GPU-backed inference alongside their app tier.
Where ACA differs from Azure Kubernetes Service (AKS) is scope. AKS gives you the full Kubernetes API surface, custom operators, and CRDs. ACA deliberately hides that complexity: you cannot install arbitrary Kubernetes operators, and you don’t get direct access to the underlying cluster. In exchange, you skip cluster upgrades, node patching, and control-plane billing entirely. If your team is asking “do we actually need raw Kubernetes, or do we just need our containers to run and scale,” ACA is built for the second answer.
Prerequisites and Versions
Confirm each of these before starting. Version mismatches are the single most common cause of failed deployments in this tutorial.
| Requirement | Minimum Version / Spec | Notes |
|---|---|---|
| Azure CLI | 2.65 or newer | Older CLI builds miss the containerapp extension update path |
| Azure Container Apps CLI extension | Latest via az extension add | Installed automatically in Step 2 |
| Docker Engine | 25.x or newer | Needed to build and test the container image locally |
| Azure subscription | Pay-As-You-Go or Free Trial with quota | Free tier grant applies per subscription, not per app |
| Node.js (for the sample app) | 20 LTS or newer | Any language works; this tutorial uses a small Node/Express API |
| Registry access | Azure Container Registry (ACR) or Docker Hub | ACR is used in this walkthrough for private image storage |
Budget roughly 90 minutes for the full walkthrough, including image build time and waiting for the environment to provision, which typically takes 3 to 5 minutes on its own.
Step 1: Install and Update the Azure CLI
Start by confirming your CLI version and logging in. If you’re on Linux or WSL, the install script handles everything; macOS users should use Homebrew.
# Check current version
az version
# Linux (Debian/Ubuntu)
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
# macOS
brew update && brew install azure-cli
# Log in (opens a browser window)
az login
# Confirm your active subscription
az account show --output table
If you manage multiple subscriptions, set the correct one explicitly before continuing, since ACA’s free monthly grant of 180,000 vCPU-seconds, 360,000 GiB-seconds, and 2 million HTTP requests applies per subscription, not per resource group.
az account set --subscription "Your-Subscription-Name-Or-ID"
Step 2: Install the Container Apps Extension and Register Providers
ACA relies on two resource providers that are not enabled by default on fresh subscriptions: Microsoft.App and Microsoft.OperationalInsights (for the Log Analytics workspace ACA uses for logging).
az extension add --name containerapp --upgrade
az provider register --namespace Microsoft.App --wait
az provider register --namespace Microsoft.OperationalInsights --wait
# Verify both show "Registered"
az provider show --namespace Microsoft.App --query "registrationState"
az provider show --namespace Microsoft.OperationalInsights --query "registrationState"
Provider registration can take a minute or two on a brand-new subscription. Don’t skip the --wait flag, or the next step will fail with an unregistered-provider error that’s confusing if you don’t know to look for it.
Step 3: Create a Resource Group and Container Apps Environment
A Container Apps Environment is the logical boundary that groups your apps for shared networking, logging, and billing. Apps in the same environment can talk to each other over internal DNS without leaving the environment’s virtual network.
RESOURCE_GROUP="aca-tutorial-rg"
LOCATION="eastus"
ENVIRONMENT="aca-tutorial-env"
az group create \
--name $RESOURCE_GROUP \
--location $LOCATION
az containerapp env create \
--name $ENVIRONMENT \
--resource-group $RESOURCE_GROUP \
--location $LOCATION
This single command provisions a Log Analytics workspace behind the scenes and wires it to your environment automatically, so you get container logs and metrics without configuring a separate observability stack. As of mid-2026, ACA is available in an expanded region list that now includes Germany North, Korea South, Chile Central, Belgium Central, New Zealand North, and Jio India Central, on top of the original core regions, so pick whichever is closest to your users.
Step 4: Build and Push a Container Image to Azure Container Registry
For this tutorial, use a minimal Express API with a health check and a deliberately slow endpoint you’ll use later to test autoscaling.
// server.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 8080;
app.get('/', (req, res) => {
res.json({ message: 'Hello from Azure Container Apps', region: process.env.REGION || 'unset' });
});
app.get('/health', (req, res) => res.status(200).send('OK'));
app.get('/slow', async (req, res) => {
await new Promise(r => setTimeout(r, 2000));
res.json({ message: 'Finished after 2 seconds' });
});
app.listen(PORT, () => console.log(`Listening on port ${PORT}`));
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 8080
CMD ["node", "server.js"]
Now create a registry and push the image. Azure Container Registry integrates natively with ACA’s managed identity, so you avoid handling registry credentials manually.
ACR_NAME="acatutorialacr$RANDOM"
az acr create \
--resource-group $RESOURCE_GROUP \
--name $ACR_NAME \
--sku Basic \
--admin-enabled true
az acr build \
--registry $ACR_NAME \
--image aca-demo-api:v1 .
Using az acr build instead of a local docker build + docker push pair means the build happens inside Azure, so you don’t need Docker running locally at all if you’d rather not. It’s also noticeably faster on slow upload connections since only your source, not the built image, travels over your network.
Step 5: Deploy Your First Container App
With the environment and image ready, deploy the app itself.
ACR_SERVER=$(az acr show --name $ACR_NAME --query loginServer --output tsv)
ACR_PASSWORD=$(az acr credential show --name $ACR_NAME --query "passwords[0].value" --output tsv)
az containerapp create \
--name aca-demo-api \
--resource-group $RESOURCE_GROUP \
--environment $ENVIRONMENT \
--image "$ACR_SERVER/aca-demo-api:v1" \
--registry-server $ACR_SERVER \
--registry-username $ACR_NAME \
--registry-password $ACR_PASSWORD \
--target-port 8080 \
--ingress external \
--min-replicas 0 \
--max-replicas 5 \
--cpu 0.5 \
--memory 1.0Gi \
--env-vars REGION=eastus
The --min-replicas 0 flag is the key setting for cost control: it tells ACA to scale the app down to zero running replicas, and stop billing usage charges entirely, whenever there’s no traffic. The first request after an idle period triggers a cold start, typically a few seconds for a small container like this one.
Grab the public URL and confirm it works:
FQDN=$(az containerapp show \
--name aca-demo-api \
--resource-group $RESOURCE_GROUP \
--query properties.configuration.ingress.fqdn \
--output tsv)
curl https://$FQDN/
curl https://$FQDN/health
Step 6: Configure Autoscaling Rules Beyond the Default
The default HTTP scale rule scales based on concurrent requests per replica, which is fine for many APIs but not ideal if your endpoints have wildly different resource costs. Add an explicit concurrency-based rule and a second rule for CPU utilization.
az containerapp update \
--name aca-demo-api \
--resource-group $RESOURCE_GROUP \
--min-replicas 0 \
--max-replicas 10 \
--scale-rule-name http-concurrency \
--scale-rule-type http \
--scale-rule-http-concurrency 20
This tells ACA to add a new replica for roughly every 20 concurrent in-flight requests per existing replica, capping out at 10 replicas total. Because ACA scaling is powered by KEDA under the hood, the same scale-rule mechanism also supports non-HTTP triggers, queue depth, Kafka lag, cron schedules, without you installing KEDA yourself or touching a `ScaledObject` manifest.
One caveat worth flagging early: if your app uses Dapr actors for stateful workloads, scale-to-zero is not supported, since actor state needs at least one replica alive to serve requests. Keep --min-replicas at 1 or higher for any Dapr actor-based service.
Step 7: Load Test the Scale-to-Zero Behavior
Verify scaling actually works before trusting it in production. Use a simple load generator against the slow endpoint you built earlier.
# Install a lightweight load tool if you don't have one
npm install -g loadtest
# Fire 200 requests, 20 concurrent, against the slow endpoint
loadtest -n 200 -c 20 https://$FQDN/slow
# Watch replica count scale up in real time
watch -n 2 "az containerapp replica list \
--name aca-demo-api \
--resource-group $RESOURCE_GROUP \
--query 'length(@)'"
You should see the replica count climb from zero to several instances within roughly 15 to 30 seconds of load starting, then drift back down to zero a few minutes after the load stops, assuming no other traffic keeps it warm. If replicas never scale past 1, double-check that --max-replicas was actually applied with the update command from Step 6.
Step 8: Add Secrets and Environment-Specific Configuration
Never bake credentials into environment variables in plain text. ACA has a built-in secrets store scoped to the container app, separate from Azure Key Vault, useful for values that don’t need vault-level rotation policies.
az containerapp secret set \
--name aca-demo-api \
--resource-group $RESOURCE_GROUP \
--secrets "api-key=REPLACE_WITH_REAL_VALUE"
az containerapp update \
--name aca-demo-api \
--resource-group $RESOURCE_GROUP \
--set-env-vars "API_KEY=secretref:api-key"
For anything that genuinely needs centralized rotation and audit logging, wire ACA to Azure Key Vault directly using a managed identity instead, since the built-in secrets store is meant for lighter-weight configuration, not compliance-grade secret management. Teams already running keyless authentication patterns on Google Cloud may recognize this approach from our GCP Workload Identity Federation setup guide, since both platforms are moving away from long-lived static credentials toward short-lived, identity-based tokens.
Step 9: Set Up Internal-Only Services With Service Discovery
Most real applications aren’t a single public API; they’re a public front door talking to internal services. Deploy a second app with internal-only ingress to see how ACA’s built-in DNS-based service discovery works.
az containerapp create \
--name aca-internal-worker \
--resource-group $RESOURCE_GROUP \
--environment $ENVIRONMENT \
--image "$ACR_SERVER/aca-demo-api:v1" \
--registry-server $ACR_SERVER \
--registry-username $ACR_NAME \
--registry-password $ACR_PASSWORD \
--target-port 8080 \
--ingress internal \
--min-replicas 1 \
--max-replicas 3
Apps in the same environment can reach aca-internal-worker by name over the internal environment DNS, with no load balancer or service mesh configuration required. This is the pattern to use for anything that shouldn’t be reachable from the public internet: internal APIs, background workers, and Dapr sidecar-based services.
Step 10: Attach a Custom Domain and Managed TLS Certificate
For production traffic, replace the auto-generated *.azurecontainerapps.io hostname with your own domain.
# Add a CNAME record pointing your domain at the app's FQDN first, then:
az containerapp hostname add \
--hostname api.yourdomain.com \
--name aca-demo-api \
--resource-group $RESOURCE_GROUP
az containerapp hostname bind \
--hostname api.yourdomain.com \
--name aca-demo-api \
--resource-group $RESOURCE_GROUP \
--environment $ENVIRONMENT \
--validation-method CNAME
ACA provisions and renews the TLS certificate automatically once domain ownership validates, with no manual certificate renewal to track. Propagation of the CNAME record can take anywhere from a few minutes to a few hours depending on your DNS provider’s TTL settings.
Step 11: Monitor Logs, Metrics, and HTTP Traffic
Since every environment ships with a Log Analytics workspace by default, structured logs are queryable immediately with no extra agent installation.
# Stream live logs from the console
az containerapp logs show \
--name aca-demo-api \
--resource-group $RESOURCE_GROUP \
--follow
# Query historical logs with Log Analytics Kusto queries
az monitor log-analytics query \
--workspace $(az containerapp env show --name $ENVIRONMENT --resource-group $RESOURCE_GROUP --query properties.appLogsConfiguration.logAnalyticsConfiguration.customerId --output tsv) \
--analytics-query "ContainerAppConsoleLogs_CL | take 20"
A newer addition worth turning on is the ContainerAppHTTPLogs diagnostic category, which logs every inbound HTTP request with status codes and latency, useful for debugging 502s that don’t show up in application-level logs because the request never reached your container.
Step 12: Automate Deployments With GitHub Actions
Manual az containerapp update commands don’t scale past a solo side project. Wire up continuous deployment so every push to main rebuilds and redeploys automatically.
# .github/workflows/deploy.yml
name: Deploy to Azure Container Apps
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Build and push image
run: |
az acr build --registry ${{ secrets.ACR_NAME }} \
--image aca-demo-api:${{ github.sha }} .
- name: Deploy new revision
run: |
az containerapp update \
--name aca-demo-api \
--resource-group ${{ secrets.RESOURCE_GROUP }} \
--image ${{ secrets.ACR_NAME }}.azurecr.io/aca-demo-api:${{ github.sha }}
Every deploy through containerapp update creates a new revision rather than overwriting the running one, and ACA keeps the previous revision available for instant rollback if the new one misbehaves. Traffic can also be split between revisions for canary-style rollouts, directing, for example, 10% of requests to a new revision before committing fully.
Step 13: Lock Down Networking With VNET Integration
Everything so far has used ACA’s default managed networking, which is fine for a demo but usually not acceptable once real customer data or internal databases enter the picture. For production workloads, attach the Container Apps Environment to your own virtual network instead of letting Azure manage networking entirely on its own.
VNET_NAME="aca-vnet"
SUBNET_NAME="aca-subnet"
az network vnet create \
--resource-group $RESOURCE_GROUP \
--name $VNET_NAME \
--address-prefix 10.0.0.0/16 \
--subnet-name $SUBNET_NAME \
--subnet-prefix 10.0.0.0/23
SUBNET_ID=$(az network vnet subnet show \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--name $SUBNET_NAME \
--query id --output tsv)
az containerapp env create \
--name aca-vnet-env \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--infrastructure-subnet-resource-id $SUBNET_ID \
--internal-only false
The subnet needs at least a /23 range for Consumption workload profiles, since ACA reserves a meaningful block of IP addresses for its own internal load balancing and platform components, separate from the IPs your app replicas actually consume. Setting --internal-only true instead removes the public endpoint entirely, useful for environments that should only be reachable through a VPN, ExpressRoute circuit, or an internal Application Gateway sitting in front of the environment.
Once VNET-injected, you gain access to Network Security Groups (NSGs) and User-Defined Routes (UDRs) on the subnet, which is the mechanism most enterprises use to force all outbound traffic through a firewall appliance for compliance logging before it ever reaches the public internet. This is also the setup required if your container apps need to reach resources inside an on-premises network over ExpressRoute, since the default managed networking mode has no route back to on-premises infrastructure at all.
Step 14: Apply Managed Identity and Least-Privilege Access
The registry credentials and connection strings used earlier in this tutorial work fine for a walkthrough, but production apps should authenticate to other Azure services using a managed identity rather than embedded secrets wherever possible. Assign a system-assigned identity to the app and grant it scoped access to a resource, in this example, an Azure Key Vault holding the real API key from Step 8.
az containerapp identity assign \
--name aca-demo-api \
--resource-group $RESOURCE_GROUP \
--system-assigned
PRINCIPAL_ID=$(az containerapp identity show \
--name aca-demo-api \
--resource-group $RESOURCE_GROUP \
--query principalId --output tsv)
az keyvault set-policy \
--name your-keyvault-name \
--object-id $PRINCIPAL_ID \
--secret-permissions get list
With this in place, your application code can request a token from the standard Azure Identity SDK without ever storing a client secret anywhere in the environment, the registry, or source control. This closes off one of the most common findings in cloud security audits: a service principal secret sitting in a container image layer or an environment variable that any teammate with read access to the resource group can retrieve in plain text. Scope the Key Vault access policy to exactly the secrets the app needs, not the entire vault, since a compromised container should not automatically mean a compromised vault. If you’re managing secrets across a larger fleet of services rather than a single app, it’s worth reviewing our Kubernetes secrets management guide for patterns that translate cleanly to ACA’s own secrets store.
Azure Container Apps vs Azure Kubernetes Service: Choosing the Right Layer
The question that comes up most often once a team has followed this tutorial is whether they should have used AKS instead. The honest answer depends less on scale and more on how much control your team actually wants over the underlying platform.
| Consideration | Azure Container Apps | Azure Kubernetes Service (AKS) |
|---|---|---|
| Cluster management | Fully managed, no visible control plane | You manage node pools, upgrades, and patching |
| Custom Kubernetes operators / CRDs | Not supported | Fully supported |
| Scale-to-zero | Native, no extra configuration | Possible via KEDA, but requires manual setup |
| Learning curve for a small team | Low; CLI-driven, no YAML manifests required | High; requires Kubernetes operational knowledge |
| Billing model | Per-second Consumption or per-node Dedicated | Per-node VM cost plus optional control plane tier |
| Best fit | Microservices, APIs, background workers, event-driven apps | Complex multi-tenant platforms, custom schedulers, service mesh at scale |
Teams that pick AKS by default because “that’s what serious companies use” frequently end up rebuilding, poorly, the exact autoscaling and service-discovery behavior that ACA gives you out of the box. The reverse mistake also happens: teams outgrow ACA’s abstraction the moment they need a custom admission controller, a service mesh with mutual TLS between every pod, or fine-grained node-level scheduling for GPU bin-packing across dozens of models. If you’re not sure which camp you’re in yet, start on ACA. Migrating a well-structured container app from ACA to AKS later is a matter of writing Kubernetes manifests around an image that already runs cleanly in a container; the reverse migration, stripping complexity out of an over-engineered Kubernetes setup, is considerably more painful.
Pricing: What You’ll Actually Pay
ACA’s Consumption plan bills active usage per second, with a meaningful monthly free grant per subscription, and a separate, lower rate for idle replicas kept warm via min-replicas above zero.
| Resource | Active Rate | Idle Rate | Monthly Free Grant |
|---|---|---|---|
| vCPU-second | $0.000024 | $0.000008 | 180,000 vCPU-seconds |
| GiB-second (memory) | $0.000003 | $0.000001 | 360,000 GiB-seconds |
| HTTP requests | $0.40 per million | n/a | 2,000,000 requests |
| Dedicated plan management fee | $0.10/hour per environment | n/a | Not covered by free grant |
Idle compute, meaning replicas kept alive by a non-zero min-replicas setting but not actively serving requests, is not covered by the free tier at all, so a service you keep “always warm” to avoid cold starts will accrue idle charges around one-third the active rate. For genuinely spiky or low-traffic workloads, letting the app scale fully to zero remains the cheapest option since Azure doesn’t bill usage charges once replica count hits zero.
Azure Container Apps vs AWS Fargate vs Google Cloud Run
All three platforms solve the same basic problem, run my container without managing a server, but the cost and capability curves diverge quickly once you look past the marketing pages.
| Factor | Azure Container Apps | AWS Fargate | Google Cloud Run |
|---|---|---|---|
| Scale-to-zero | Yes, no usage charge at zero | No, tasks bill while running | Yes, no usage charge at zero |
| Free monthly grant | 180K vCPU-sec, 360K GiB-sec, 2M requests | None | 180K vCPU-sec, 360K GiB-sec, 2M requests |
| Max execution time | Unlimited | Unlimited | 60 minutes |
| Built-in event-driven autoscaling | Yes, via native KEDA integration | No, requires separate Application Auto Scaling config | Partial, via Cloud Run jobs and Eventarc |
| Built-in service mesh / actors | Yes, via native Dapr integration | No, requires App Mesh or a sidecar | No, requires a separate mesh add-on |
| Est. cost, low-traffic workload (<50K req/mo) | $5–$8/mo | $25–$35/mo | $2–$5/mo |
| Est. cost, 1M req/mo at 500ms avg | $8–$18/mo | $25–$35/mo | $5–$15/mo |
Google Cloud Run generally wins on raw cost per request for low-to-medium traffic, largely because Google’s per-second billing granularity and free tier are slightly more generous. AWS Fargate has no free tier at all and charges for the full task lifetime regardless of idle time, which makes it the most expensive of the three for spiky or low-traffic services, though it remains the default choice for teams already standardized on ECS. ACA’s advantage isn’t raw per-request cost, it’s the fact that KEDA and Dapr come pre-wired, which removes real engineering hours that Fargate and Cloud Run both require you to spend wiring up equivalent functionality yourself. For a deeper cost breakdown across all three, see our full AWS Fargate vs Cloud Run vs Azure Container Apps comparison.
5 Common Pitfalls When Deploying to Azure Container Apps
1. Forgetting that idle replicas still cost money. Setting --min-replicas 1 “just to be safe” against cold starts quietly opts you out of the biggest cost advantage ACA offers. Only set a non-zero minimum when cold-start latency is genuinely unacceptable for your use case.
2. Using Dapr actors with scale-to-zero enabled. Actor state requires at least one live replica. Apps that silently stop responding after a period of inactivity are almost always a Dapr actor app that still has min-replicas set to 0.
3. Skipping resource provider registration. A fresh subscription without Microsoft.App registered will fail environment creation with an error that doesn’t clearly point back to the missing registration step.
4. Assuming internal ingress apps are reachable from outside the environment. Internal-only apps resolve over the environment’s internal DNS only; they are unreachable from the public internet or from a different Container Apps Environment, even in the same resource group.
5. Mixing up Consumption and Dedicated pricing models. Dedicated workload profiles charge per reserved node whether or not it’s fully utilized, plus a flat $0.10/hour management fee per environment. Teams that assume Dedicated behaves like Consumption billing get an unpleasant invoice surprise the first month.
Troubleshooting Guide
Deployment fails with “resource provider not registered.” Re-run the provider registration commands from Step 2 and wait for registrationState to return Registered before retrying.
App returns a 502 immediately after deploy. Confirm --target-port matches the port your container actually listens on. This is the most common cause of an otherwise healthy container failing all requests.
Replicas never scale past one under load. Check that --max-replicas was actually applied; running az containerapp update without re-specifying it can silently reset scaling limits to defaults in some CLI versions.
Cold starts feel slower than expected. Large base images add real seconds to cold start time. Switch to an Alpine or distroless base image and confirm you’re not pulling unnecessary layers at container start.
Custom domain validation never completes. DNS propagation delays are the usual cause. Confirm the CNAME record resolves correctly using dig or nslookup before retrying the bind command.
Logs show nothing in Log Analytics. Confirm the environment’s Log Analytics workspace wasn’t deleted independently of the environment itself; ACA does not recreate it automatically if removed manually.
Secrets aren’t picked up after updating them. Updating a secret value with az containerapp secret set does not automatically restart running replicas. Trigger a new revision to force the app to re-read the updated secret.
GitHub Actions deploy succeeds but old code still runs. Confirm the image tag actually changed between builds. Reusing a static tag like latest can cause ACA to skip creating a new revision if it doesn’t detect a change.
Advanced Tips for Production Workloads
Use revision labels to run true blue-green deployments rather than relying solely on percentage-based traffic splitting; labels let you point a stable URL at whichever revision you designate as current, independent of deploy order. For workloads that need hardware-backed isolation, such as processing sensitive data inside a container you don’t fully trust, the Confidential Compute workload profile, generally available since mid-2026, runs your container inside a hardware Trusted Execution Environment without requiring any code changes. Teams running background agentic workloads should also look at Container Apps Sandboxes, a microVM-based isolation primitive that entered public preview in mid-2026, purpose-built for running untrusted or dynamically generated code safely alongside your main services.
If your organization is standardizing on infrastructure-as-code, define environments and apps with Bicep or Terraform rather than raw CLI commands once you move past the prototype stage; both support the full workload profile and scale-rule configuration surface used in this tutorial.
Cost Optimization Checklist Before Going to Production
Run through this checklist before pointing real traffic at your Container Apps environment. Each item maps directly to a line item you’ll otherwise find on next month’s Azure invoice.
- Confirm every non-critical service actually has
min-replicasset to 0, not 1, unless cold-start latency has been measured and found unacceptable for that specific endpoint. - Check whether any app was accidentally deployed on a Dedicated workload profile when Consumption would suffice; Dedicated bills per reserved node regardless of traffic.
- Review the
--max-replicasceiling on every app; a misconfigured scale rule combined with a traffic spike or a retry storm from a misbehaving client can scale an app far higher than intended before anyone notices. - Audit egress-heavy apps. Outbound data transfer to the public internet is billed separately from compute and is easy to overlook until a service that streams large files racks up an unexpected bandwidth charge.
- Set up budget alerts in Azure Cost Management scoped to the resource group holding your Container Apps environment, so a runaway scale event triggers a notification instead of a surprise at billing close.
- Reassess Dedicated GPU nodes monthly. At close to $3,200/month for the largest profile, an idle GPU node left running after a project winds down is one of the more expensive mistakes teams make on this platform.
Complete Working Project Structure
By the end of this tutorial, your project directory should look like this, ready to clone into any new environment:
aca-demo-api/
├── server.js
├── package.json
├── Dockerfile
├── .github/
│ └── workflows/
│ └── deploy.yml
└── infra/
├── main.bicep
└── containerapp.bicep
This structure separates application code, container definition, CI/CD automation, and infrastructure-as-code cleanly, matching how most production ACA deployments are actually organized once teams move past manual CLI commands.
When Azure Container Apps Is the Wrong Choice
ACA is not a fit for every workload. Teams that need custom Kubernetes operators, CRDs, or direct node-level access should use AKS instead, since ACA deliberately hides that layer. Workloads with hard sub-100ms latency requirements on every single request, with zero tolerance for cold starts, should keep min-replicas above zero on any platform or consider Dedicated workload profiles, which trade the free-tier and scale-to-zero economics for consistent, VM-backed performance. And organizations already deeply invested in AWS-native tooling, IAM policies, VPC peering, existing ECS task definitions, will generally find it cheaper in engineering time to stay on Fargate or migrate to App Runner rather than rewriting their deployment pipeline for a different cloud entirely.
There’s also a middle case worth naming directly: teams running batch jobs that need to process a queue for hours at a stretch with no HTTP traffic at all. ACA supports this through the dedicated Jobs feature, which runs a container to completion rather than as a long-lived replica, but if that queue-processing pattern is the majority of your workload rather than a side task next to an API, a purpose-built batch service, or Azure Batch directly, may fit better than bending Container Apps Jobs to the task. Match the platform to the shape of the workload rather than standardizing on one service for everything just because it’s already familiar. For a broader look at how ACA’s event-driven model stacks up against the serverless functions layer, see our AWS Lambda vs Azure Functions vs GCP comparison, and for service mesh questions once you outgrow ACA’s built-in Dapr networking, our Istio vs Linkerd vs Cilium breakdown covers the AKS-side alternatives.
Frequently Asked Questions
Is Azure Container Apps built on Kubernetes?
Yes. ACA runs on managed Kubernetes infrastructure internally, using KEDA for autoscaling and Dapr for microservice patterns, but Microsoft fully abstracts the cluster layer away. You cannot access the underlying Kubernetes API directly.
Does Azure Container Apps support GPU workloads?
Yes, through Dedicated workload profiles. NC-series nodes with A100 GPUs are available for inference or other GPU-bound workloads, billed per node rather than per second.
How does Azure Container Apps pricing compare to AWS Fargate?
ACA is generally cheaper for low-traffic or spiky workloads because of its scale-to-zero support and free monthly grant, while Fargate has no free tier and bills for the full task runtime regardless of idle time. For sustained, always-on workloads, the gap narrows considerably.
Can I run a stateful application on Azure Container Apps?
Yes, using Dapr’s state management APIs or by keeping min-replicas above zero for apps managing their own local state, but scale-to-zero must be disabled for Dapr actor-based stateful workloads specifically.
What is the maximum container execution time on Azure Container Apps?
There is no maximum execution time on ACA, unlike Google Cloud Run’s 60-minute cap or AWS Lambda’s 15-minute limit, making it suitable for long-running background jobs as well as request-response APIs.
Do I need to know Kubernetes to use Azure Container Apps?
No. ACA is specifically designed so teams get Kubernetes-grade autoscaling and networking benefits without needing Kubernetes expertise. Familiarity with Docker and basic CLI usage is sufficient for everything in this tutorial.
What happened to Azure Container Instances (ACI)? Is it being replaced by ACA?
ACI remains available for simple, single-container scenarios without autoscaling needs, but Microsoft has steered most new serverless container investment toward ACA, which offers autoscaling, revisions, and Dapr/KEDA integration that ACI lacks.
Is Azure Container Apps generally available or still in preview?
Core Container Apps functionality has been generally available since 2022. Confidential Compute workload profiles reached GA in mid-2026, while newer features like Container Apps Sandboxes remain in public preview as of September 2026.
Can Azure Container Apps run scheduled or batch jobs instead of always-on APIs?
Yes. The Container Apps Jobs feature runs a container to completion on a manual trigger, a cron schedule, or an event source, then exits, rather than staying up as a long-lived HTTP-serving replica. This is the right primitive for nightly data processing, report generation, or queue-draining tasks that don’t need a persistent public endpoint.
How many container apps can run inside a single environment?
Microsoft documents environment-level quotas rather than a hard app-count ceiling, and most teams hit practical organizational limits, too many services to reason about in one environment, long before they hit a platform quota. Splitting large systems into multiple environments by team or by production/staging boundary is common practice once an environment starts hosting more than a handful of unrelated services.


