Skip to main content

Deploy .NET Microservices to Azure Kubernetes Service (AKS)

Learn how to deploy .NET microservices to Azure Kubernetes Service (AKS) step by step with Docker, YAML manifests, and best practices. Start deploying today.

If you are building .NET microservices and want to run them in the cloud with automatic scaling, self-healing, and zero-downtime deployments, Azure Kubernetes Service (AKS) is the most popular choice for .NET teams. AKS is Microsoft's managed Kubernetes offering: Azure runs the control plane for you (for free), and you only pay for the worker nodes your containers run on. In this Azure Kubernetes Service tutorial, you will containerize an ASP.NET Core microservice, push it to Azure Container Registry, create an AKS cluster, and deploy it with Kubernetes YAML manifests — plus the best practices and pitfalls that separate a demo from a production-ready deployment.

What Is Azure Kubernetes Service and Why Use It for .NET Microservices?

Kubernetes is an open-source container orchestrator. It schedules containers onto machines, restarts them when they crash, load-balances traffic, and rolls out new versions gradually. Running Kubernetes yourself is hard: you have to manage etcd, the API server, certificates, and upgrades. AKS removes that burden.

Why does this matter for .NET microservices specifically?

  • Independent scaling: Your order service can scale to 20 pods during a sale while the reporting service stays at 2. A monolith cannot do that.
  • Fast, safe releases: Rolling updates replace pods one at a time and roll back automatically if health checks fail.
  • First-class .NET support: Microsoft publishes official, hardened Linux images for ASP.NET Core, and .NET 8/9/10 are tuned for containers (cgroup-aware GC, smaller chiseled images).
  • Azure integration: Managed identities, Azure Key Vault, Azure Monitor, and Azure Container Registry (ACR) plug in with a single CLI flag.

Prerequisites

  • An Azure subscription (a free account works for this tutorial)
  • Azure CLI 2.60 or later
  • Docker Desktop (or any Docker engine)
  • .NET 8 SDK or later
  • kubectl — install it with az aks install-cli

Step 1: Create a .NET Microservice

We will build a small "Products" API using minimal APIs. The important additions for Kubernetes are health check endpoints, which Kubernetes uses to decide whether a pod is alive and ready to receive traffic.

dotnet new webapi -n ProductService --no-openapi
cd ProductService

Replace Program.cs with the following:

using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;

var builder = WebApplication.CreateBuilder(args);

// Health checks: "live" means the process is running; "ready" means it can serve traffic.
builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" })
    .AddCheck("startup-warmup", () => HealthCheckResult.Healthy(), tags: new[] { "ready" });

var app = builder.Build();

var products = new List<Product>
{
    new(1, "Keyboard", 49.99m),
    new(2, "Monitor", 229.00m),
    new(3, "Headset", 89.50m)
};

app.MapGet("/api/products", () => Results.Ok(products));

app.MapGet("/api/products/{id:int}", (int id) =>
    products.FirstOrDefault(p => p.Id == id) is { } product
        ? Results.Ok(product)
        : Results.NotFound());

// Expose the pod name so you can watch load balancing across replicas.
app.MapGet("/api/whoami", () =>
    Results.Ok(new { Host = Environment.MachineName, Version = "1.0.0" }));

app.MapHealthChecks("/healthz/live", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("live")
});

