DOTNET Contents

Dockerizing ASP.NET Core (Production)

Dockerizing ASP.NET is not “FROM mcr.microsoft.com/dotnet/aspnet”. Production containers need non-root, deterministic builds, fast startup, correct port binding, and aggressive size control without breaking diagnostics.

On this page

Production incident

Your container works locally, but in Kubernetes it crash-loops. It binds to localhost only. Or it runs as root and a supply-chain audit flags it. Or you ship a 1.2GB image, deployments take forever, and autoscaling is slow. Another incident: you trimmed the image so hard that diagnostic tools are missing during a live outage. Root cause: containerization treated as packaging, not an operational artifact.

Symptoms

  • CrashLoopBackOff after deploy due to wrong ports/bindings.
  • Slow deployments and slow scale-out due to huge images.
  • Security scanners flag root user and writable filesystem issues.
  • No ability to collect diagnostics in production images.

Root causes

  • App binds to wrong URL (localhost) or wrong port assumptions.
  • Single-stage builds that include SDK and build tools.
  • Running as root by default.
  • No health endpoints and no readiness assumptions in orchestration.

Diagnosis

# Check Dockerfile and runtime binding config
grep -R "FROM mcr\.microsoft\.com/dotnet" -n Dockerfile*
grep -R "ASPNETCORE_URLS\|DOTNET_URLS\|EXPOSE" -n .
grep -R "USER " -n Dockerfile*

Anti-pattern

  • Shipping SDK image to production runtime.
  • Running as root and writing anywhere in the filesystem.
  • Relying on implicit ports and environment defaults.

Correct pattern

  • Multi-stage builds: build with SDK, run with runtime image.
  • Non-root user, minimal filesystem permissions.
  • Explicit ports and explicit binding (0.0.0.0).
  • Keep diagnostics story: either include minimal tools or provide a separate debug image strategy.

Dockerfile baseline (multi-stage, non-root)

# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /out

# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app

# Non-root (exact user setup depends on base image)
# If base image supports it, create a user; otherwise use a predefined one.
USER app

ENV ASPNETCORE_URLS=http://0.0.0.0:8080
EXPOSE 8080

COPY --from=build /out ./
ENTRYPOINT ["dotnet","MyApp.dll"]

Operational hardening

  • Set resource requests/limits to avoid noisy neighbor issues.
  • Configure graceful shutdown time budgets (terminationGracePeriod) and app drain behavior.
  • Use readiness probes tied to real dependency readiness (not just process alive).

Security and performance impact

  • Security: non-root containers reduce blast radius. Smaller images reduce supply-chain surface.
  • Performance: smaller images and fewer layers speed up deploys and scale-outs.

Operational notes

  • Monitoring: image pull time, startup time, crash loops, readiness failure reasons.
  • Rollback: keep last-known-good image tags and avoid mutable tags (latest).

Checklist

  • Multi-stage Dockerfile (SDK build, runtime image).
  • Runs as non-root with minimal permissions.
  • Explicit bind address/port and consistent EXPOSE.
  • Image size is controlled without removing critical diagnostics.
  • Immutable tags and fast rollback exist.