
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Migrating .NET workloads to containers requires more than just a working Dockerfile; you need a configuration that respects cluster resources and orchestrator lifecycle events. To successfully run ASP.NET Core on Kubernetes, you must align your application’s startup behavior, memory management, and logging output with cloud-native expectations. This guide provides the exact multi-stage build patterns, manifest configurations, and operational guardrails I use in production environments to ensure .NET services remain stable under load.
mcr.microsoft.com/dotnet/aspnet:9.0-alpine runtime, configure HTTP health probes at /health, set explicit resource requests matching GC heap limits, and deploy via a Deployment and Service manifest targeting port 8080.How do you optimize an ASP.NET Core Dockerfile for Kubernetes?
The foundation of any reliable .NET deployment is the container image itself. A common mistake is using the SDK image for runtime or failing to leverage layer caching, resulting in slow CI pipelines and bloated images that increase node pull times. When you prepare to reduce Docker image size with multi-stage builds, the goal for .NET is typically an Alpine-based runtime image under 120MB.
In 2026, .NET 9 (and the upcoming .NET 10 preview) defaults to listening on port 8080 inside the container rather than port 80. This removes the need for root privileges, significantly improving security posture. Your Dockerfile should reflect this shift while separating build dependencies from the final artifact.
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
WORKDIR /src
COPY ["MyApp.csproj", "."]
RUN dotnet restore "MyApp.csproj"
COPY . .
RUN dotnet publish "MyApp.csproj" -c Release -o /app/publish --no-restore
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final
WORKDIR /app
COPY --from=build /app/publish .
ENV ASPNETCORE_HTTP_PORTS=8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"] This pattern ensures that source code changes don't invalidate the NuGet restore cache. The --no-restore flag in the publish step prevents redundant network calls. For teams managing sensitive compliance requirements, always scan this final image with Trivy or Grype before pushing to your registry to catch base OS vulnerabilities early.
What Kubernetes manifests are required for ASP.NET Core?
Once your image is built, you need declarative YAML to tell Kubernetes how to run it. While Helm charts are excellent for packaging, understanding the raw primitives is essential for debugging. At minimum, you need a Deployment for replica management and a Service for internal networking. If external access is required, pair these with an Ingress resource as detailed in my guide on Kubernetes ingress controllers explained.
Critical to .NET stability is the alignment of resource requests and limits. The .NET garbage collector reads cgroup limits to determine heap size. If you set a memory limit of 512Mi but request only 128Mi, the scheduler may place the pod on a node with insufficient headroom, leading to OOMKills during GC pressure spikes. Always set requests equal to limits for memory in stateless web apps to guarantee QoS class "Guaranteed".
apiVersion: apps/v1
kind: Deployment
metadata:
name: aspnet-core-app
spec:
replicas: 3
selector:
matchLabels:
app: aspnet-core-app
template:
metadata:
labels:
app: aspnet-core-app
spec:
containers:
- name: app
image: myregistry.com/myapp:v1.2.0
ports:
- containerPort: 8080
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production" Note the distinct paths for liveness and readiness. Liveness checks if the process is deadlocked; readiness checks if dependencies like databases are connected. Mixing these causes cascading failures when a downstream service blips, as Kubernetes will restart pods unnecessarily instead of simply removing them from the load balancer rotation.
How do you configure health checks and graceful shutdown?
Kubernetes assumes your application is disposable. To run ASP.NET Core on Kubernetes reliably, your app must signal its state explicitly. Without proper health endpoints, the orchestrator cannot distinguish between a busy server and a crashed one. Additionally, without graceful shutdown handling, in-flight requests will be terminated abruptly during deployments or scaling events.
Implementing Microsoft.AspNetCore.Diagnostics.HealthChecks
Add the health check middleware in Program.cs. Separate the liveness check (lightweight, no dependencies) from the readiness check (verifies DB, Redis, external APIs).
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" })
.AddNpgSql(connectionString, name: "postgres", tags: new[] { "ready" })
.AddRedis(redisConnection, name: "cache", tags: new[] { "ready" });
app.MapHealthChecks("/health", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("ready"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
}); Handling SIGTERM for Zero-Downtime Deploys
When Kubernetes stops a pod, it sends SIGTERM. ASP.NET Core handles this natively, but you must ensure long-running background tasks respect the cancellation token. Configure the shutdown timeout to match your terminationGracePeriodSeconds (default 30s) minus a safety buffer.
- Set
HostOptions.ShutdownTimeoutto 25 seconds in configuration. - Ensure all
IHostedServiceimplementations awaitstoppingToken. - Use
IAsyncDisposablefor cleaning up database connections or message consumers. - Avoid blocking calls in shutdown handlers; they can delay SIGKILL and cause data corruption.
How does ASP.NET Core autoscaling compare to native Kestrel tuning?
Scaling in Kubernetes operates on two levels: cluster-level (HPA/VPA) and application-level (Kestrel thread pool). Misconfiguring either leads to latency spikes or wasted spend. Understanding the interaction between horizontal pod autoscaling in Kubernetes and .NET runtime metrics is critical for performance.
| Scaling Dimension | Primary Metric | .NET Consideration | Risk if Misconfigured |
|---|---|---|---|
| Horizontal Pod Autoscaler | CPU / Custom Metrics | CPU usage often lags behind thread saturation | Scale-up too late during burst traffic |
| Kestrel Thread Pool | ThreadPool.ThreadCount | Default min threads may be too low for high concurrency | Request queuing despite low CPU |
| Memory Limits | Working Set Bytes | GC Heap respects cgroup limits in .NET 9+ | OOMKill if GC cannot reclaim fast enough |
| Vertical Pod Autoscaler | Historical Usage | Changes require pod restart (disruptive) | Downtime during recommendation application |
For most ASP.NET Core APIs, CPU-based HPA works well because Kestrel is highly efficient. However, if your app performs heavy synchronous I/O or complex serialization, monitor System.Threading.ThreadPool.QueueLength via OpenTelemetry. Exposing this as a Prometheus metric allows HPA to scale based on actual request backlog rather than proxy CPU usage. Set your HPA stabilization window to prevent flapping; .NET apps typically need 60–90 seconds to warm up JIT-compiled code paths after startup.
How do you manage secrets and configuration securely?
Never bake connection strings or API keys into your Docker image. When you run ASP.NET Core on Kubernetes, configuration should be injected at runtime. Use ConfigMaps for non-sensitive settings and Secrets for credentials. For production-grade security, integrate with external secret stores as described in Kubernetes secrets management done right.
ASP.NET Core’s configuration provider hierarchy automatically maps environment variables to settings. A variable named ConnectionStrings__Default overrides the corresponding JSON key. For larger configurations, mount ConfigMaps as files in /app/config and add them as optional sources. This avoids hitting environment variable length limits in some container runtimes.
env:
- name: ConnectionStrings__Default
valueFrom:
secretKeyRef:
name: app-db-secret
key: connection-string
- name: Logging__LogLevel__Default
valueFrom:
configMapKeyRef:
name: app-config
key: log-level
volumeMounts:
- name: config-volume
mountPath: /app/config/appsettings.Production.json
subPath: appsettings.Production.json
readOnly: true Always mark secret volumes as readOnly: true to prevent accidental writes. Enable .NET User Secrets only for local development; in Kubernetes, rely entirely on the orchestrator’s injection mechanism. Audit your manifests regularly to ensure no plaintext secrets leak into annotations or labels, which are stored unencrypted in etcd.
Next Steps for Production ASP.NET Core Deployments
Successfully operating .NET in containers requires treating the runtime as a first-class citizen of the orchestration layer, not an afterthought. By optimizing your Dockerfile, aligning resource boundaries with GC behavior, implementing granular health checks, and securing configuration injection, you create a foundation that survives real-world traffic and audit scrutiny. Continue hardening your stack by exploring OpenTelemetry observability standards to gain deep visibility into .NET internals without vendor lock-in. If your team needs help validating these patterns against SOC 2 or ISO 27001 requirements, reach out to discuss your infrastructure and ensure your migration is both performant and compliant.