app.MapHealthChecks("/healthz/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});

app.Run();

public record Product(int Id, string Name, decimal Price);

Why two health endpoints? A liveness failure tells Kubernetes to restart the container. A readiness failure tells it to stop sending traffic but leave the container alone. If you point liveness at a check that depends on your database, a database outage will cause Kubernetes to restart-loop every pod — making the outage worse. Keep liveness trivial, and put dependency checks under readiness.

Step 2: Write a Production-Ready Dockerfile

Use a multi-stage build so the final image contains only the runtime and your compiled output, not the SDK. Also run as a non-root user — a requirement in most enterprise clusters.

# Stage 1: build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Copy the csproj first so package restore is cached between builds
COPY ProductService.csproj .
RUN dotnet restore

COPY . .
RUN dotnet publish -c Release -o /app/publish --no-restore

# Stage 2: runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .

# Listen on 8080 (non-privileged port) and run as the built-in 'app' user
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
USER app

ENTRYPOINT ["dotnet", "ProductService.dll"]

Build and test it locally before touching the cloud:

docker build -t productservice:1.0.0 .
docker run --rm -p 8080:8080 productservice:1.0.0
curl http://localhost:8080/api/products

Step 3: Push the Image to Azure Container Registry

AKS pulls images from a registry. Azure Container Registry (ACR) is the natural choice because AKS can authenticate to it with a managed identity — no image-pull secrets to rotate.

az login
az group create --name rg-dotnet-aks --location eastus

# Registry names must be globally unique and alphanumeric
az acr create --resource-group rg-dotnet-aks --name acrdotnetdemo123 --sku Basic

# Build in the cloud (no local Docker needed) and push in one step
az acr build --registry acrdotnetdemo123 --image productservice:1.0.0 .

Step 4: Create the AKS Cluster

The --attach-acr flag grants the cluster's identity AcrPull permission on your registry. This is the single most common thing beginners forget, and it causes the dreaded ImagePullBackOff error.

az aks create \
  --resource-group rg-dotnet-aks \
  --name aks-dotnet-demo \
  --node-count 2 \
  --node-vm-size Standard_B2s \
  --enable-managed-identity \
  --attach-acr acrdotnetdemo123 \
  --generate-ssh-keys

# Download credentials so kubectl talks to your new cluster
az aks get-credentials --resource-group rg-dotnet-aks --name aks-dotnet-demo
kubectl get nodes

Cluster creation takes 3–5 minutes. When kubectl get nodes shows two nodes in Ready state, you are set.

Step 5: Deploy .NET Microservices to Kubernetes with YAML

A Kubernetes deployment for a .NET microservice needs two objects: a Deployment (how many pods, which image, health probes, resource limits) and a Service (a stable network endpoint that load-balances across pods). Save this as k8s/productservice.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: productservice
  labels:
    app: productservice
spec:
  replicas: 3
  selector:
    matchLabels:
      app: productservice
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  template:
    metadata:
      labels:
        app: productservice
    spec:
      containers:
        - name: productservice
          image: acrdotnetdemo123.azurecr.io/productservice:1.0.0
          ports:
            - containerPort: 8080
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: "Production"
            - name: DOTNET_gcServer
              value: "0"
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"
          livenessProbe:
            httpGet:
              path: /healthz/live
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /healthz/ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: productservice
spec:
  type: LoadBalancer
  selector:
    app: productservice
  ports:
    - port: 80
      targetPort: 8080

Apply it and watch the rollout:

kubectl apply -f k8s/productservice.yaml
kubectl rollout status deployment/productservice
kubectl get service productservice --watch

Once the EXTERNAL-IP column changes from <pending> to a real address, call it a few times. You will see different pod names come back, proving traffic is spread across replicas:

for i in 1 2 3 4 5; do curl -s http://<EXTERNAL-IP>/api/whoami; echo; done

Why the resource limits matter for .NET

The .NET garbage collector reads the container's memory limit and sizes its heap accordingly. Without a limit, a single pod can consume the whole node and get evicted. Set DOTNET_gcServer=0 (workstation GC) for small pods with under 1 CPU — Server GC creates one heap per core and wastes memory when CPU is throttled.

Step 6: Roll Out a New Version with Zero Downtime

Change the version string in /api/whoami to "1.1.0", build a new tag, and update the deployment:

az acr build --registry acrdotnetdemo123 --image productservice:1.1.0 .
kubectl set image deployment/productservice productservice=acrdotnetdemo123.azurecr.io/productservice:1.1.0
kubectl rollout status deployment/productservice

# Something wrong? Roll back instantly.
kubectl rollout undo deployment/productservice

Because we set maxUnavailable: 0, Kubernetes starts a new pod, waits for its readiness probe to pass, and only then terminates an old one. Users never see an error.

Step 7: Configuration and Secrets the Right Way

Never bake connection strings into images. Use a ConfigMap for non-sensitive settings and a Secret for credentials. ASP.NET Core reads environment variables automatically, using __ (double underscore) as the section separator:

apiVersion: v1
kind: Secret
metadata:
  name: productservice-secrets
type: Opaque
stringData:
  ConnectionStrings__Products: "Server=tcp:mysql.database.windows.net;Database=Products;..."

Then reference it in the container spec:

          envFrom:
            - secretRef:
                name: productservice-secrets

In your code, builder.Configuration.GetConnectionString("Products") just works. For production, go one step further and use the Azure Key Vault Secrets Store CSI driver, so secrets never live in the cluster at all.

Step 8: Autoscale Your .NET Microservice

A Horizontal Pod Autoscaler adds pods when CPU crosses a threshold. Because we declared CPU requests, Kubernetes can compute utilization percentages:

kubectl autoscale deployment productservice --cpu-percent=70 --min=2 --max=10
kubectl get hpa

Pair this with the AKS cluster autoscaler (az aks update --enable-cluster-autoscaler --min-count 2 --max-count 5) so new nodes appear when pods have nowhere to run.

Azure Kubernetes Service Best Practices for .NET Teams

  • Use an Ingress controller instead of one LoadBalancer per service. Each type: LoadBalancer service allocates a public IP. With ten microservices that gets expensive and messy. Enable the AKS Application Routing add-on (managed NGINX) and route by path or host.
  • Pin image tags, never use latest. latest makes rollbacks impossible and hides which version is actually running.
  • Handle SIGTERM gracefully. ASP.NET Core's generic host already listens for shutdown signals and drains in-flight requests. Don't override ShutdownTimeout to zero, and set terminationGracePeriodSeconds to match.
  • Log to stdout as JSON. Azure Monitor Container Insights collects console output automatically. Use Serilog with a compact JSON formatter so logs are queryable in Log Analytics.
  • Add OpenTelemetry. Distributed tracing is the only sane way to debug a request that crosses five microservices. .NET's OpenTelemetry packages export to Azure Monitor in a few lines.
  • Use Helm or Kustomize once you have more than two services. Copy-pasting YAML across services drifts fast.
  • Automate with GitHub Actions or Azure DevOps. A pipeline that builds, pushes to ACR, and runs kubectl apply (or a GitOps tool like Argo CD / Flux) keeps deployments repeatable.

Common Pitfalls When Deploying .NET to AKS

  • ImagePullBackOff: The cluster cannot authenticate to ACR. Run az aks update --attach-acr <name> and check the image name is spelled exactly as it appears in ACR.
  • CrashLoopBackOff: The app is dying at startup. Run kubectl logs <pod> --previous to see the exception. Common causes: missing configuration, or binding to port 80 as a non-root user (use 8080).
  • Pods stuck in Pending: Resource requests exceed what nodes offer. Run kubectl describe pod <pod> and look at the Events section.
  • OOMKilled: The memory limit is too low for the .NET heap. Raise it or reduce allocations; check kubectl top pods for real usage.
  • Liveness probe too aggressive: If the app takes 20 seconds to warm up and initialDelaySeconds is 5, Kubernetes kills it before it ever starts. Use a startupProbe for slow-starting services.
  • Data Protection keys not shared: With multiple replicas, ASP.NET Core antiforgery tokens and auth cookies will randomly fail unless you persist Data Protection keys to Azure Blob Storage or Key Vault.

Clean Up

AKS nodes bill by the hour. When you are done experimenting, delete the whole resource group:

az group delete --name rg-dotnet-aks --yes --no-wait

Conclusion: Key Takeaways

Deploying .NET microservices to Azure Kubernetes Service is far less intimidating once you understand the handful of building blocks involved. Here is what to remember:

  • Azure Kubernetes Service gives you a managed control plane for free; you only pay for worker nodes.
  • Containerize with a multi-stage Dockerfile, run as non-root, and listen on port 8080.
  • Push images to Azure Container Registry and attach it to the cluster with --attach-acr.
  • Every Deployment needs separate liveness and readiness probes plus CPU/memory requests and limits — the .NET GC depends on them.
  • Use rolling updates with maxUnavailable: 0 for zero-downtime releases, and kubectl rollout undo when things go wrong.
  • Keep configuration in ConfigMaps and Secrets (or Key Vault), autoscale with HPA, and centralize traffic through an Ingress controller.

With this foundation in place, you can add more microservices, wire in a message broker like Azure Service Bus, and build out a full CI/CD pipeline — all on the same AKS cluster you created today.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...