Deploying .NET to Azure Container Apps — Full Guide
Deploy .NET microservices to Azure Container Apps: managed environments, Dapr integration, KEDA scaling, traffic splitting, secrets management, and production observability setup.
Deploying .NET to Azure Container Apps — Full Guide
Azure Container Apps (ACA) is a managed serverless container platform built on Kubernetes. You get auto-scaling, Dapr integration, traffic splitting, and managed ingress without writing a single Kubernetes manifest.
When to Choose ACA
Azure Container Apps:
✓ Managed Kubernetes — no cluster to maintain
✓ Built-in KEDA — scale to zero, scale on queue depth, HTTP traffic
✓ Dapr sidecar — service discovery, pub/sub, state management
✓ Traffic splitting — blue/green, canary with percentage routing
✓ Managed ingress with TLS — no nginx config needed
✓ Consumption billing — pay per request (can be $0 at idle)
✗ Less control than raw AKS
✗ Limited egress control (use AKS + Istio for complex network policies)
AKS:
✓ Full Kubernetes — any workload, any network policy
✗ You manage cluster upgrades, node pools, and security patchingStep 1: Create the Environment
# Install Azure CLI extension
az extension add --name containerapp --upgrade
# Create resource group
az group create --name rg-orders --location uksouth
# Create Container Apps environment (shared infrastructure for all apps)
az containerapp env create \
--name aca-env-orders \
--resource-group rg-orders \
--location uksouth \
--logs-destination log-analytics \
--logs-workspace-id $LOG_ANALYTICS_WORKSPACE_ID \
--logs-workspace-key $LOG_ANALYTICS_KEYStep 2: Deploy a .NET App
# Build and push image
az acr build \
--registry myregistry \
--image order-service:latest \
--file src/OrderService/Dockerfile .
# Create the container app
az containerapp create \
--name order-service \
--resource-group rg-orders \
--environment aca-env-orders \
--image myregistry.azurecr.io/order-service:latest \
--target-port 8080 \
--ingress external \
--min-replicas 1 \
--max-replicas 10 \
--cpu 0.5 \
--memory 1Gi \
--env-vars \
ASPNETCORE_ENVIRONMENT=Production \
"ConnectionStrings__DefaultConnection=secretref:db-connection-string" \
--secrets \
"db-connection-string=Host=postgres.example.com;Database=orders;..."Step 3: Bicep / Infrastructure as Code
// container-apps.bicep — declarative resource definition
param environment string = 'production'
param imageTag string = 'latest'
resource acaEnv 'Microsoft.App/managedEnvironments@2023-05-01' = {
name: 'aca-env-orders'
location: resourceGroup().location
properties: {
daprAIInstrumentationKey: applicationInsights.properties.InstrumentationKey
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalyticsWorkspace.properties.customerId
sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey
}
}
}
}
resource orderService 'Microsoft.App/containerApps@2023-05-01' = {
name: 'order-service'
location: resourceGroup().location
properties: {
managedEnvironmentId: acaEnv.id
configuration: {
ingress: {
external: true
targetPort: 8080
transport: 'http2'
corsPolicy: {
allowedOrigins: ['https://app.example.com']
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE']
}
}
dapr: {
enabled: true
appId: 'order-service'
appPort: 8080
httpReadBufferSize: 64
}
secrets: [
{
name: 'db-connection-string'
keyVaultUrl: 'https://kv-orders.vault.azure.net/secrets/db-connection-string'
identity: '/subscriptions/.../resourcegroups/.../providers/Microsoft.ManagedIdentity/userAssignedIdentities/aca-identity'
}
]
}
template: {
containers: [
{
name: 'order-service'
image: 'myregistry.azurecr.io/order-service:${imageTag}'
resources: { cpu: json('0.5'), memory: '1Gi' }
env: [
{ name: 'ASPNETCORE_ENVIRONMENT', value: environment }
{ name: 'ConnectionStrings__DefaultConnection', secretRef: 'db-connection-string' }
]
probes: [
{
type: 'Liveness'
httpGet: { path: '/health/live', port: 8080 }
initialDelaySeconds: 10
periodSeconds: 10
}
{
type: 'Readiness'
httpGet: { path: '/health/ready', port: 8080 }
initialDelaySeconds: 5
periodSeconds: 5
}
]
}
]
scale: {
minReplicas: 1
maxReplicas: 20
rules: [
{
name: 'http-scaling'
http: { metadata: { concurrentRequests: '100' } }
}
]
}
}
}
}Step 4: KEDA Scaling Rules
// Scale on Azure Service Bus queue depth
scale: {
minReplicas: 0 // scale to zero when idle (save cost)
maxReplicas: 30
rules: [
{
name: 'service-bus-scaling'
custom: {
type: 'azure-servicebus'
metadata: {
queueName: 'order-processing'
messageCount: '10' // one replica per 10 messages
namespace: 'sb-orders.servicebus.windows.net'
}
auth: [
{
secretRef: 'service-bus-connection'
triggerParameter: 'connection'
}
]
}
}
]
}Step 5: Traffic Splitting (Blue/Green and Canary)
# Deploy new version as a revision
az containerapp update \
--name order-service \
--resource-group rg-orders \
--image myregistry.azurecr.io/order-service:v2.0.0 \
--revision-suffix v2
# Canary: send 10% of traffic to new revision
az containerapp ingress traffic set \
--name order-service \
--resource-group rg-orders \
--revision-weight \
order-service--v1=90 \
order-service--v2=10
# Promote to 100% after validation
az containerapp ingress traffic set \
--name order-service \
--resource-group rg-orders \
--revision-weight order-service--v2=100
# Deactivate old revision
az containerapp revision deactivate \
--name order-service \
--resource-group rg-orders \
--revision order-service--v1Step 6: Dapr Integration in .NET
// Program.cs — Dapr client for service invocation and pub/sub
builder.Services.AddDaprClient();
// Service-to-service calls (service discovery built in)
public class OrderNotificationService(DaprClient dapr)
{
public async Task NotifyShippedAsync(int orderId, CancellationToken ct)
{
// Calls notification-service via Dapr — no hard-coded URLs
await dapr.InvokeMethodAsync(
HttpMethod.Post,
"notification-service", // Dapr app ID
"api/notify/shipped",
new { OrderId = orderId },
ct);
}
}
// Pub/sub via Dapr
public class OrderEventPublisher(DaprClient dapr)
{
public async Task PublishOrderPlacedAsync(int orderId, CancellationToken ct)
=> await dapr.PublishEventAsync("pubsub", "order-placed",
new OrderPlacedEvent(orderId, DateTime.UtcNow), ct);
}
// Subscribe to events
[ApiController]
[Route("api/dapr")]
public class DaprSubscriptionController : ControllerBase
{
[Topic("pubsub", "order-placed")]
[HttpPost("order-placed")]
public async Task<IActionResult> HandleOrderPlaced(
OrderPlacedEvent @event,
[FromServices] IInventoryService inventory,
CancellationToken ct)
{
await inventory.ReserveAsync(@event.OrderId, ct);
return Ok(); // Dapr marks message as processed
}
}Step 7: GitHub Actions Deployment
# .github/workflows/deploy-aca.yml
- name: Deploy to Azure Container Apps
uses: azure/container-apps-deploy-action@v1
with:
appSourcePath: ${{ github.workspace }}
acrName: myregistry
containerAppName: order-service
resourceGroup: rg-orders
imageToDeploy: myregistry.azurecr.io/order-service:sha-${{ github.sha }}
targetPort: 8080
ingress: external
environmentVariables: |
ASPNETCORE_ENVIRONMENT=Production
OpenTelemetry__Endpoint=https://ingest.example.comInterview Answer
"Azure Container Apps is managed Kubernetes with a serverless pricing model — you deploy containers without writing K8s manifests. The environment is the shared infrastructure (networking, monitoring, Dapr), and each container app is a versioned, scalable unit. KEDA scaling rules let you scale to zero and back up on HTTP concurrency, queue depth, or any custom metric — important for cost control. Traffic splitting is built in: deploy a new revision, canary it at 10%, validate, promote to 100%, deactivate old revision — no nginx config. Dapr sidecar handles service discovery, pub/sub, and state management — your .NET code calls dapr.InvokeMethodAsync('other-service', ...) and Dapr resolves the current instance. Secrets: reference Azure Key Vault secrets directly in the Bicep/ARM config via keyVaultUrl + managed identity — secrets are never in your code or CI variables. For .NET apps: expose health endpoints at /health/live and /health/ready, set probes in Bicep so ACA can route around unhealthy instances during rolling deploys."
Enjoyed this article?
Explore the Cloud & DevOps learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.