Azure Container Apps — Run Production Containers Without Kubernetes
Posted on: 4/17/2026 5:05:43 PM
Table of contents
- Contents
- 1. What is Azure Container Apps?
- 2. System architecture
- 3. Compared with AKS, App Service, and Container Instances
- 4. Core features
- 5. Free Tier in detail — near-zero production cost
- 6. Deploying .NET 10 apps to ACA
- 7. Dapr Integration — Microservices without vendor lock-in
- 8. Autoscaling with KEDA — from zero to thousands of replicas
- 9. Jobs — Background and scheduled tasks
- 10. Networking & Security
- 11. Observability & Monitoring
- 12. What's new in 2026
- 13. Production best practices
- 14. Conclusion
1. What is Azure Container Apps?
Azure Container Apps (ACA) is Microsoft Azure's serverless container platform that lets you run containerized applications without managing a Kubernetes cluster, node pools, or any underlying infrastructure. ACA automatically handles scaling, load balancing, TLS certificates, and revision management — you focus on code.
If you've ever felt Kubernetes is overkill for a backend API or a handful of microservices, ACA is exactly what you need. It delivers the power of container orchestration without requiring you to understand etcd, kube-proxy, or CNI plugins.
Why not use Kubernetes directly?
AKS (Azure Kubernetes Service) is powerful when you need full control of the cluster. But for most web apps, APIs, and microservices, ACA provides 90% of the features you need with only 10% of the management effort. You don't have to think about node upgrades, cluster networking, or RBAC policies — Azure handles it.
2. System architecture
ACA is built on top of Kubernetes (AKS) but fully abstracts that layer from developers. The architecture has three main tiers:
graph TB
subgraph Internet
U[Users / Clients]
end
subgraph ACA_ENV["Container Apps Environment"]
direction TB
INGRESS["Built-in Ingress
TLS + Load Balancer"]
subgraph APPS["Container Apps"]
A1["API Gateway
.NET 10"]
A2["Order Service
.NET 10"]
A3["Notification
Worker"]
end
subgraph INFRA["Platform Services"]
DAPR["Dapr Sidecar"]
KEDA["KEDA Autoscaler"]
ENVOY["Envoy Proxy"]
end
subgraph STORAGE["Managed Storage"]
LOG["Log Analytics"]
SECRET["Secret Store"]
end
end
U --> INGRESS
INGRESS --> A1
A1 --> DAPR
DAPR --> A2
DAPR --> A3
A2 --> KEDA
A3 --> KEDA
APPS --> LOG
APPS --> SECRET
style ACA_ENV fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style INGRESS fill:#e94560,stroke:#fff,color:#fff
style DAPR fill:#2c3e50,stroke:#fff,color:#fff
style KEDA fill:#2c3e50,stroke:#fff,color:#fff
style ENVOY fill:#2c3e50,stroke:#fff,color:#fff
style A1 fill:#fff,stroke:#e94560,color:#2c3e50
style A2 fill:#fff,stroke:#e94560,color:#2c3e50
style A3 fill:#fff,stroke:#e94560,color:#2c3e50
style LOG fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style SECRET fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
Azure Container Apps Environment — architecture overview
A Container Apps Environment is the shared boundary for every container app, similar to a Kubernetes namespace. Apps in the same environment share a virtual network, logging configuration, and Dapr components.
Envoy Proxy handles ingress routing, TLS termination, and traffic splitting between revisions. KEDA (Kubernetes Event-Driven Autoscaling) controls scaling based on HTTP traffic, queue depth, cron schedule, or any metric KEDA supports. Dapr (Distributed Application Runtime) provides microservices building blocks: service invocation, state management, pub/sub, bindings, and secrets.
3. Compared with AKS, App Service, and Container Instances
Azure offers many container services. The table below helps pick the right one for the job:
| Criterion | Container Apps | AKS | App Service | Container Instances |
|---|---|---|---|---|
| Infrastructure mgmt | Serverless, none to manage | You manage the cluster | Managed PaaS | Serverless, simple |
| Scaling | 0 → N, KEDA-based | HPA, VPA, Cluster Autoscaler | 1 → 30 instances | No auto-scale |
| Scale-to-zero | ✅ Yes | ❌ No (always a node) | ❌ No (always an instance) | N/A |
| Dapr integration | ✅ Built-in | Self-install | ❌ No | ❌ No |
| Revision management | ✅ Traffic splitting | Manage Deployments yourself | Deployment slots | ❌ No |
| GPU support | ✅ Serverless GPU (GA 2026) | ✅ GPU node pools | ❌ No | ✅ GPU containers |
| Jobs/Cron | ✅ Native Jobs | CronJob resource | WebJobs | Schedule yourself |
| Free tier | ✅ 180K vCPU-s/month | Free control plane | F1 tier (limited) | No free tier |
| Best for | Microservices, APIs, event-driven | Complex workloads, full control | Simple web apps | Short batch jobs |
When to pick ACA over AKS?
If your team is under 5 people without a dedicated DevOps/Platform engineer, ACA is almost always the better choice. You save hundreds of hours a year not managing cluster upgrades, node draining, and network policies. Only reach for AKS when you truly need full control (custom operators, complex service mesh, multi-tenancy at the cluster level).
4. Core features
4.1. Ingress & Traffic Management
ACA provides built-in HTTP ingress with automatic TLS termination. You don't need to install NGINX Ingress Controller or cert-manager — flip on ingress and you have an HTTPS endpoint.
# Create a container app with ingress
az containerapp create \
--name my-api \
--resource-group my-rg \
--environment my-env \
--image myregistry.azurecr.io/my-api:v1 \
--target-port 8080 \
--ingress external \
--min-replicas 0 \
--max-replicas 10
Traffic splitting enables Blue/Green deployments and A/B testing:
# Split traffic: 80% current revision, 20% new revision
az containerapp ingress traffic set \
--name my-api \
--resource-group my-rg \
--revision-weight my-api--v1=80 my-api--v2=20
4.2. Revision Management
Every time you change the container image or configuration, ACA creates a new revision. You can run multiple revisions simultaneously and split traffic between them — ideal for canary deployments.
graph LR
LB["Load Balancer"]
LB -->|"80%"| R1["Revision v1
3 replicas"]
LB -->|"20%"| R2["Revision v2
1 replica"]
R1 --> DB[(Database)]
R2 --> DB
style LB fill:#e94560,stroke:#fff,color:#fff
style R1 fill:#fff,stroke:#e94560,color:#2c3e50
style R2 fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50
style DB fill:#2c3e50,stroke:#fff,color:#fff
Traffic splitting between two revisions for a canary deployment
4.3. Secret Management
ACA lets you define secrets directly or reference them from Azure Key Vault. Secrets are injected into the container via environment variables — no credentials live inside the container image.
# Add a secret from Key Vault
az containerapp secret set \
--name my-api \
--resource-group my-rg \
--secrets "db-conn=keyvaultref:https://my-vault.vault.azure.net/secrets/db-connection,identityref:/subscriptions/.../my-identity"
5. Free Tier in detail — near-zero production cost
This is ACA's most attractive feature: a free tier generous enough to run production for small and mid-sized apps.
| Resource | Free Tier (per subscription/month) | After the free limit |
|---|---|---|
| vCPU | 180,000 vCPU-seconds | $0.000024/vCPU-second |
| Memory | 360,000 GiB-seconds | $0.000003/GiB-second |
| Requests | 2,000,000 requests | $0.40/million requests |
| Scale-to-zero | ✅ Not billed when replicas = 0 | — |
| Idle charge | Discounted rate while idle | Lower than active rate |
What's the free tier actually enough for?
180,000 vCPU-seconds ≈ one 0.25 vCPU container running continuously for ~8.3 days, or many scale-to-zero containers running only when there's a request. For a small API receiving a few hundred requests per day, the free tier covers the whole month for $0. Health-probe requests aren't billed either.
A realistic cost estimate
Suppose you have one .NET 10 API with 0.5 vCPU / 1 GiB RAM, averaging 50,000 requests/day, processing time ~100 ms/request:
Active time/day: 50,000 × 100ms = 5,000 seconds
vCPU-seconds/day: 5,000 × 0.5 = 2,500
vCPU-seconds/month: 2,500 × 30 = 75,000 ← within free tier (180,000)
GiB-seconds/day: 5,000 × 1 = 5,000
GiB-seconds/month: 5,000 × 30 = 150,000 ← within free tier (360,000)
Requests/month: 50,000 × 30 = 1,500,000 ← within free tier (2,000,000)
→ Cost: $0/month
6. Deploying .NET 10 apps to ACA
6.1. An optimized Dockerfile for .NET 10
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS runtime
WORKDIR /app
COPY --from=build /app .
ENV ASPNETCORE_URLS=http://+:8080
ENV DOTNET_EnableDiagnostics=0
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApi.dll"]
Why use an Alpine image?
The aspnet:10.0-alpine image is only ~110 MB compared to ~220 MB for the Debian image. Lighter containers → faster cold start, faster image pulls, saved bandwidth. With Native AOT, images can drop below 50 MB.
6.2. Deploying via Azure CLI
# Create the resource group and environment
az group create --name rg-myapp --location southeastasia
az containerapp env create \
--name env-myapp \
--resource-group rg-myapp \
--location southeastasia
# Build and push the image to ACR
az acr create --name myappacr --resource-group rg-myapp --sku Basic
az acr build --registry myappacr --image my-api:v1 .
# Deploy the container app
az containerapp create \
--name api-myapp \
--resource-group rg-myapp \
--environment env-myapp \
--image myappacr.azurecr.io/my-api:v1 \
--registry-server myappacr.azurecr.io \
--target-port 8080 \
--ingress external \
--cpu 0.25 --memory 0.5Gi \
--min-replicas 0 \
--max-replicas 5 \
--env-vars "ASPNETCORE_ENVIRONMENT=Production"
6.3. Deploying with .NET Aspire
If you're using .NET Aspire, deploying to ACA is trivial with azd (Azure Developer CLI):
# Initialize an Aspire project
dotnet new aspire-starter -n MyApp
cd MyApp
# Deploy to ACA
azd init
azd up
Aspire automatically creates the Container Apps Environment, pushes container images to ACR, configures service discovery between projects, and sets up health checks — all with two commands.
6.4. Configuring Health Probes
ACA supports the same three types of health probes as Kubernetes:
// Program.cs - Configure health checks
builder.Services.AddHealthChecks()
.AddSqlServer(builder.Configuration.GetConnectionString("Default")!)
.AddRedis(builder.Configuration.GetConnectionString("Redis")!);
app.MapHealthChecks("/healthz/live", new HealthCheckOptions
{
Predicate = _ => false // Liveness: only checks the app is alive
});
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready") // Readiness: checks dependencies
});
# container app config
properties:
template:
containers:
- name: my-api
probes:
- type: Liveness
httpGet:
path: /healthz/live
port: 8080
periodSeconds: 10
- type: Readiness
httpGet:
path: /healthz/ready
port: 8080
periodSeconds: 5
- type: Startup
httpGet:
path: /healthz/live
port: 8080
failureThreshold: 30
periodSeconds: 2
7. Dapr Integration — Microservices without vendor lock-in
Dapr (Distributed Application Runtime) is a building-block runtime for microservices, and ACA integrates Dapr at the platform level — just flip a flag, no install or sidecar management required.
graph LR
subgraph ACA_ENV["Container Apps Environment"]
subgraph APP1["Order Service"]
C1["App Code"]
D1["Dapr Sidecar"]
end
subgraph APP2["Payment Service"]
C2["App Code"]
D2["Dapr Sidecar"]
end
subgraph APP3["Email Service"]
C3["App Code"]
D3["Dapr Sidecar"]
end
end
C1 -->|"HTTP/gRPC"| D1
D1 -->|"Service Invocation"| D2
D2 --> C2
D1 -->|"Pub/Sub"| D3
D3 --> C3
style C1 fill:#fff,stroke:#e94560,color:#2c3e50
style C2 fill:#fff,stroke:#e94560,color:#2c3e50
style C3 fill:#fff,stroke:#e94560,color:#2c3e50
style D1 fill:#e94560,stroke:#fff,color:#fff
style D2 fill:#e94560,stroke:#fff,color:#fff
style D3 fill:#e94560,stroke:#fff,color:#fff
The Dapr sidecar pattern inside Azure Container Apps
The most useful Dapr building blocks
| Building Block | Description | Default backend on ACA |
|---|---|---|
| Service Invocation | Call another service by name — no IP/port needed | Internal DNS |
| State Management | Key-value store for sessions, carts, preferences | Azure Cosmos DB, Azure Table Storage |
| Pub/Sub | Publish/Subscribe messaging between services | Azure Service Bus, Azure Event Hubs |
| Bindings | Input/output bindings to external systems | Azure Blob Storage, Azure Queue, SMTP |
| Secrets | Access secrets from secret stores | Azure Key Vault |
// Call the Payment Service via Dapr (no URL knowledge needed)
using var client = new DaprClientBuilder().Build();
var order = new Order { Id = "ORD-001", Amount = 99.99m };
var result = await client.InvokeMethodAsync<Order, PaymentResult>(
"payment-service", // App ID of the target service
"api/process",
order
);
// Pub/Sub: publish an event
await client.PublishEventAsync("pubsub", "order-completed", order);
// State Management: save state
await client.SaveStateAsync("statestore", $"order-{order.Id}", order);
8. Autoscaling with KEDA — from zero to thousands of replicas
ACA uses KEDA (Kubernetes Event-Driven Autoscaling) to scale based on many different event sources. This is a clear edge over App Service (CPU/memory only) and Container Instances (no auto-scale).
Common scaling rules
# Scale by HTTP traffic
scale:
minReplicas: 0
maxReplicas: 20
rules:
- name: http-rule
http:
metadata:
concurrentRequests: "50"
---
# Scale by Azure Service Bus queue
scale:
minReplicas: 0
maxReplicas: 30
rules:
- name: queue-rule
custom:
type: azure-servicebus
metadata:
queueName: orders
messageCount: "5"
auth:
- secretRef: sb-connection
triggerParameter: connection
---
# Scale by cron schedule
scale:
minReplicas: 1
maxReplicas: 10
rules:
- name: business-hours
custom:
type: cron
metadata:
timezone: "Asia/Ho_Chi_Minh"
start: "0 8 * * 1-5"
end: "0 18 * * 1-5"
desiredReplicas: "5"
Scale-to-zero caveats
When scaling down to 0 replicas, the first request pays a cold start (typically 2-5 seconds for .NET). To mitigate, set minReplicas: 1 (you'll lose free tier for idle time) or use Native AOT to drop startup below 100 ms. Apps scaled by CPU/memory can't go to zero.
9. Jobs — Background and scheduled tasks
Besides apps (services that run continuously), ACA supports Jobs — containers that run to completion and stop. There are three trigger types:
| Trigger type | Description | Use case |
|---|---|---|
| Manual | Runs on demand (API call or CLI) | Database migrations, one-off scripts |
| Schedule | Runs on a cron expression | Daily reports, cleanup, data sync |
| Event | Runs when there's an event from a queue/topic | Image processing, batch email, ETL |
# Create a scheduled job that runs at 2:00 AM daily
az containerapp job create \
--name job-daily-report \
--resource-group rg-myapp \
--environment env-myapp \
--image myappacr.azurecr.io/report-generator:v1 \
--trigger-type Schedule \
--cron-expression "0 2 * * *" \
--cpu 0.5 --memory 1Gi \
--replica-timeout 3600 \
--env-vars "DB_CONN=secretref:db-connection"
10. Networking & Security
10.1. Network architecture
graph TB
INET["Internet"]
subgraph VNET["Azure Virtual Network"]
subgraph SUBNET["ACA Subnet"]
ENV["Container Apps Environment"]
APP1["Public App
(External Ingress)"]
APP2["Internal Service
(Internal Ingress)"]
APP3["Background Worker
(No Ingress)"]
end
subgraph PRIV["Private Subnet"]
DB["Azure SQL"]
KV["Key Vault"]
end
end
INET -->|"HTTPS"| APP1
APP1 -->|"Internal DNS"| APP2
APP2 --> APP3
APP2 --> DB
APP1 --> KV
style INET fill:#e94560,stroke:#fff,color:#fff
style APP1 fill:#fff,stroke:#e94560,color:#2c3e50
style APP2 fill:#fff,stroke:#2c3e50,color:#2c3e50
style APP3 fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style DB fill:#2c3e50,stroke:#fff,color:#fff
style KV fill:#2c3e50,stroke:#fff,color:#fff
Networking: combining external/internal ingress with VNet integration
ACA supports three levels of network access:
- External ingress: reachable from the internet over HTTPS
- Internal ingress: only reachable within the environment (service-to-service)
- No ingress: no inbound traffic, runs in the background only (workers, jobs)
10.2. Managed Identity
Instead of storing connection strings or API keys, use Managed Identity to authenticate with Azure services — no credentials to manage:
// Use Managed Identity to access Azure SQL
builder.Services.AddDbContext<AppDbContext>(options =>
{
var conn = new SqlConnection(builder.Configuration.GetConnectionString("Default"));
conn.AccessToken = new DefaultAzureCredential()
.GetToken(new TokenRequestContext(new[] { "https://database.windows.net/.default" }))
.Token;
options.UseSqlServer(conn);
});
// Managed Identity for Azure Blob Storage
builder.Services.AddAzureClients(clientBuilder =>
{
clientBuilder.AddBlobServiceClient(new Uri("https://mystorage.blob.core.windows.net"));
clientBuilder.UseCredential(new DefaultAzureCredential());
});
10.3. Confidential Computing (Preview 2026)
ACA now supports Trusted Execution Environments (TEEs) for workloads that need hardware-level data security — data stays encrypted even while being processed in memory. A fit for fintech, healthcare, and apps handling PII.
11. Observability & Monitoring
ACA integrates natively with Azure Monitor and Log Analytics. All stdout/stderr from containers is collected automatically.
// Configure OpenTelemetry for ACA
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation()
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter());
// KQL query: top 10 slowest endpoints
ContainerAppConsoleLogs_CL
| where ContainerAppName_s == "my-api"
| where Log_s contains "Request finished"
| parse Log_s with * "in " Duration:real "ms"
| summarize avg(Duration), p95=percentile(Duration, 95) by Endpoint=extract("Path: ([^ ]+)", 1, Log_s)
| top 10 by p95 desc
12. What's new in 2026
13. Production best practices
13.1. Container image
- Use Alpine or distroless base images to reduce the attack surface
- Multi-stage build: separate build stage (SDK) from runtime stage (ASP.NET runtime)
- Pin specific image versions (
aspnet:10.0.1-alpine), avoid:latest - Use .dockerignore to exclude bin/, obj/, .git/
13.2. Configuration & secrets
- Use environment variables instead of appsettings.json for cloud config
- Sensitive values → Azure Key Vault references, never hardcoded
- Managed Identity for every Azure service — don't use connection strings with passwords
13.3. Scaling & performance
- Set
minReplicas: 0for services that don't need always-on (enjoy the free tier) - Use the concurrentRequests scaling rule for HTTP APIs (usually 50-100)
- Enable Native AOT for minimal APIs to cut cold start from ~3 s to ~100 ms
- Design stateless — don't keep state in the container, use external stores (Redis, Cosmos DB)
13.4. Reliability
- Configure Liveness, Readiness, Startup probes fully
- Use revision management + traffic splitting for zero-downtime deploys
- Deploy to Southeast Asia (Singapore) for the lowest latency from Vietnam
- Set up alerts on replica count, error rate, and response time
Production deployment checklist
✅ Health probes (liveness + readiness) configured
✅ Managed Identity for Azure services
✅ Secrets stored in Key Vault
✅ Min 2 replicas for critical services
✅ VNet integration for database access
✅ Log Analytics workspace configured
✅ Custom domain + TLS certificate
✅ Resource limits (CPU/memory) explicitly set
14. Conclusion
Azure Container Apps is the ideal choice for most .NET developers who want to run containers without facing Kubernetes's complexity. With a generous free tier (180K vCPU-seconds, 2M requests/month), scale-to-zero, built-in Dapr integration, and new 2026 features like Serverless GPU and Confidential Computing, ACA is strong enough for everything from side projects to serious production workloads.
If you're on App Service and want more control, or using AKS but finding it too complex for your team size — try ACA. You'll be surprised how simple it feels while still being production-grade.
References
- Azure Container Apps Overview — Microsoft Learn
- .NET on Azure Container Apps — Microsoft Learn
- Azure Container Apps Pricing
- Billing in Azure Container Apps — Microsoft Learn
- What's new in Azure Container Apps at Ignite'25
- Set scaling rules in Azure Container Apps
- Build microservices with Dapr and ACA
GitHub Actions CI/CD for .NET 2026 — OIDC, Egress Firewall, and Immutable Actions for Production Pipelines
Optimizing INP with scheduler.yield() — Elevating Web Responsiveness in 2026
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.