Running .NET well inside a container is a different skill from writing .NET well, and interviewers at the senior level use Docker and Kubernetes questions to find out whether a candidate has actually operated a cluster or only deployed to one. The .NET runtime behaves differently under a cgroup than it does on a workstation — the garbage collector reads memory limits, the thread pool reacts to CPU quotas, and a container that looks healthy in isolation can still crash-loop the moment Kubernetes starts enforcing its own health checks. These questions probe image construction, container-aware runtime tuning, the Kubernetes lifecycle hooks a .NET service has to respect, and the diagnostic instincts needed when a pod is unhealthy and the dashboards don't yet explain why.
Q1 Walk through how you'd optimize a Dockerfile for a production ASP.NET Core service. What actually moves the needle on image size and build time?#
Short answer: Use a multi-stage build so the SDK, NuGet caches and source tree never reach the final image; order layers so dotnet restore runs against only the project files before the rest of the source is copied in, so dependency layers stay cached across builds that only change application code; and pick the smallest runtime base image that still meets your operational needs — Ubuntu Chiseled for size and a reduced attack surface, a standard Debian-based image when you need a shell or package manager for in-container debugging.
The restore-before-copy ordering is the single biggest build-time win: Docker caches layers by content hash, so if .csproj files are copied and restored in their own layer before the rest of the source, a change to a .cs file doesn't invalidate the (often slowest) restore step. Chiseled runtime images take size and security further than a normal mcr.microsoft.com/dotnet/aspnet base — they ship only the packages .NET actually needs, have no shell or package manager, and run as a non-root app user by default, which materially shrinks the attack surface but also means you lose kubectl exec ... sh for live debugging, so that has to be replaced with kubectl debug ephemeral containers rather than baking debugging tools into the production image. For the smallest and fastest-starting images of all, publishing with Native AOT on a runtime-deps base skips the managed runtime entirely, at the cost of losing reflection-heavy library compatibility, so it's a deliberate trade-off rather than a default. A .dockerignore excluding bin/, obj/ and .git keeps the build context small, and a NuGet package cache mount (--mount=type=cache,target=/root/.nuget/packages) speeds up cold builds in CI where layer caching between runs isn't guaranteed.
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["OrdersApi/OrdersApi.csproj", "OrdersApi/"]
RUN dotnet restore "OrdersApi/OrdersApi.csproj"
COPY . .
RUN dotnet publish "OrdersApi/OrdersApi.csproj" -c Release -o /app/publish --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "OrdersApi.dll"]What interviewers look for: multi-stage build reasoning and correct layer ordering stated as why, not just what, plus honest awareness that a shell-less chiseled image trades away a common debugging workflow.
Common mistakes: copying the entire source tree before restoring, which busts the dependency cache on every source change, or shipping the SDK image to production because "it already has dotnet on it."
Q2 How does the .NET GC and thread pool behave differently inside a container versus on bare metal, and what production symptom does getting this wrong actually cause?#
Short answer: Inside a container, the GC reads the cgroup memory limit — not the host's total RAM — and, unless you configure a hard limit explicitly, caps the heap at 75% of it by default; the runtime also reads the CPU quota to derive an effective processor count that drives both the GC's heap/thread count and the thread pool's default sizing, and getting either of these misaligned with the pod's actual requests/limits produces either OOMKilled pods under load or CPU-throttled latency spikes that don't show up as sustained high CPU in dashboards.
The OOM failure mode is the more common incident: if a pod's memory limit is set generously above its requests "for headroom," the GC happily grows the heap toward that higher ceiling during a load spike, and then the node's cgroup enforcement kills the container — not because the application leaked memory, but because the GC was never told the realistic ceiling the surrounding infrastructure could actually sustain. The CPU story is subtler and easier to miss in a review: a fractional CPU limit (say, 500m) throttles the process against a CFS quota measured over short periods, so a burst of GC or thread-pool work can get starved of scheduled time even while the pod's average CPU utilization graph looks unremarkable, which is why "CPU looks fine but p99 latency spikes" is one of the most common misdiagnosed container performance tickets. The practical fix in both cases is the same discipline: keep requests and limits close together so the runtime's view of "available resources" matches what the pod can actually sustain, confirm behavior with dotnet-counters' GC and thread-pool counters rather than guessing, and on .NET 9 and later, lean on DATAS to let server GC size itself to real data volume instead of assuming it owns the whole node.
What interviewers look for: the specific mechanism — cgroup-aware GC ceiling and CPU-quota-driven thread pool sizing — and the two concrete failure modes it causes, not a vague "GC behaves differently in containers."
Follow-up questions:
- How would you confirm CPU throttling is actually happening, rather than assuming it from a hunch?
- What does DATAS change about this tuning story starting in .NET 9?
Q3 Explain the difference between liveness, readiness and startup probes in Kubernetes, and how you'd wire them to an ASP.NET Core service.#
Short answer: A readiness probe tells Kubernetes whether the pod should currently receive traffic, and gates both Service load-balancer inclusion and rollout progress; a liveness probe tells Kubernetes whether the process is unhealthy enough that killing and restarting it is the right fix; a startup probe suppresses both of the others until a slow-starting app finishes booting — and the most common production failure is wiring the liveness probe to a check that also fails during a downstream outage or a long GC pause, which turns a transient slowdown into a self-inflicted restart storm.
The design rule that resolves most probe mistakes: liveness should answer "is this process fundamentally stuck" with the cheapest possible check and no downstream dependency calls, because a database outage shouldn't cause Kubernetes to kill and restart pods that are otherwise fine — restarting doesn't fix a dead database, it just adds a restart storm on top of an already-degraded dependency. Readiness, by contrast, should check the dependencies that matter for serving a request correctly, because pulling an unready pod out of rotation is exactly the correct response to a degraded downstream. ASP.NET Core's health checks middleware makes the split easy to implement: register separate check sets and map them to separate endpoints so liveness and readiness never share a check.
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"])
.AddSqlServer(connectionString, tags: ["ready"])
.AddCheck<QueueDependencyHealthCheck>("queue", tags: ["ready"]);
app.MapHealthChecks("/healthz/live", new HealthCheckOptions { Predicate = c => c.Tags.Contains("live") });
app.MapHealthChecks("/healthz/ready", new HealthCheckOptions { Predicate = c => c.Tags.Contains("ready") });livenessProbe:
httpGet: { path: /healthz/live, port: 8080 }
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet: { path: /healthz/ready, port: 8080 }
periodSeconds: 5
startupProbe:
httpGet: { path: /healthz/live, port: 8080 }
failureThreshold: 30
periodSeconds: 2What interviewers look for: correct semantic separation of the three probe types, plus the specific "liveness checking a downstream dependency causes a restart storm" failure mode named unprompted.
Common mistakes: pointing liveness and readiness at the same deep health check, or omitting a startup probe on a service with a slow boot (EF Core migrations, cache warm-up) and watching it get killed before it ever reports healthy.
Q4 Walk through what has to happen, in order, for an ASP.NET Core pod to shut down without dropping in-flight requests during a rolling update or scale-down.#
Short answer: Kubernetes marks the pod terminating, removes it from Service endpoints and sends SIGTERM to the container at essentially the same moment, not sequentially — so the application has to keep serving in-flight requests during the grace period while IHostApplicationLifetime's ApplicationStopping token fires and Kestrel stops accepting new connections, and the pod is only safe to hard-kill once that in-flight work has genuinely finished, not just once SIGTERM has been delivered.
The concurrency of endpoint removal and SIGTERM is the detail candidates get backward most often, and it's exactly why a short preStop sleep is standard practice: it buys time for the endpoint removal to actually propagate through kube-proxy across every node before the process starts refusing new connections, because updating iptables or IPVS rules cluster-wide is not instantaneous. terminationGracePeriodSeconds sets the deadline before Kubernetes escalates to SIGKILL, and it has to be longer than your longest reasonable in-flight request plus the preStop sleep, or real traffic gets truncated. ASP.NET Core's generic host wires SIGTERM to graceful shutdown automatically, but any BackgroundService that ignores the cancellation token it's given will get hard-killed mid-work on every single deploy regardless of how carefully the HTTP path is drained.
public sealed class OrderExportWorker(IHostApplicationLifetime lifetime) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await ExportPendingOrdersAsync(stoppingToken); // observes the token, not a while(true)
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}lifecycle:
preStop:
exec: { command: ["sh", "-c", "sleep 5"] }
terminationGracePeriodSeconds: 30What interviewers look for: the concurrent, not sequential, relationship between endpoint removal and SIGTERM, and the preStop-sleep pattern that follows directly from it.
Common mistakes: assuming SIGTERM only arrives after the pod is already fully out of rotation, which leads to dropped requests on every rollout.
Q5 How do you manage configuration and secrets for a .NET service in Kubernetes, and where does Azure Key Vault actually help versus a plain Kubernetes Secret?#
Short answer: Non-secret configuration belongs in a ConfigMap and secret values in a Kubernetes Secret, surfaced to the app either as environment variables — simplest, and works naturally with IConfiguration's environment variable provider — or as mounted files, which is the better choice for anything that rotates, since a mounted Secret can update without a pod restart if the app watches the file; for real secret lifecycle management — rotation, audit, centralized access control — the Kubernetes Secret itself should be backed by Azure Key Vault via the Secrets Store CSI driver rather than holding the actual value directly.
A detail that surprises candidates who haven't operated a cluster: a raw Kubernetes Secret is only base64-encoded, not encrypted at rest, unless the cluster has envelope encryption configured — treating the Secret object alone as sufficient secret hygiene is a common and risky misconception. The env-var-versus-mounted-file choice comes down to rotation: environment variables are captured once at process start, so a rotated secret needs a pod restart to take effect, while a projected volume mount can be watched by IConfiguration's file provider and picked up live. The Azure Key Vault Provider for Secrets Store CSI Driver bridges the two worlds: it syncs a Key Vault secret into a Kubernetes Secret on the node, giving the application the simplicity of a normal mount while giving the platform team Key Vault's access policies and audit trail, and it can optionally sync updates back automatically on rotation. For secrets management discipline, the rule I hold every team to without exception is that nothing sensitive is ever committed to a Helm values file or baked into an image layer — image layers are trivially extractable, so that's not a gray area.
What interviewers look for: the file-versus-env-var rotation trade-off, correct knowledge that raw Kubernetes Secrets aren't encrypted by default, and a real mechanism for Key Vault integration rather than "just use Key Vault" with no detail behind it.
Common mistakes: treating a Kubernetes Secret object as equivalent to a properly managed, audited secret store.
Q6 How would you configure autoscaling for a bursty .NET API versus a queue-processing worker? Why doesn't plain CPU-based HPA work well for the worker?#
Short answer: CPU- or memory-based HorizontalPodAutoscaler is a reasonable default for a request/response API, since CPU usage tracks request volume fairly directly, but a queue-processing worker's CPU can look nearly idle right up until a burst of messages arrives, so scaling on CPU reacts too late — that workload needs KEDA scaling on the actual queue depth (Service Bus, Storage Queue, Kafka consumer lag) so new pods spin up in response to backlog, the real cost driver, instead of in response to CPU that's already climbing after the backlog exists.
A .NET-specific wrinkle that affects both cases: a new pod takes real time to become genuinely ready — JIT and ReadyToRun warm-up, DI container construction, EF Core model building — so a scale-up policy tuned as if new capacity appears instantly will under-provision during that warm-up window; pairing a realistic startup probe with a tuned behavior.scaleUp.stabilizationWindowSeconds avoids both premature traffic and scale-up thrashing. For the queue worker, a KEDA ScaledObject watches the queue's message count directly and scales the deployment — including to zero, if that fits the workload — based on backlog rather than CPU, which is the metric that actually reflects the work waiting to be done. It's also worth naming the layer below HPA in this answer: HPA scales pod count, the cluster autoscaler scales node count to fit those pods, and a common gap is HPA creating pods that then sit Pending because the cluster autoscaler hasn't provisioned a node yet — node pool sizing and Pending-to-Running latency need to be planned together, not treated as someone else's problem.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata: { name: order-worker-scaler }
spec:
scaleTargetRef: { name: order-worker }
minReplicaCount: 0
maxReplicaCount: 30
triggers:
- type: azure-servicebus
metadata: { queueName: orders, messageCount: "20" }What interviewers look for: recognizing CPU as a lagging indicator for queue-driven work and naming KEDA (or an equivalent custom-metric scaler) as the fix, plus awareness of how .NET startup latency affects scale-up responsiveness.
Common mistakes: applying the same CPU-based HPA configuration to both workload types without questioning whether CPU is even the right signal for the queue worker.
Q7 Explain how a Kubernetes rolling update actually works for a Deployment, and what has to be true about your .NET service for it to be safe.#
Short answer: A rolling update creates new-version pods and waits for each to pass its readiness probe before terminating an equivalent number of old-version pods, bounded by maxSurge (how many extra pods can exist above the desired count during the rollout) and maxUnavailable (how many can be below it) — which means old and new versions of your service serve traffic simultaneously for the entire rollout window, so the deployment is only safe if both versions can coexist against the same database schema, message contracts and downstream dependencies.
Readiness is the actual gate that makes this safe or unsafe: a pod that reports ready before it's truly warmed up gets real production traffic prematurely, so readiness has to reflect genuine capability, not merely "the process started." The compatibility point is where rollouts actually cause incidents in practice: a rolling update that pairs a new pod expecting a new database column against old pods that haven't been replaced yet requires the migration to be backward-compatible for the whole rollout window — the same expand/contract discipline behind safe zero-downtime database migrations, which doesn't become optional just because the deployment mechanism is a Kubernetes rolling update rather than a manual blue-green cutover. progressDeadlineSeconds pauses and flags a rollout that's stuck on failing readiness instead of hanging forever, and a Recreate strategy — every old pod killed before any new one starts — deliberately trades away zero downtime for simplicity, which is sometimes the right, explicit choice for a singleton workload.
spec:
strategy:
type: RollingUpdate
rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }
progressDeadlineSeconds: 300What interviewers look for: the "old and new versions run concurrently" framing tied directly to backward-compatible schema and contract changes, not a recitation of maxSurge/maxUnavailable definitions alone.
Common mistakes: shipping a breaking database migration in the same rollout as the code that depends on it, and assuming the rollout is an instantaneous switch rather than a window where both versions coexist.
Q8 A pod is stuck in CrashLoopBackOff. Walk through your triage process for a .NET service.#
Short answer: Start with kubectl describe pod for the Events section — image pull failures, scheduling failures, OOMKilled — and kubectl logs <pod> --previous to see the last container's actual output before it died, since the current, freshly restarted container may not have logged anything useful yet; that pair of commands resolves most crash loops before you need to attach a debugger at all.
I work the possibilities in a fixed order, cheapest evidence first: describe pod's events for OOMKilled (exit code 137, pointing back to the memory-limit-versus-GC-ceiling discussion) or ImagePullBackOff from a bad tag or missing registry credentials; logs --previous for an unhandled startup exception, commonly a configuration value or connection string that only exists — or is misspelled — in the production environment, which is itself a strong argument for failing fast and loudly on missing configuration rather than swallowing the error; the exit code, since 137 is SIGKILL (OOM or a liveness probe that finally gave up), 143 is SIGTERM (often a graceful shutdown that ran past its grace period), and 1 is an unhandled managed exception; and if the container does start but the liveness probe is what's actually killing it, temporarily widening failureThreshold or checking whether a GC pause is tripping the probe's timeout. For anything that genuinely needs live inspection inside a shell-less chiseled or distroless image, kubectl debug attaches an ephemeral debug container instead of trying to exec into a container that has no shell to exec into. This evidence-first sequence mirrors the same discipline used in performance profiling: confirm the failure mode from cheap evidence before reaching for a heavier tool.
kubectl describe pod order-worker-7d8f9c-abcde
kubectl logs order-worker-7d8f9c-abcde --previous
kubectl debug -it order-worker-7d8f9c-abcde --image=busybox --target=order-workerWhat interviewers look for: a repeatable, ordered triage sequence starting from describe/logs --previous rather than jumping straight to exec'ing into the container, plus fluency with the common exit codes.
Common mistakes: only checking the current, freshly restarted container's live logs and missing the previous container's actual exit reason entirely.
Q9 How do you size CPU and memory requests and limits for a .NET service, and what actually breaks when the gap between request and limit is too wide?#
Short answer: Set requests to what the service genuinely needs at steady state, so the scheduler places it accurately and the container-aware GC and thread pool see a realistic baseline, and keep limits close to requests rather than far above them — a large gap lets the GC grow the heap toward a ceiling the node isn't actually guaranteed to back, which is exactly the setup behind "cluster looks healthy, one pod randomly gets OOMKilled" incidents.
On the memory side, the GC treats the container's memory limit (or an explicit HeapHardLimit) as its ceiling and will grow toward it under sustained load; if limits is set generously above requests "for headroom," the GC uses that headroom, and a genuine load spike across several pods co-located on the same node can then exceed what the node actually has free, triggering an eviction that has nothing to do with a leak in the application. On the CPU side, an aggressive limit set well below realistic peak usage causes CFS-quota throttling — the process is starved of scheduled time within short windows even while average utilization graphs look unremarkable — which surfaces as intermittent latency spikes, especially in GC- or thread-pool-heavy code paths. My default starting point is CPU requests equal to limits (which earns the Guaranteed QoS class and avoids throttling surprises entirely) and a modest, not generous, memory headroom above requests, adjusted afterward from real dotnet-counters/kubectl top data rather than guessed up front — and on .NET 9 and later, leaning on DATAS instead of hand-tuning a hard heap limit for every individual service.
resources:
requests: { cpu: "500m", memory: "384Mi" }
limits: { cpu: "500m", memory: "512Mi" }What interviewers look for: the specific mechanism — GC growing toward the limit, CPU throttling from CFS quotas — rather than generic "set requests and limits" advice, and a concrete, defensible opinion on how close requests and limits should sit.
Follow-up questions:
- What Quality of Service class does Kubernetes assign when requests equal limits, and why does that matter under node memory pressure?
- How would DATAS change your approach to setting a memory limit on .NET 9 or later?
Q10 When would you add a sidecar container to a .NET pod versus building that capability into the application itself, and what's the resource cost of getting it wrong?#
Short answer: Reach for a sidecar when the capability is genuinely cross-cutting and platform-owned — a service mesh proxy, a log or metrics shipper, a secret-sync container — because it lets a platform team update that capability without touching every application's code or release cadence; keep a capability in-process when it sits on the application's hot path, where the extra network hop or per-pod resource overhead of a sidecar would cost more than it saves, which covers most ordinary business logic.
The sidecar pattern shares the pod's network namespace and lifecycle, which is exactly what makes a mesh proxy (mutual TLS, retries and circuit breaking handled at the network layer, independent of application code) or a log shipper (tailing stdout without touching the app) a natural fit. The cost is real and easy to underestimate: every pod now runs an extra container with its own requests, so a lightweight sidecar added to hundreds of pods across a cluster can add up to a meaningful slice of total cluster capacity, and sidecar readiness has to be coordinated with the main container's — native Kubernetes sidecar container support exists specifically to fix the old ordering hazard where a mesh proxy wasn't yet ready when the main container started sending it traffic. Init containers solve a related but distinct problem: one-time setup that must finish before the main container starts, such as running EF Core migrations or waiting for a dependency to become reachable. They run to completion, in order, before the pod's regular containers start, which makes them the right place for "must happen exactly once, before traffic" work and the wrong place for anything that needs to keep running, since a hung init container blocks the pod from ever starting at all.
What interviewers look for: the cross-cutting-versus-hot-path framing for sidecars, and a correct distinction between "runs alongside" (sidecar) and "runs once before" (init container) rather than treating the two as interchangeable.
Common mistakes: putting one-time migration logic in a sidecar, where it either keeps the container running unexpectedly or confuses pod readiness, instead of in an init container.
Quick-Fire Round#
| Question | Answer |
|---|---|
| What's the default GC memory ceiling inside a container? | 75% of the cgroup memory limit, unless a hard limit is set explicitly. |
| Should liveness or readiness check downstream dependencies? | Readiness — a failing dependency should pull the pod from rotation, not trigger a restart. |
| What does Kubernetes do at the same moment it sends SIGTERM? | Removes the pod from Service endpoints. |
| What exit code means OOMKilled? | 137. |
| Are raw Kubernetes Secrets encrypted at rest by default? | No — only base64-encoded, unless envelope encryption is configured. |
| What scales pods on queue depth instead of CPU? | KEDA, via a ScaledObject. |
| What controls how many extra pods a rolling update can create? | maxSurge (maxUnavailable controls how many can be unavailable). |
| First two commands to triage a CrashLoopBackOff? | kubectl describe pod, then kubectl logs --previous. |
| What runs once before a pod's main containers start? | An init container. |
What kind of base image has no shell to exec into? | A chiseled or distroless runtime image. |
How to Prepare#
- Be able to explain precisely why the GC's default 75% container memory ceiling matters, and what happens when requests and limits diverge.
- Practice the liveness-versus-readiness distinction with the specific "liveness checking a dependency causes a restart storm" failure story.
- Know the SIGTERM-and-endpoint-removal race condition and why a
preStophook exists because of it. - Rehearse a
CrashLoopBackOfftriage sequence out loud, starting fromkubectl describeandlogs --previous, not fromexec. - Have one KEDA or custom-metric autoscaling example ready that isn't just CPU-based HPA.
- Know the difference between a sidecar and an init container cold, with one real example of each.