CalSync — Automate Outlook Calendar Colors

Auto-color-code events for your team using rules. Faster visibility, less admin. 10-user minimum · 12-month term.

CalSync Colors is a service by CPI Consulting

In this blog post How to Build Lean Reliable .NET Docker Images for Production we will walk through how to package .NET apps into small, fast, and secure Docker images that run the same way on your laptop and in production.

We’ll start with a high-level view of how containers and .NET fit together, then move into a practical, battle-tested Dockerfile you can adapt today. Along the way we’ll cover base image choices, multi-stage builds, caching, security hardening, multi-arch images, and CI/CD tips.

Why containers and .NET play well together

Docker packages your application and its dependencies into an immutable image. At runtime, that image becomes a container with isolated processes, limited filesystem views, and predictable configuration. The image is built as a stack of layers; when you change only part of your app, Docker reuses unchanged layers for speed.

.NET complements this model nicely. Microsoft ships official SDK and runtime images with consistent tags, security updates, and support for multiple architectures. With multi-stage builds, you can compile in a larger SDK image and run in a smaller runtime image. That gives you fast, repeatable builds and compact production containers.

The moving parts in a .NET container

  • Base image: The starting point. For .NET you’ll usually pick SDK (build time) and ASP.NET or runtime (run time) images from Microsoft’s registry.
  • Layers: Each command in your Dockerfile adds a layer. Arrange commands to maximize caching and minimize rebuilds.
  • Registry: Where images live (e.g., Docker Hub, ACR, ECR, GCR). CI/CD pushes images there; orchestrators pull them.
  • Entrypoint: The command that starts your app inside the container (e.g., dotnet MyApp.dll).

Choose the right base images for .NET

Microsoft publishes well-maintained .NET images on mcr.microsoft.com. Common choices:

  • mcr.microsoft.com/dotnet/sdk:8.0 – full SDK for restore, build, and publish.
  • mcr.microsoft.com/dotnet/aspnet:8.0 – runtime for ASP.NET Core web apps.
  • mcr.microsoft.com/dotnet/runtime:8.0 or runtime-deps:8.0 – for console/services without ASP.NET.

OS flavor trade-offs:

  • Debian/Ubuntu (default): Great compatibility, good tooling, slightly larger.
  • Alpine: Very small, uses musl libc; some native dependencies may need tweaks. Validate performance and compatibility.

Tip: Pin your base images to a major.minor version (e.g., 8.0) or digest for deterministic builds.

A production-grade Dockerfile for ASP.NET Core

Here’s a clean, multi-stage Dockerfile for a typical ASP.NET Core app named MyApp. It favors caching, small size, and non-root execution.

What this Dockerfile does

  • Separates build and runtime: compile in the SDK, run in the slim runtime.
  • Optimizes caching: dotnet restore happens before copying the full source.
  • Publishes for Release: strips unneeded build assets and reduces size.
  • Runs as non-root: a simple, effective security improvement.

Build and run locally

Open http://localhost:8080 to test.

Make builds fast and repeatable

Use a .dockerignore

Prevent large or irrelevant files from bloating your build context:

Lock dependencies

For deterministic restores, check in packages.lock.json and use locked mode in CI:

Structure your Dockerfile for cache hits

  • Copy .csproj first and restore; only when package references change will that step invalidate.
  • Copy the rest of the source and build/publish afterwards.
  • Avoid RUN commands that change frequently near the top of the file.

Keep images small and secure

  • Choose the smallest suitable runtime: aspnet for web, runtime/runtime-deps for services.
  • Publish with trimming where appropriate: dotnet publish -c Release -p:PublishTrimmed=true (test thoroughly as trimming can remove reflection-heavy code).
  • Run as a non-root user (as shown) and keep file permissions minimal.
  • Avoid installing shell tools in the runtime image unless you need them.
  • Scan images for vulnerabilities regularly with your preferred scanner.
  • Keep base images updated; rebuild when Microsoft ships security patches.

Working with secrets and configuration

Never bake secrets into images. Prefer environment variables, mounted files, or a secrets manager. For private NuGet feeds during build, use Docker BuildKit secrets:

At runtime, keep configuration external: pass environment variables (-e Key=Value) or mount configuration files (-v /path:/app/config), or rely on your orchestrator’s config/secrets facilities.

Multi-architecture images

If you deploy to both x64 and ARM64 (e.g., cloud + Apple Silicon dev machines), publish a multi-arch image:

Add tests to your image build

Catch issues early by running tests in a separate stage. Example layout:

Wire this stage before publish so failing tests stop the pipeline.

Optional advanced pattern Native AOT for ultra-small services

For certain workloads, .NET 8’s Native AOT can produce a single self-contained binary with very fast startup.

Note: AOT has compatibility limits; validate your libraries and reflection usage.

Quick checklist

  • Use multi-stage builds (SDK for build, runtime for run).
  • Pin base image versions; rebuild regularly for patches.
  • Adopt a solid .dockerignore.
  • Run as non-root; keep images minimal.
  • Externalize secrets and config.
  • Automate with BuildKit, caching, and CI/CD.
  • Scan images and monitor CVEs.

Wrapping up

Building lean, reliable .NET Docker images is mostly about good structure and consistent habits: pick the right base, use multi-stage builds, keep layers cache-friendly, and ship non-root runtime images. With these practices in place, your images will build faster, deploy safer, and run predictably from developer machines to production.


Discover more from CPI Consulting

Subscribe to get the latest posts sent to your email.