How to deploy .NET Web API to Kubernetes

Introduction
Kubernetes has a reputation for being enormous, and it is - but the slice you need to get a .NET API running is small and worth learning. You need a container image, a place to push it, and two YAML objects: a Deployment that says "run three copies of this image" and a Service that gives those copies one stable address.
This video walks that exact path with an ASP.NET Core Web API. You containerize it with a multi-stage Dockerfile, push the image to a registry, apply a Deployment and a Service, and watch Kubernetes schedule three pods and load-balance across them. It also covers what self-heals for free once the app is running under Kubernetes.
🎬 Watch the full video here:
The multi-stage Dockerfile
The Dockerfile the .NET tooling generates uses four stages, and the split is the point:
base- the ASP.NET runtime image, the small one the app actually runs on.build- the full SDK image, used todotnet restoreanddotnet build.publish- runsdotnet publishto produce the trimmed output.final- copies only the publish output ontobase.
The result is that the SDK, your source, and the NuGet cache never ship. The final image contains the runtime and your compiled app and nothing else. One detail specific to the current .NET images: they run as a non-root app user by default and expose port 8080 (not 80 or 5000). Your Service targetPort has to match that.
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
Build and tag the image, then push it to a registry your cluster can pull from - Docker Hub in the video, but Azure Container Registry, GitHub Container Registry, or AWS ECR work the same way. The tag in your manifest (v1) must be a tag that actually exists in the registry.
The Deployment
A Deployment is a controller. You describe the desired state - "three pods running this image" - and it makes reality match, continuously.
apiVersion: apps/v1
kind: Deployment
metadata:
name: kubernetes-api-deployment
spec:
replicas: 3
selector:
matchLabels:
app: kubernetes-api
template:
metadata:
labels:
app: kubernetes-api
spec:
containers:
- name: kubernetes-api
image: <your-registry>/kubernetes-api:v1
ports:
- containerPort: 8080
The relationship that confuses people at first: spec.selector.matchLabels must match spec.template.metadata.labels. The Deployment finds and manages its pods by that label. The same label is what the Service uses to find pods to route to, so all three places (selector, pod labels, and the Service selector) carry app: kubernetes-api.
With this applied, kubectl get pods shows three pods. Kill one and Kubernetes starts a replacement within seconds - that is the self-healing you get without writing anything.
The Service
Pods are disposable and each gets its own internal IP that changes when the pod is replaced. A Service is the stable networking layer in front of them:
apiVersion: v1
kind: Service
metadata:
name: kubernetes-api-service
spec:
selector:
app: kubernetes-api
ports:
- protocol: TCP
port: 5000
targetPort: 8080
type: LoadBalancer
targetPort: 8080 is the container's port; port: 5000 is the port the Service exposes. type: LoadBalancer asks the cloud provider for an external load balancer with a public IP. On a local cluster (Docker Desktop, minikube, kind) that behaves differently - Docker Desktop maps it to localhost, minikube needs minikube tunnel - which is the usual "why is my EXTERNAL-IP stuck at pending" question.
Traffic then flows: external IP, to the Service, round-robined across the three pods.
Applying and inspecting
Everything is kubectl apply -f Deployment.yaml, then a handful of commands you will use constantly:
kubectl get pods/kubectl get deployments/kubectl get services- what exists and its status.kubectl describe pod <name>- events, the first place to look when a pod will not start.kubectl logs <pod>- your app's stdout, which is where ASP.NET Core logging goes in a container.kubectl delete pod <name>- prove the self-healing by watching it come back.
What running under Kubernetes gives you
- Self-healing - a crashed or deleted pod is recreated to hold
replicassteady. - Horizontal scale - change
replicasand re-apply, or set up a HorizontalPodAutoscaler. - Rolling updates - push
v2, update the image tag, re-apply, and the Deployment replaces pods gradually with rollback available. - Load balancing - the Service spreads traffic without you configuring anything.
Common pitfalls
- Port mismatch. Current .NET container images listen on
8080. If your ServicetargetPortsays80, connections hang. imagePullBackOff. The cluster cannot pull the image - wrong name, wrong tag, or a private registry with no image pull secret configured.- Label selectors that do not match. The Deployment reports zero ready pods, or the Service has no endpoints, because a label was mistyped in one of the three places it appears.
- Expecting
LoadBalancerto work identically everywhere. Local clusters need a tunnel or port mapping; the external IP will otherwise sit at<pending>. - Config and secrets baked into the image. Connection strings belong in ConfigMaps and Secrets injected as environment variables, not in
appsettings.jsoninside the container.
Key Takeaways
- A multi-stage Dockerfile keeps the SDK and source out of the final runtime image.
- .NET container images run as non-root and listen on port
8080- match that in your Service. - A Deployment continuously reconciles desired replica count; a Service gives those pods one stable address.
- The
app:label must match across the Deployment selector, the pod template, and the Service selector. type: LoadBalancergets a public IP in the cloud but needs a tunnel or port mapping locally.- Self-healing, rolling updates, and load balancing come for free once the app runs as a Deployment.
- Push configuration and secrets in through ConfigMaps and Secrets, never baked into the image.
Get the Full Source Code
The complete runnable solution - the ASP.NET Core Web API, the multi-stage Dockerfile, and the Deployment and Service manifests - is available to Patreon supporters. If you want to apply the manifests against your own cluster and watch three pods come up instead of rebuilding it from the walkthrough above, you can find it on Patreon.