Docker & Containerization

16 questions found

What are the official .NET Docker base images, and what's the difference between the sdk, aspnet, and runtime image variants?

Beginner
The sdk image (mcr.microsoft.com/dotnet/sdk) includes the full SDK for building and publishing applications -- large, used only in build stages. The aspnet image (mcr.microsoft.com/dotnet/aspnet) includes the ASP.NET Core runtime for running web applications -- smaller, used as the final production base for web apps. The runtime image (mcr.microsoft.com/dotnet/runtime) includes just the base .NET runtime without ASP.NET Core-specific components, for non-web console/worker applications.
# Building a console app: runtime is sufficient (smaller than aspnet)
FROM mcr.microsoft.com/dotnet/runtime:8.0 AS final

# Building a web API: needs aspnet for Kestrel, MVC, etc.
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
Real-world example A background worker service that processes messages and has no web endpoints uses the smaller dotnet/runtime base image instead of dotnet/aspnet, avoiding unnecessary ASP.NET Core components it will never use.

Common follow-ups: What's the size difference between these base image variants?;What is the runtime-deps image and when would you use it instead?

.NET CLI SDK & Project Structure (csproj);CI/CD Publishing & Deployment

How does a multi-stage Dockerfile reduce the final image size for a .NET application, and what does each stage typically do?

Intermediate
A multi-stage Dockerfile uses a build stage (based on the full SDK image) to restore, compile, and publish the application, then a separate, final stage (based on a minimal runtime image) copies only the published output artifacts from the build stage, discarding the SDK, intermediate build files, and source code entirely from the final image -- resulting in a dramatically smaller, more secure production image containing only what's needed to run the application.
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]
Real-world example A team reduces their production image from 1.1GB (accidentally including the full SDK) to 210MB by properly separating build and final stages, directly improving deployment speed and reducing the attack surface.

Common follow-ups: Why copy the .csproj file separately before the rest of the source code?;How many stages can a Dockerfile have, and when would you need more than two?

.NET CLI SDK & Project Structure (csproj);CI/CD Publishing & Deployment

Why does copying only the .csproj file before the rest of the source code in a Dockerfile improve build caching?

Advanced
Docker caches each instruction's layer based on its inputs; by copying only the .csproj (which changes infrequently) and running dotnet restore before copying the rest of the source code (which changes on every commit), Docker can reuse the cached restore layer across builds as long as dependencies haven't changed, avoiding a full package restore on every single build even when only application code (not dependencies) changed -- significantly speeding up iterative CI builds.
# Optimized layer caching:
COPY *.csproj .
RUN dotnet restore          # cached as long as .csproj is unchanged
COPY . .                    # this layer invalidates on every code change
RUN dotnet publish -c Release -o /app/publish  # but restore step above stays cached
Real-world example A CI pipeline's Docker build time drops from 3 minutes to 45 seconds after restructuring the Dockerfile to copy project files separately, since NuGet restore (the slowest step) now hits the Docker layer cache on every commit that doesn't touch dependencies.

Common follow-ups: How does this pattern extend to multi-project solutions with several .csproj files?;What invalidates the restore layer's cache even without dependency changes?

.NET CLI SDK & Project Structure (csproj);CI/CD Publishing & Deployment

How do you configure a .NET container to respect memory and CPU limits set by the container orchestrator (like Kubernetes resource limits)?

Intermediate
Modern .NET automatically detects container memory and CPU limits (via cgroup information on Linux) and configures the garbage collector and thread pool accordingly by default -- the GC scales its heap size targets based on the container's memory limit rather than the host machine's total memory, and the thread pool sizes itself based on the container's CPU limit (not the host's core count), preventing the classic problem of a containerized app assuming it has access to the entire host's resources.
# Kubernetes resource limits automatically respected by the .NET runtime:
resources:
  limits:
    memory: "512Mi"
    cpu: "1"
