All articles

Modern .NET Backend Roadmap — Part 7: Azure Deployment

Ship the container to Azure: choosing between App Service, Container Apps and AKS, pushing to a registry, secrets and managed identity, and capturing it all as Bicep.

0 · log in to like, save & follow Share on LinkedIn Share on X

The image from part 6 runs anywhere containers run. This part puts it on Azure — choosing the right compute service, pushing to a container registry, wiring configuration and secrets properly, and the cost/operations trade-offs the pricing page won't tell you. (The same decisions map almost one-to-one onto AWS and GCP; the service names change, the shape doesn't.)

Modern .NET Backend Roadmap — Part 7: Azure Deployment

Choosing the compute service

Azure offers at least five ways to run a container. For a typical API, the honest decision tree:

  • Azure App Service (Web App for Containers) — PaaS with the least ceremony: TLS, custom domains, deployment slots, easy scaling rules. Right default for "one API, steady traffic".
  • Azure Container Apps — serverless containers on managed Kubernetes underneath: scale-to-zero, KEDA-based event scaling, Dapr integration. Right default for "several services, spiky traffic" — and where microservices (part 9) most comfortably land.
  • AKS — real Kubernetes. Choose it when you need its ecosystem and have (or are) a platform team; you're signing up to operate a cluster.
  • A plain VM with Docker Compose — unfashionable and underrated for small workloads: geeksarray.com runs on exactly this (a $12/month droplet-class VM, Compose, Caddy for TLS) for a fraction of PaaS pricing. The trade is that patching, backups, and TLS renewals are your job.

This part deploys to Container Apps — it fits the series' trajectory.

Step 1: registry

az group create --name roadmap-rg --location centralindia
az acr create --resource-group roadmap-rg --name roadmapacr --sku Basic
az acr build --registry roadmapacr --image roadmap-api:v1 .

az acr build uploads the build context and runs your Dockerfile in Azure — no local Docker needed, and the image lands in the registry in one step. (It honors .dockerignore, which part 6 taught us to write early.)

Step 2: the app

az containerapp env create --name roadmap-env --resource-group roadmap-rg --location centralindia

az containerapp create \
  --name roadmap-api --resource-group roadmap-rg --environment roadmap-env \
  --image roadmapacr.azurecr.io/roadmap-api:v1 \
  --registry-server roadmapacr.azurecr.io --registry-identity system \
  --target-port 8080 --ingress external \
  --min-replicas 0 --max-replicas 3 \
  --secrets jwt-key=<your-32-char-secret> \
  --env-vars Jwt__Key=secretref:jwt-key ConnectionStrings__Default="<db-connection-string>"

The pieces that matter:

  • --target-port 8080 matches the ASPNETCORE_URLS we baked into the image. Ingress gives you HTTPS on a generated hostname immediately; bind a custom domain when ready.
  • --min-replicas 0 is scale-to-zero: a dev/staging app that costs nothing at night. For production APIs set --min-replicas 1+ — cold starts are real.
  • Secrets vs. env vars. Jwt__Key arrives as a secret reference, not a plain env var in your deployment scripts. The next step up is Azure Key Vault with managed identity, which removes the secret from the CLI command too.
  • Managed identity for the registry (--registry-identity system) — no registry passwords to store or rotate.

Step 3: the database

The SQLite file dies with the container — fine for the demo, wrong for production. Swap the connection string for Azure Database for PostgreSQL Flexible Server (or Azure SQL). Because part 1 isolated persistence behind ports and part 2 kept the provider choice in one line (UseSqliteUseNpgsql), this is an infrastructure-only change. Start with the burstable tier (~₹1,200/month); you can scale a database up much more easily than you can un-choose a serverless pricing model.

Configuration recap

The image never changes between environments. Everything environment-specific flows in as env vars/secrets, exactly as in part 6's local run:

