Run ASP.NET Core on Kubernetes

Khimananda Oli 8 min read Programming and Languages
Run ASP.NET Core on Kubernetes

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.

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.

SDK Stagedotnet/sdk:9.0-alpineRestore & BuildPublish Release/app/publishRuntime Stageaspnet:9.0-alpineCopy ArtifactsPort 8080 (Non-root)Kubernetes NodeContainer RuntimeImage Pull & StartHealth Probes Active
Optimized multi-stage build pipeline reducing image size and attack surface for ASP.NET Core on Kubernetes

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.ShutdownTimeout to 25 seconds in configuration.
  • Ensure all IHostedService implementations await stoppingToken.
  • Use IAsyncDisposable for cleaning up database connections or message consumers.
  • Avoid blocking calls in shutdown handlers; they can delay SIGKILL and cause data corruption.
KubeletASP.NET CoreDependenciesGET /health200 OKGET /health/readyCheck DB/RedisConnected200 ReadySIGTERMDrain Requests
Health probe validation and SIGTERM handling sequence for reliable ASP.NET Core operations

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 DimensionPrimary Metric.NET ConsiderationRisk if Misconfigured
Horizontal Pod AutoscalerCPU / Custom MetricsCPU usage often lags behind thread saturationScale-up too late during burst traffic
Kestrel Thread PoolThreadPool.ThreadCountDefault min threads may be too low for high concurrencyRequest queuing despite low CPU
Memory LimitsWorking Set BytesGC Heap respects cgroup limits in .NET 9+OOMKill if GC cannot reclaim fast enough
Vertical Pod AutoscalerHistorical UsageChanges 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.

External Secret Store(Vault / AWS SM)Encrypted CredentialsKubernetes APISecrets & ConfigMapsBase64 EncodedASP.NET Core PodRuntime EnvironmentEnv Vars InjectedMounted Config FilesSync OperatorMount / Inject
Secure configuration flow from external secret store through Kubernetes API to ASP.NET Core runtime

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.

Frequently Asked Questions

Use the official mcr.microsoft.com/dotnet/aspnet:9.0-alpine image for production deployments. It includes the runtime without SDK bloat, reducing attack surface and pull times. Alpine variants are under 100MB, significantly faster than Debian-based images for cluster scaling events and node provisioning.

Add the Microsoft.AspNetCore.Diagnostics.HealthChecks package and map endpoints at /health/live and /health/ready. Configure Kubernetes livenessProbe and readinessProbe in your deployment YAML to hit these HTTP paths, ensuring traffic only routes to fully initialized application instances.

Missing environment variables or incorrect connection strings typically cause immediate crashes. Check pod logs with kubectl logs and verify ConfigMaps and Secrets are correctly mounted. Ensure the entrypoint command matches your published output directory structure exactly.

Yes, for small workloads. Kubernetes control plane costs plus node overhead exceed App Service Basic tier pricing. Reserve Kubernetes for microservices architectures requiring auto-scaling, custom networking, or multi-region failover that managed PaaS cannot provide cost-effectively.

Never embed secrets in Dockerfiles or ConfigMaps. Use Kubernetes Secrets mounted as environment variables or files, integrated with Azure Key Vault or AWS Secrets Manager via CSI drivers. Rotate credentials externally and sync automatically using sealed-secrets or external-secrets operator.

Profile your app first using dotnet-dump or Application Insights. Set memory requests to baseline usage plus twenty percent headroom, limits at double requests. CPU requests should match sustained load averages, preventing throttling while allowing burst capacity during traffic spikes.

Yes, but Linux containers are strongly preferred. Windows nodes cost more, have slower startup times, and limited ecosystem support. Only use Windows containers if your ASP.NET Core app depends on legacy .NET Framework libraries or Windows-specific APIs unavailable on Linux.

Terminate TLS at the ingress controller level using cert-manager for automatic certificate provisioning. Configure your ASP.NET Core app to trust forwarded headers via ForwardedHeadersOptions middleware, avoiding double encryption overhead between ingress and pod while maintaining secure external communication.

DNS resolution latency and cold starts commonly cause slowness. Enable DNS caching in CoreDNS, use readiness probes to prevent premature routing, and consider keeping minimum replica counts above zero. Profile network calls to identify service mesh or CNI plugin overhead.

No, service meshes add complexity unnecessary for most deployments. Start with Kubernetes native services and ingress controllers. Adopt Istio or Linkerd only when you require mutual TLS between services, advanced traffic splitting, or distributed tracing across polyglot microservices.

Write structured JSON logs to stdout/stderr using Serilog or NLog console sinks. Deploy Fluent Bit or Vector as DaemonSets to collect container logs and forward to Elasticsearch, Loki, or cloud-native logging services. Avoid writing logs to container filesystems.

Use RollingUpdate with maxSurge of one and maxUnavailable of zero. Implement graceful shutdown handling in Program.cs to complete in-flight requests before SIGTERM. Combine with preStop hooks sleeping five seconds to allow endpoint removal propagation before container termination begins.

Use Bridge to Kubernetes or Telepresence for local debugging against remote services without port-forwarding complexity. For production issues, attach dotnet-dump or dotnet-trace via ephemeral debug containers. Never ship debug symbols or development configurations in production container images.

Both work well. Choose Helm for complex templating across multiple environments with shared chart repositories. Prefer Kustomize for simpler overlay-based configuration management without template language overhead. Many teams standardize on Kustomize for GitOps workflows with ArgoCD or Flux.

Use ReadyToRun compilation during publish to reduce JIT overhead. Enable tiered compilation and profile-guided optimization in .NET 9. Pre-warm caches in startup tasks behind readiness gates. Smaller images pull faster, so trim unused dependencies and use single-file publishing where appropriate.