Docker for .NET Developers: Complete Beginner to Pro Guide 2026

·

·

Docker and .NET have come a long way together. Microsoft now ships official .NET images, Visual Studio has first-class Docker support, and containerized .NET apps routinely run in production at scale. But if you’ve never containerized a .NET application before, the learning curve is steeper than the tutorials let on.

This guide starts from scratch — what Docker actually is, why .NET developers specifically benefit from it, and how to go from a fresh ASP.NET Core app to a production-ready container setup.

Table of Contents

Why .NET Developers Need Docker

The old “works on my machine” problem is real. A junior dev on Windows, a senior on Mac, and CI running Ubuntu — three environments, three potential failure modes. Docker eliminates that variable entirely.

Beyond that, .NET-specific reasons to containerize:

  • Runtime version pinning: Pin to .NET 8.0.3 exactly — no more “what runtime does production have?” surprises.
  • Side-by-side deployments: Run .NET 6 and .NET 8 apps on the same host without version conflicts.
  • Kubernetes readiness: Every major cloud Kubernetes offering (AKS, EKS, GKE) expects container images.
  • Dependency bundling: No more “install this NuGet feed on the server” — everything is in the image.

Prerequisites

  • Docker Desktop installed (Windows/Mac) or Docker Engine (Linux)
  • .NET 8 SDK
  • Basic command-line familiarity

Your First .NET Dockerfile

Create a new ASP.NET Core app and add a Dockerfile to the project root:

dotnet new webapi -n MyApi
cd MyApi

Now create Dockerfile:

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 8080

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyApi.csproj", "."]
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApi.dll"]

Build and run it:

docker build -t myapi:latest .
docker run -p 8080:8080 myapi:latest

Hit http://localhost:8080/weatherforecast — your .NET API is running in a container.

Understanding the Base Images

Microsoft publishes several .NET images. Choose carefully:

  • mcr.microsoft.com/dotnet/sdk:8.0 — Full SDK for building. ~700MB. Never use this as runtime.
  • mcr.microsoft.com/dotnet/aspnet:8.0 — ASP.NET Core runtime. ~200MB. Use for web apps.
  • mcr.microsoft.com/dotnet/runtime:8.0 — Base runtime. ~130MB. Use for console apps/workers.
  • mcr.microsoft.com/dotnet/runtime-deps:8.0 — Just native dependencies. ~40MB. Use with self-contained AOT apps.

Multi-Stage Builds: Smaller, Faster Images

The Dockerfile above already uses multi-stage builds — that’s the AS build and AS final pattern. Here’s why it matters:

Without multi-stage builds, your final image would contain the entire .NET SDK (~700MB). With multi-stage builds, only the compiled output is copied to the final image (~200MB). That’s a 3x size difference — multiply that across every push to your registry.

Optimized Multi-Stage Dockerfile for Production

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src

# Copy csproj and restore first (layer caching optimization)
COPY ["MyApi/MyApi.csproj", "MyApi/"]
RUN dotnet restore "MyApi/MyApi.csproj"

# Copy everything and build
COPY . .
WORKDIR "/src/MyApi"
RUN dotnet build "MyApi.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
RUN dotnet publish "MyApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyApi.dll"]

Layer Caching Trick

Notice that COPY ["MyApi.csproj", "MyApi/"] and RUN dotnet restore happen before copying source files. This is intentional — the restore layer only invalidates when your .csproj changes. If only your C# files changed, Docker reuses the cached restore layer. Builds go from 45 seconds to 8 seconds.

Docker Compose for .NET + SQL Server + Redis

Real apps don’t run in isolation. Here’s a production-realistic docker-compose.yml for a .NET API with SQL Server and Redis:

version: '3.8'

services:
  api:
    build:
      context: .
      dockerfile: MyApi/Dockerfile
    ports:
      - "8080:8080"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ConnectionStrings__DefaultConnection=Server=sqlserver;Database=MyDb;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True
      - ConnectionStrings__Redis=redis:6379
    depends_on:
      sqlserver:
        condition: service_healthy
      redis:
        condition: service_started

  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=YourStrong!Passw0rd
      - MSSQL_PID=Developer
    ports:
      - "1433:1433"
    volumes:
      - sqlserver_data:/var/opt/mssql
    healthcheck:
      test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "YourStrong!Passw0rd" -Q "SELECT 1" || exit 1
      interval: 10s
      timeout: 3s
      retries: 10

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

volumes:
  sqlserver_data:
  redis_data:

Start everything: docker compose up -d. Tear it down: docker compose down. Your entire dev environment in one command.

