Deploy a .NET Service to Kubernetes

Khimananda Oli 7 min read Programming and Languages
Deploy a .NET Service to Kubernetes

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.

Ingress Controller.NET Pod A.NET Pod B.NET Pod CConfigMap / SecretDatabase / API
High-level topology when you deploy a .NET service to Kubernetes: Ingress routes traffic to replicated pods that consume configuration from ConfigMaps and Secrets before connecting to backend data stores.

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.

SchedulerKubelet.NET ContainerStartup Probe (/health)Readiness Probe ActiveLiveness Probe OngoingProbe Configurationstartup: period=5s fail=30readiness: period=10s fail=3liveness: period=15s fail=3initialDelay: 0 (startup)timeout: 3s all probes
Pod lifecycle sequence when you deploy a .NET service to Kubernetes: startup probe gates readiness, preventing premature traffic routing during JIT warmup and dependency initialization.

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.

ApproachBest ForSecurity LevelComplexity
Environment VariablesSimple key-value pairs, connection stringsMedium (visible in pod spec)Low
Mounted Secret VolumesCertificates, complex config filesHigh (not in env, file permissions)Medium
External Secrets OperatorVault/AWS SM/Azure KV integrationHighest (synced, audited, rotated)High
Sealed Secrets / SOPSGitOps-friendly encrypted manifestsHigh (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.

Naive DeploymentSDK base image (1.2GB+)Running as root userNo health probes definedSecrets in plain env varsNo resource limits setSingle replica, no HPAProduction DeploymentChiseled runtime (~90MB)Non-root UID 1654Startup + Readiness + LivenessExternal Secrets OperatorRequests + Limits defined3+ replicas with HPAMaturity
Side-by-side comparison of naive versus production configurations when you deploy a .NET service to Kubernetes, highlighting security, reliability, and operational maturity differences.

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.

Frequently Asked Questions

Use mcr.microsoft.com/dotnet/aspnet:9.0-alpine for production deployments in 2026. Alpine reduces attack surface and image size significantly compared to Debian-based images, resulting in faster pod startup times and lower storage costs across your cluster nodes.

Yes, always configure them.

Check resource limits and unhandled exceptions first. .NET services often exceed default memory limits during JIT compilation or garbage collection spikes. Inspect pod logs with kubectl logs and verify your Dockerfile exposes the correct port matching the containerPort definition in your deployment manifest.

Never bake secrets into container images. Mount sensitive configuration as Kubernetes Secrets and non-sensitive settings as ConfigMaps using volume mounts. This allows environment-specific overrides without rebuilding containers and keeps credentials out of source control and image layers entirely.

Implement IHostApplicationLifetime to handle SIGTERM signals properly. Configure terminationGracePeriodSeconds to allow in-flight requests to complete before forced termination. Without this, active database connections and HTTP requests drop abruptly during rolling updates or scaling events causing user-facing errors.

Start with 250m CPU and 512Mi memory for typical APIs. Profile actual usage with Vertical Pod Autoscaler recommendations before tuning. .NET garbage collector behavior changes under container constraints, so test thoroughly under load rather than relying on generic sizing guidelines from documentation.

Use OpenTelemetry .NET SDK with automatic instrumentation. Export traces to Jaeger or Tempo via OTLP protocol. Configure sampling rates appropriately since high-cardinality spans increase storage costs. Inject trace context through W3C headers for cross-service correlation without modifying application code manually.

Avoid if possible.

Enable ReadyToRun compilation during build to pre-compile framework assemblies. Use tiered PGO in .NET 9 to optimize hot paths after warmup. Combine with horizontal pod autoscaler scale-down delays to prevent premature termination of warmed instances during traffic fluctuations throughout the day.

NGINX Ingress Controller remains the standard choice in 2026 for .NET workloads. It handles WebSocket upgrades, gRPC streaming, and path-based routing reliably. Configure proxy-buffer-size and client-max-body-size annotations explicitly since defaults often truncate large API responses or file uploads common in enterprise applications.

Use Bridge to Kubernetes or Telepresence to route cluster traffic to your local debugger. This avoids pushing broken images repeatedly while testing configuration changes. Replicate environment variables and mounted volumes locally to match production conditions accurately during troubleshooting sessions.

Helm suits teams managing multiple environments with parameterized charts. Kustomize works better for GitOps workflows where overlay patches modify base manifests per environment. Both integrate with ArgoCD and Flux. Choose based on team familiarity rather than technical superiority since both handle .NET deployments equally well.

Implement mTLS using Linkerd or Istio service mesh. .NET supports client certificates natively through HttpClientHandler. Rotate certificates automatically via cert-manager. Avoid plaintext HTTP between pods even within cluster boundaries since network policies alone cannot prevent lateral movement after initial compromise.

Set DOTNET_GCHeapHardLimitPercent to reserve headroom below container memory limit. .NET GC assumes available memory equals cgroup limit but overhead from native allocations and thread stacks consumes additional space. Monitor with dotnet-counters and adjust limits based on observed working set plus twenty percent buffer.

Use GitHub Actions or GitLab CI with docker/build-push-action. Tag images with Git SHA and semantic version for traceability. Scan with Trivy before pushing to registry. Implement multi-stage Dockerfiles to separate build dependencies from runtime, keeping final images minimal and reducing vulnerability exposure surface significantly.