Azure Container Apps sits in a useful middle ground: you get container-level control without managing Kubernetes directly, plus built-in autoscaling (including scale-to-zero), traffic splitting, and Dapr integration if you want it. Here's how we containerize and ship a .NET 10 API to it.

Key takeaways
  • Use a multi-stage Dockerfile so the final image only contains the published output, not the SDK.
  • Scale rules are KEDA-based — you can scale on HTTP concurrency, queue depth, or CPU, including down to zero replicas.
  • Pull secrets from Key Vault via a managed identity rather than baking them into environment variables or the image.
  • Use revisions and traffic splitting for blue-green or canary rollouts instead of replacing a single running revision.

A multi-stage Dockerfile

dockerfile
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["OrderApi/OrderApi.csproj", "OrderApi/"]
RUN dotnet restore "OrderApi/OrderApi.csproj"
COPY . .
WORKDIR /src/OrderApi
RUN dotnet publish -c Release -o /app/publish --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "OrderApi.dll"]

Provisioning the environment

A Container Apps environment groups your apps into a shared network boundary with a Log Analytics workspace. Create it once per environment (dev/staging/prod), then deploy apps into it.

bash
az containerapp env create \
  --name order-api-env \
  --resource-group order-api-rg \
  --location eastus

az containerapp create \
  --name order-api \
  --resource-group order-api-rg \
  --environment order-api-env \
  --image myregistry.azurecr.io/order-api:latest \
  --target-port 8080 \
  --ingress external \
  --min-replicas 0 \
  --max-replicas 10

Scaling rules

Container Apps uses KEDA under the hood, so you can scale on HTTP concurrency, a queue's message count, or CPU/memory — including down to zero replicas when there's no traffic, which matters for internal or low-traffic APIs.

json
{
  "scale": {
    "minReplicas": 0,
    "maxReplicas": 10,
    "rules": [
      {
        "name": "http-concurrency",
        "http": { "metadata": { "concurrentRequests": "50" } }
      }
    ]
  }
}
Watch forScale-to-zero means the first request after idle time pays a cold-start cost while a replica starts. For latency-sensitive, customer-facing traffic, set minReplicas to at least 1 and let scale-to-zero apply only to genuinely bursty internal workloads.

Secrets via managed identity, not environment variables

Reference Key Vault secrets directly rather than copying secret values into Container Apps' own secret store or plain environment variables. Grant the app's managed identity get access on the vault, then reference the secret by URI.

bash
az containerapp identity assign --name order-api --resource-group order-api-rg --system-assigned

az keyvault set-policy \
  --name order-api-kv \
  --object-id <managed-identity-principal-id> \
  --secret-permissions get

az containerapp secret set \
  --name order-api \
  --resource-group order-api-rg \
  --secrets "db-connection=keyvaultref:https://order-api-kv.vault.azure.net/secrets/db-connection,identityref:system"

Revisions and traffic splitting

Every deployment creates a new revision. Instead of cutting traffic over instantly, split it between the old and new revision and shift gradually while you watch error rates and latency.

bash
az containerapp ingress traffic set \
  --name order-api \
  --resource-group order-api-rg \
  --revision-weight order-api--v2=20 order-api--v1=80

Once the new revision has proven itself at partial traffic, shift the remaining weight over and deactivate the old revision. If something looks wrong, shifting weight back is a single command, not a redeploy.

Need help with this in production?

astrivo helps teams design, build, and modernize systems like this. Let's talk about your context.