
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Moving from traditional IIS hosting to containers introduces specific challenges around startup time, configuration injection, and signal handling. When you deploy a .NET service to Kubernetes, success depends less on the cluster itself and more on how well your container image and manifests respect cloud-native constraints. This guide covers the exact patterns I use in production to run ASP.NET Core workloads reliably on EKS, AKS, and GKE.
How do you containerize a .NET application for Kubernetes?
The foundation of any reliable deploy a .NET service to Kubernetes workflow is a properly constructed container image. Never use the SDK image as your runtime base; it adds hundreds of megabytes of unnecessary tooling and increases your attack surface. In 2026, the standard is a multi-stage build targeting the mcr.microsoft.com/dotnet/aspnet:9.0-noble-chiseled image. Chiseled images contain only the minimal OS packages required to run .NET, eliminating package managers and shells entirely.
Multi-stage Dockerfile for production
This Dockerfile compiles your application in the SDK stage and copies only the published output to the runtime stage. Note the explicit port 8080, which aligns with .NET 9's default non-root configuration.
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ["MyService.csproj", "."]
RUN dotnet restore "MyService.csproj"
COPY . .
RUN dotnet publish "MyService.csproj" -c Release -o /app/publish --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:9.0-noble-chiseled AS final
WORKDIR /app
EXPOSE 8080
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyService.dll"] A common mistake is running as root. The chiseled images default to a non-root user (UID 1654), which satisfies most pod security standards without additional configuration. If your application writes to disk, ensure directories are owned by this UID or use a tmpfs mount. For teams managing multiple microservices, consider packaging these manifests into reusable templates as described in our Helm chart templating deep dive.
What Kubernetes manifests are needed for a .NET deployment?
You need at minimum a Deployment and a Service. The Deployment manages replica count, update strategy, and pod template specification. The Service provides stable networking within the cluster. Below is a production-ready Deployment manifest incorporating lessons from years of operating .NET workloads.
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-dotnet-service
labels:
app: my-dotnet-service
spec:
replicas: 3
selector:
matchLabels:
app: my-dotnet-service
template:
metadata:
labels:
app: my-dotnet-service
spec:
containers:
- name: app
image: registry.example.com/my-dotnet-service:v1.4.2
ports:
- containerPort: 8080
env:
- name: ConnectionStrings__Default
valueFrom:
secretKeyRef:
name: db-credentials
key: connection-string
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "1000m" Always set both resource requests and limits. Without them, the scheduler cannot make informed placement decisions, and a single runaway pod can starve its node. Refer to Kubernetes resource limits and requests for sizing guidance specific to .NET memory profiles. The GC in .NET 9 respects cgroup v2 limits natively, so container memory limits now work predictably without legacy environment variable hacks.
How do you configure health probes for ASP.NET Core?
Health probes are non-negotiable. Without them, Kubernetes cannot distinguish between a healthy pod and one stuck in a deadlock, leading to silent failures. ASP.NET Core provides built-in middleware via Microsoft.AspNetCore.Diagnostics.HealthChecks. Register it in Program.cs:
builder.Services.AddHealthChecks()
.AddSqlServer(connectionString)
.AddRedis(redisConnection);
app.MapHealthChecks("/health/live", new HealthCheckOptions {
Predicate = _ => false
});
app.MapHealthChecks("/health/ready"); The liveness endpoint returns 200 if the process is responsive, regardless of downstream dependencies. The readiness endpoint includes database and cache checks. Configure three distinct probes in your manifest:
- Startup probe: Allows up to 150 seconds (30 failures × 5s period) for JIT compilation and initial connections. Prevents liveness kills during cold start.
- Readiness probe: Checks every 10 seconds. Removes pod from Service endpoints on failure, stopping new traffic.
- Liveness probe: Checks every 15 seconds. Restarts the container only if the process itself is unresponsive.
Never point liveness at a database-dependent endpoint. Transient DB outages would cascade into pod restarts, making recovery slower. Understanding this distinction prevents many 3 AM incidents. For deeper debugging when probes fail unexpectedly, see debugging CrashLoopBackOff in Kubernetes.
How do you manage secrets and configuration securely?
Hardcoding connection strings in images or manifests is a critical security failure. Use Kubernetes Secrets for sensitive values and ConfigMaps for non-sensitive settings. Inject them as environment variables for simple cases or volume mounts for structured configuration files like appsettings.json overrides.
| Approach | Best For | Security Level | Complexity |
|---|---|---|---|
| Environment Variables | Simple key-value pairs, connection strings | Medium (visible in pod spec) | Low |
| Mounted Secret Volumes | Certificates, complex config files | High (not in env, file permissions) | Medium |
| External Secrets Operator | Vault/AWS SM/Azure KV integration | Highest (synced, audited, rotated) | High |
| Sealed Secrets / SOPS | GitOps-friendly encrypted manifests | High (decrypts in-cluster only) | Medium |
For SOC 2 or ISO 27001 compliance, prefer External Secrets Operator or sealed secrets. These keep plaintext secrets out of Git and provide audit trails. Environment variables are acceptable for staging but should be avoided for production PII or financial data. Always enable RBAC restrictions on Secret access; see Kubernetes RBAC: secure your cluster for least-privilege patterns.
How do you expose and scale a .NET service in production?
Internal Services handle east-west traffic, but external access requires an Ingress controller. NGINX Ingress and Traefik are the most common choices for .NET workloads. Configure TLS termination at the Ingress level using cert-manager for automated certificate management. Your Ingress resource should specify path-based routing and appropriate timeouts:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-dotnet-ingress
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- api.example.com
secretName: api-tls
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-dotnet-service
port:
number: 8080 Scaling requires Horizontal Pod Autoscaler (HPA) configured against meaningful metrics. CPU utilization alone is often insufficient for .NET services where memory pressure or request latency matters more. Define custom metrics via Prometheus Adapter when possible. Set conservative min/max replica bounds to prevent runaway scaling during incident conditions. Test your autoscaling behavior under load before relying on it in production.
Deploy a .NET Service to Kubernetes Reliably
Successfully running .NET in Kubernetes requires attention to image construction, probe design, secret hygiene, and scaling policy. Each layer compounds: a poorly built image undermines even perfect manifests, and missing probes negate sophisticated autoscaling. Start with the chiseled multi-stage Dockerfile, add all three probe types, externalize secrets properly, and validate under realistic load before going live. If your team needs hands-on support architecting or auditing a .NET Kubernetes migration, reach out directly to discuss your specific workload requirements and compliance constraints.