Azure Container Apps for .NET Developers
Azure Container Apps sits in the sweet spot between Azure Functions and Kubernetes. You get container-based deployment with automatic scaling, built-in ingress, and Dapr integration — without managing a cluster.
For .NET developers, it's particularly compelling. You containerise your ASP.NET Core app or background worker, deploy it, and Container Apps handles HTTPS termination, load balancing, and scaling to zero.
Containerising a .NET Application
.NET 8+ includes built-in container publishing. No Dockerfile needed:
dotnet publish -c Release /t:PublishContainer \
-p ContainerImageName=myregistry.azurecr.io/my-api \
-p ContainerImageTag=1.0.0
If you prefer a Dockerfile for more control:
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ["MyApi/MyApi.csproj", "MyApi/"]
RUN dotnet restore "MyApi/MyApi.csproj"
COPY . .
RUN dotnet publish "MyApi/MyApi.csproj" -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /app
COPY --from=build /app .
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApi.dll"]
Container Apps expects your app to listen on port 8080 by default (configurable). .NET 8+ defaults to port 8080 in containers, so this aligns naturally.
Deploying with the Azure CLI
Create a Container Apps environment and deploy:
az containerapp env create \
--name my-env \
--resource-group my-rg \
--location uksouth
az containerapp create \
--name my-api \
--resource-group my-rg \
--environment my-env \
--image myregistry.azurecr.io/my-api:1.0.0 \
--target-port 8080 \
--ingress external \
--min-replicas 0 \
--max-replicas 10 \
--registry-server myregistry.azurecr.io \
--registry-identity system
The --registry-identity system flag uses the container app's managed identity to pull images from ACR. No registry passwords needed.
Scaling Rules
Container Apps uses KEDA under the hood for event-driven scaling. HTTP scaling is built in:
az containerapp update \
--name my-api \
--resource-group my-rg \
--scale-rule-name http-rule \
--scale-rule-type http \
--scale-rule-http-concurrency 50
This scales based on concurrent HTTP requests. At 50 concurrent requests per replica, the platform adds more replicas up to your maximum.
For queue-based scaling, you can trigger on Azure Service Bus:
az containerapp update \
--name my-worker \
--resource-group my-rg \
--scale-rule-name queue-rule \
--scale-rule-type azure-servicebus \
--scale-rule-metadata "queueName=orders" "messageCount=5" \
--scale-rule-auth "connection=servicebus-connection"
Health Probes
Configure health probes so Container Apps knows when your application is ready:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy());
var app = builder.Build();
app.MapHealthChecks("/health/ready");
app.MapHealthChecks("/health/live");
Then configure the probes in your container app definition. The platform uses liveness probes to restart unhealthy containers and readiness probes to control traffic routing.
Environment Variables and Secrets
Inject configuration via environment variables and secrets:
az containerapp update \
--name my-api \
--resource-group my-rg \
--set-env-vars "ASPNETCORE_ENVIRONMENT=Production" \
--secrets "db-connection=keyvaultref:https://myvault.vault.azure.net/secrets/DbConnection,identityref:/subscriptions/.../identity"
In your .NET app, these are standard environment variables:
builder.Configuration.AddEnvironmentVariables();
Secrets from Key Vault references are resolved by the platform and injected as environment variables. Your app never sees the Key Vault URI.
Revisions and Traffic Splitting
Container Apps supports multiple revisions running simultaneously. This enables blue-green deployments:
az containerapp update \
--name my-api \
--resource-group my-rg \
--image myregistry.azurecr.io/my-api:2.0.0 \
--revision-suffix v2
az containerapp ingress traffic set \
--name my-api \
--resource-group my-rg \
--revision-weight "my-api--v1=80" "my-api--v2=20"
Route 20% of traffic to the new version, monitor, then shift to 100% when confident. Rollback is instant — just shift traffic back.
Background Workers
Not everything is a web API. Container Apps handles background workers too:
public class OrderWorker : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public OrderWorker(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await using var scope = _scopeFactory.CreateAsyncScope();
var processor = scope.ServiceProvider.GetRequiredService<IOrderProcessor>();
await processor.ProcessPendingOrdersAsync(stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
}
}
Deploy without ingress (--ingress none) and scale based on queue depth or custom metrics.
When to Choose Container Apps
Use Container Apps when Azure Functions is too constrained (you need WebSockets, long-running processes, or specific runtime versions) but Kubernetes is too much operational overhead. It's the containerised middle ground, and for most .NET web APIs and background workers, it's the right default choice.