ConnectionStrings__Default   → managed database
Jwt__Key                     → Key Vault / Container Apps secret
ASPNETCORE_ENVIRONMENT       → Production

If you catch yourself building a -prod image variant, stop — that's configuration trying to sneak into the artifact.

Operations you should turn on before traffic

  • Log streaming: az containerapp logs show --follow for the quick look; wire Log Analytics for retention.
  • Health probes: point liveness/readiness at a cheap endpoint so the platform restarts a wedged replica.
  • Revisions: Container Apps keeps immutable revisions per deploy — az containerapp revision list — giving you instant rollback (--revision traffic splitting even enables canaries).
  • Budget alert: az consumption budget create ... — a ₹2,000 alert email beats a surprise invoice. Cloud bills fail silently in the expensive direction.

Scaling rules that match how APIs actually load

Container Apps scales on KEDA rules, and the defaults are HTTP-centric:

az containerapp update --name roadmap-api --resource-group roadmap-rg \
  --scale-rule-name http-load --scale-rule-type http \
  --scale-rule-http-concurrency 60 \
  --min-replicas 1 --max-replicas 5

Sixty concurrent requests per replica is a sane starting point for an I/O-bound API; watch your p95 latency and tune. Two traps:

  • Scaling on CPU for an I/O-bound API. A .NET API waiting on the database shows 8% CPU while queueing requests — CPU rules never fire. Concurrency (or queue-depth, for workers) reflects real pressure.
  • Min replicas 0 in production. Scale-to-zero saves ~₹800/month and costs you a multi-second cold start on the request that matters — usually the demo to a client. Keep 1 warm replica for anything user-facing; save zero for staging.

And remember the database is the actual ceiling: five API replicas hammering a burstable Postgres just moves the queue. Scale reads with part 5's caching before scaling compute.

Custom domain and TLS

The generated *.azurecontainerapps.io hostname is fine for testing; production wants your domain:

az containerapp hostname add --hostname api.geeksarray.com \
  --name roadmap-api --resource-group roadmap-rg
az containerapp hostname bind --hostname api.geeksarray.com \
  --name roadmap-api --resource-group roadmap-rg --validation-method CNAME

You add the CNAME (and a TXT validation record) at your DNS provider, and Azure provisions and renews the certificate — the same "TLS becomes someone else's job" trade that Caddy gives you on a VM. The verification dance takes minutes; the certificate renewal you never think about again is the point.

What this actually costs

Rough monthly reality for this stack in Central India, small-but-real traffic:

Piece Tier ~Cost
Container Apps 1 warm replica, 0.5 vCPU/1 GiB ₹1,100–1,600
PostgreSQL Flexible Burstable B1ms ₹1,200–1,500
Container Registry Basic ₹400
Log Analytics first 5 GB free ₹0–500

Call it ₹3,000–4,000/month — versus ~₹1,000 for the VM-plus-Compose shape. What the extra buys: managed TLS and scaling, revisions with instant rollback, a database with automated backups/patching, and no 2 a.m. kernel updates. Whether that trade is worth 3× is a business question; knowing both shapes (this series has now built each) means you're choosing, not defaulting.

Infrastructure as code

Every command above is a one-off. The moment the environment works, capture it — Bicep is the natural Azure choice:

resource api 'Microsoft.App/containerApps@2024-03-01' = {
  name: 'roadmap-api'
  properties: {
    configuration: { ingress: { external: true, targetPort: 8080 } }
    template: {
      containers: [{ name: 'api', image: '${acr.properties.loginServer}/roadmap-api:v1' }]
      scale: { minReplicas: 1, maxReplicas: 3 }
    }
  }
}

az deployment group create --template-file main.bicep rebuilds the environment from nothing — which turns disaster recovery from a runbook into a command.

Next

Manual az acr build + az containerapp update works exactly until the second team member joins. Part 8 automates the whole path — build, test, image, deploy — with GitHub Actions, so the only deployment command left is git push.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.