I have shipped .NET APIs with Docker Compose for years. It works well on a single machine. You define your services, run docker-compose up, and everything connects. Simple.
Then the traffic grows. Or the team grows. Or someone asks: "What happens if that container crashes at 3am?" Compose doesn't have a great answer. Kubernetes does.
This is not a beginner Kubernetes tutorial. It is about the specific shift in thinking that .NET developers face when they stop running containers locally and start running them in a real cluster.
What Compose Gives You (And What It Doesn't)
Docker Compose is excellent at defining how services relate to each other. A web API, a Redis cache, a SQL Server instance. You write one YAML file, and it all starts together. Local development becomes reproducible.
But Compose is not designed for production. It has no concept of desired state. If a container dies, Compose does not restart it automatically (unless you set restart: always, which is a blunt instrument). There is no load balancing, no rolling update, no health-check driven traffic shifting.
Kubernetes solves all of this, but it asks you to think in a completely different way.
The Mental Model Shift
In Compose, you think about containers. In Kubernetes, you think about desired state.
You declare what you want. "I want three replicas of this API running, on port 80, with at least 512MB of memory each." Kubernetes figures out how to make that happen, and keeps making it happen even when nodes fail, containers crash, or you deploy a new version.
For .NET developers, this means your application needs to be genuinely stateless. If you are storing session data in memory, you will have a bad time the moment Kubernetes load-balances a second request to a different pod. Move session state to Redis or a database before you touch K8s.
Translating Your Compose File
Here is a typical Compose service for a .NET API:
services:
api:
image: myregistry.azurecr.io/my-api:latest
ports:
- "8080:80"
environment:
- ConnectionStrings__Default=Server=db;Database=mydb;...
depends_on:
- dbIn Kubernetes, this becomes at minimum three resources: a Deployment (defines your pods and replicas), a Service (exposes them inside the cluster), and an Ingress (routes external traffic in). You also move your connection strings into a Secret.
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-api
spec:
replicas: 3
selector:
matchLabels:
app: my-api
template:
metadata:
labels:
app: my-api
spec:
containers:
- name: my-api
image: myregistry.azurecr.io/my-api:latest
ports:
- containerPort: 80
env:
- name: ConnectionStrings__Default
valueFrom:
secretKeyRef:
name: my-api-secrets
key: connection-string
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"The resources block is not optional. Without it, a single runaway process can starve the entire node. Set requests low enough to schedule efficiently and limits high enough to handle load spikes.
Health Checks Are Not Optional
Kubernetes needs to know when your pod is ready to receive traffic and when it has gone wrong. That is what liveness and readiness probes are for.
In .NET, add the health check middleware:
builder.Services.AddHealthChecks();
app.MapHealthChecks("/healthz");Then wire it into your Deployment:
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 5
periodSeconds: 10The readiness probe tells Kubernetes not to send traffic to a pod until it is actually ready. The liveness probe tells Kubernetes to restart the pod if it gets stuck. Both matter in production.
Rolling Updates Without Downtime
One of the best things Kubernetes gives you by default is rolling updates. When you push a new image, it starts new pods, waits until they pass readiness checks, then terminates the old ones. Your users never see a gap.
To make this work reliably, pin your image tags. Never use :latest in a production Deployment. Use a specific version or commit SHA. Otherwise Kubernetes may not know a new version exists, and you lose the audit trail of what is actually running.
What Azure Kubernetes Service Adds
If you are already on Azure with .NET apps, AKS is the natural home for this. You get managed control plane, integration with Azure Container Registry for your images, and Azure Active Directory for cluster authentication.
The az aks get-credentials command wires your local kubectl directly to your cluster. From there, the same YAML you tested locally works in production.
AKS also integrates with Azure Monitor and Application Insights. Your existing .NET telemetry keeps working. You just point the instrumentation key at the right resource.
The Honest Trade-Off
Kubernetes is more complex than Compose. The YAML surface area alone is intimidating. You will spend time debugging pod scheduling, resource limits, and ingress controller configuration. That cost is real.
But the reliability gains are also real. Self-healing deployments, horizontal scaling, zero-downtime releases. For anything that people actually depend on, the trade-off is usually worth it.
Start small. Migrate one service. Get comfortable with kubectl get pods, kubectl logs, and kubectl describe. Those three commands will tell you most of what you need to know when something goes wrong.