Running EF Core Migrations in Docker

# Run migrations against the containerized database
docker compose exec api dotnet ef database update

# Or run as a separate migration service
docker compose run --rm api dotnet ef database update

Local Development Workflow

Full rebuilds on every code change are painful. Use volume mounts for hot reload during development:

# docker-compose.override.yml (development only, not committed to prod)
version: '3.8'

services:
  api:
    build:
      target: build  # stop at build stage, don't publish
    volumes:
      - ./MyApi:/src/MyApi  # mount source
    environment:
      - DOTNET_USE_POLLING_FILE_WATCHER=1
    command: dotnet watch run --project MyApi/MyApi.csproj

Now docker compose up starts with hot reload — save a file, the app restarts inside the container automatically.

Debugging in Docker with VS Code

Add to your docker-compose.override.yml:

services:
  api:
    environment:
      - VSDBG_VERSION=latest
    ports:
      - "4024:4024"  # debugger port

Then use the VS Code Docker extension’s “Attach to .NET” feature to attach a full debugger to the running container. Breakpoints work exactly as expected.

Environment Variables and Configuration

.NET’s configuration system maps environment variables to appsettings.json keys using double-underscore as section separator:

// appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "..."
  }
}

// Equivalent environment variable
ConnectionStrings__DefaultConnection=Server=...

Using .env Files with Docker Compose

# .env (never commit this)
SA_PASSWORD=YourStrong!Passw0rd
JWT_SECRET=your-super-secret-key
# docker-compose.yml — reference .env variables
services:
  sqlserver:
    environment:
      - SA_PASSWORD=${SA_PASSWORD}

Docker Secrets for Production

In production (Docker Swarm or Kubernetes), use secrets instead of environment variables for sensitive values. .NET reads secrets from /run/secrets/ by convention when configured:

builder.Configuration.AddKeyPerFile("/run/secrets", optional: true);

Production Hardening Tips

Run as Non-Root User

FROM mcr.microsoft.com/dotnet/aspnet:8.0
# .NET 8 images include a non-root 'app' user
USER app
WORKDIR /app

Health Checks

# In Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1
// In Program.cs — map a health endpoint
app.MapHealthChecks("/health");

Minimize Image Size

  • Use -alpine variants where available (though .NET alpine images have limitations)
  • Add a .dockerignore file to exclude bin/, obj/, .git/, and *.md from the build context
  • Consider Native AOT for startup-time-critical apps (cuts image size by 60%+ and startup to milliseconds)
# .dockerignore
bin/
obj/
.git/
.vs/
*.user
*.md
Dockerfile*
docker-compose*

[INTERNAL_LINK: ASP.NET Core deployment to Azure Container Apps]

FAQ

Should I use Docker Desktop or Docker Engine on Windows?

Docker Desktop is the easiest path on Windows — it includes the Docker Engine, CLI, and Docker Compose in one installer. On Windows, you’ll want WSL2 backend enabled for better performance. Docker Engine alone (via WSL2) is an option if you want to avoid Docker Desktop’s licensing requirements for larger teams.

How do I access the containerized SQL Server from SSMS?

With the ports: - "1433:1433" mapping in place, connect SSMS to localhost,1433 with SQL authentication using the SA credentials you set. It works exactly like a locally installed SQL Server from the client’s perspective.

Why is my .NET Docker image so large?

If you’re using the SDK image as your final image, that’s why. Switch to a multi-stage build (shown above) and use the aspnet or runtime image for the final stage. Also add a .dockerignore to exclude build artifacts from the context.

Can I run Windows containers for .NET?

Yes, but Linux containers are strongly preferred for .NET in 2025. Linux images are smaller, faster to pull, and supported by all cloud container services. Windows containers are niche — use them only if you have a hard dependency on Windows-specific APIs (COM, registry, etc.).

How do I handle database migrations at startup?

The safest pattern is to run migrations as an init job or a startup check, not inline in your app startup. For smaller apps, using context.Database.MigrateAsync() at startup is acceptable. For production systems, run migrations as a separate step in your CI/CD pipeline before deploying the new container.

Docker with .NET is a well-worn path in 2025. The tooling is mature, the official images are well-maintained, and the development experience with hot reload and debugger attach is genuinely good. Start with the multi-stage Dockerfile, get Compose working locally with your dependencies, and you’ll have a setup that scales from your laptop to Kubernetes without changes.

[INTERNAL_LINK: .NET 8 deployment to Azure Kubernetes Service]


Leave a Reply

Your email address will not be published. Required fields are marked *