# .NET's GC targets a heap size appropriate for 512Mi, not the node's full memory
Real-world example Before this automatic detection was reliable, a team had to manually set DOTNET_GCHeapHardLimit to prevent their containerized app's GC from assuming it had access to the entire host's 64GB of RAM, causing OOM-killed pods under memory pressure.

Common follow-ups: How do you override the automatic detection if it doesn't behave as expected?;What environment variables control GC heap limits explicitly?

Memory Management & Garbage Collection;CLR & Runtime

What is Native AOT's benefit for containerized .NET applications specifically, beyond the general startup time improvement?

Advanced
For containers, Native AOT produces a fully self-contained native executable with no separate .NET runtime dependency, letting you use an extremely minimal base image (even distroless or scratch-based images with no OS package manager or shell at all), dramatically reducing both image size and attack surface compared to even a trimmed framework-dependent image -- combined with near-instant startup, this is particularly valuable for serverless/FaaS scenarios and horizontally auto-scaled services where fast cold starts directly reduce cost and improve responsiveness.
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -r linux-x64 -p:PublishAot=true -o /app

FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy-chiseled AS final
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["/app/MyApp"]
Real-world example A serverless order-processing function switches to Native AOT with a chiseled Ubuntu base image, reducing both cold-start latency (from ~800ms to under 50ms) and image size (from 200MB to under 30MB), directly cutting both cost and user-perceived latency.

Common follow-ups: What is a 'chiseled' Ubuntu container image and how does it improve security?;What functionality is lost by adopting Native AOT (reflection-heavy libraries, etc.)?

.NET CLI SDK & Project Structure (csproj);Diagnostics & Performance

Why is it recommended to run a containerized .NET application as a non-root user, and how do you configure this in a Dockerfile?

Intermediate
Running as root inside a container provides no additional privilege within the container itself, but if an attacker manages to escape the container's isolation (via a container runtime vulnerability), running as root significantly increases the potential impact, since the compromised process would have root privileges on the host in that failure scenario -- Microsoft's official aspnet images since .NET 8 include a built-in non-root 'app' user you can switch to explicitly.
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish .
USER app  # switch to the built-in non-root user
ENTRYPOINT ["dotnet", "MyApp.dll"]
Real-world example A security audit flags several production containers running as root unnecessarily; adding a single USER app line to each Dockerfile closes this finding without any application code changes, since the base image already includes a properly configured non-root user.

Common follow-ups: What port-binding limitations does running as non-root introduce (ports below 1024)?;How do you configure file permissions correctly when running as a non-root user?

Authentication & Authorization (Identity JWT OAuth);CI/CD Publishing & Deployment

How does .NET's runtime automatically size the ThreadPool and GC based on container CPU limits, and what issue can arise with fractional CPU limits?

Advanced
The runtime reads the container's CPU quota (from cgroup cpu.cfs_quota_us/cpu.cfs_period_us on Linux) to determine the effective number of CPUs available, using this to size the ThreadPool's minimum thread count and influence Server GC's heap count -- a known historical issue arose with fractional CPU limits (like 0.5 or 1.5 CPUs, common in Kubernetes resource requests), where the runtime would sometimes round in ways that didn't perfectly match the intended allocation, leading to under- or over-provisioned thread pools; this has been progressively improved across .NET versions.
# Kubernetes deployment with a fractional CPU limit
resources:
  limits:
    cpu: "1.5"  # .NET runtime detects this and sizes ThreadPool/GC accordingly

# Can be explicitly overridden if detection seems off:
# DOTNET_PROCESSOR_COUNT=2
Real-world example A team troubleshooting inconsistent performance across pods with a 0.5 CPU limit discovers via dotnet-counters that ThreadPool sizing didn't match expectations, working around it with an explicit DOTNET_PROCESSOR_COUNT override while also upgrading to a newer .NET version with improved fractional CPU handling.

Common follow-ups: How would you verify what CPU count the runtime actually detected?;Why do fractional CPU limits pose an inherently harder detection problem than whole-number limits?

CLR & Runtime;Diagnostics & Performance

What is Docker Compose, and how does it simplify running a multi-container .NET application locally (e.g., API + database + Redis)?

Intermediate
Docker Compose defines a multi-container application's full topology (services, networks, volumes, environment variables, dependencies) in a single YAML file, letting you start an entire local development environment -- your .NET API, a PostgreSQL database, and a Redis cache -- with one `docker compose up` command, instead of manually running and networking multiple `docker run` commands.
# docker-compose.yml
services:
  api:
    build: .
    ports: ["8080:8080"]
    environment:
      - ConnectionStrings__Db=Host=db;Database=myapp
    depends_on: [db, redis]
  db:
    image: postgres:16
    environment: ["POSTGRES_PASSWORD=devpassword"]
  redis:
    image: redis:7
Real-world example A new developer onboarding to a project runs `docker compose up` once and has a fully working local environment with the API, database, and cache all correctly networked together, without manually installing or configuring PostgreSQL or Redis locally.

Common follow-ups: How does depends_on differ from actually waiting for a dependency to be ready?;How does .NET Aspire relate to and potentially replace Docker Compose for local orchestration?

Entity Framework Core & Data Access;Caching (In-Memory Distributed & Redis)

What is .NET Aspire, and how does it improve the local development and orchestration experience for cloud-native, multi-service .NET applications?

Advanced
​.NET Aspire is an opinionated stack for building observable, cloud-native applications, providing a code-first orchestration model (an AppHost project defining service dependencies like databases, caches, and other services using C#) that automatically wires up service discovery, connection strings, and health checks between projects for local development, plus a built-in dashboard showing live logs, traces, and metrics across all running services -- reducing the boilerplate of manually configuring Docker Compose files or Kubernetes manifests just for local development.
// AppHost Program.cs
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddRedis("cache");
var db = builder.AddPostgres("postgres").AddDatabase("orders");
var api = builder.AddProject<Projects.OrderApi>("orderapi")
    .WithReference(redis)
    .WithReference(db);
builder.Build().Run();
Real-world example A team adopts .NET Aspire for their microservices solution, replacing a hand-maintained docker-compose.yml with a strongly-typed C# AppHost project that automatically injects the correct connection strings into each service and provides a unified dashboard showing all services' logs and traces during local development.

Common follow-ups: How does Aspire's service discovery work without a full service mesh?;How does Aspire relate to actual production deployment versus just local development?

Microservices & Distributed Architecture Patterns;Diagnostics & Performance

How do you pass environment-specific configuration into a running .NET container, and how does this interact with the configuration system?

Intermediate
Environment variables passed to a container (via `docker run -e`, a Docker Compose environment section, or a Kubernetes deployment's env field) are automatically picked up by ASP.NET Core's environment variable configuration provider using the double-underscore convention for nested keys, letting you fully configure a container's behavior at deploy time without rebuilding the image or baking environment-specific settings into it.
docker run -e ConnectionStrings__Db="Server=prod-db;..." -e ASPNETCORE_ENVIRONMENT=Production myapp:latest

# Or in Kubernetes:
env:
  - name: ConnectionStrings__Db
    valueFrom: { secretKeyRef: { name: db-secret, key: connection-string } }
Real-world example The exact same Docker image is deployed to dev, staging, and production Kubernetes clusters, with each environment's specific connection strings and feature flags injected purely through environment variables, honoring the 'build once, deploy everywhere' principle.

Common follow-ups: How do you handle secrets specifically (versus regular config) in a Kubernetes environment variable setup?;What ASPNETCORE_ENVIRONMENT value should each deployment tier use?

Configuration & Options;Secrets Management & Configuration Providers (Key Vault User Secrets)

Showing 1–10 of 16