Docker Compose for .NET Developers

Docker Compose for .NET Developers

Markus Lehmann ยท September 05, 2025
Docker.NETDevOps
Docker.NET

Docker Compose remains one of the most practical tools for local development of multi-service applications. While .NET Aspire offers a more integrated experience, many teams still rely on Docker Compose, especially in polyglot environments. Here's how to set it up effectively for .NET projects.

The Basic Dockerfile

Every .NET service needs a Dockerfile. The standard multi-stage approach keeps images small and build times reasonable:

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

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApp.Api.dll"]

A common mistake is copying all source files before restoring, which invalidates the NuGet cache on every code change. Always copy the .csproj files first, run dotnet restore, and then copy the rest of the source.

Structuring the Compose File

A typical .NET application with a database, cache, and API looks like this:

services:
  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      ACCEPT_EULA: "Y"
      SA_PASSWORD: "YourStrong!Passw0rd"
    ports:
      - "1433:1433"
    volumes:
      - sqldata:/var/opt/mssql

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  api:
    build:
      context: .
      dockerfile: MyApp.Api/Dockerfile
    ports:
      - "5000:8080"
    environment:
      ConnectionStrings__DefaultConnection: "Server=sqlserver;Database=MyApp;User=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=true"
      ConnectionStrings__Redis: "redis:6379"
    depends_on:
      sqlserver:
        condition: service_healthy
      redis:
        condition: service_started

volumes:
  sqldata:

Health Checks Are Essential

The depends_on directive alone only waits for the container to start, not for the service inside it to be ready. Use health checks to ensure dependencies are actually available:

sqlserver:
  image: mcr.microsoft.com/mssql/server:2022-latest
  healthcheck:
    test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$$SA_PASSWORD" -Q "SELECT 1" -C -b
    interval: 10s
    timeout: 5s
    retries: 5
    start_period: 30s

Without this, your API container might start before SQL Server is accepting connections, causing startup failures.

Managing Multiple Services

When you have several .NET services, avoid duplicating build configurations. Use a shared build context:

services:
  api:
    build:
      context: .
      dockerfile: src/MyApp.Api/Dockerfile
    environment:
      ASPNETCORE_ENVIRONMENT: Development

  worker:
    build:
      context: .
      dockerfile: src/MyApp.Worker/Dockerfile
    environment:
      DOTNET_ENVIRONMENT: Development

The shared context at the solution root allows each Dockerfile to reference shared projects and NuGet packages correctly.

Development vs. Production Compose Files

Use override files to separate development and production configurations:

# Development (default)
docker compose up

# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up

The base file contains shared configuration, while the override files adjust ports, volumes, environment variables, and build settings for each environment.

Hot Reload with Volume Mounts

For a faster development loop, mount your source code into the container and use dotnet watch:

api:
  build:
    context: .
    dockerfile: MyApp.Api/Dockerfile.dev
  volumes:
    - ./src:/src
  command: dotnet watch run --project /src/MyApp.Api/MyApp.Api.csproj

This gives you live reloading without rebuilding the container image on every change. The trade-off is that the development Dockerfile needs the full SDK image.

Practical Tips

Use .env files for sensitive values instead of hardcoding them in the compose file. Docker Compose automatically loads variables from a .env file in the same directory.

Name your volumes explicitly to persist data between restarts. Anonymous volumes are recreated on every docker compose down.

Set memory limits for SQL Server containers. By default, SQL Server will consume as much memory as it can:

sqlserver:
  deploy:
    resources:
      limits:
        memory: 2G

Use docker compose logs -f api to tail logs from a specific service instead of the noisy output from all containers combined.

Docker Compose is a mature, well-understood tool that works reliably for local .NET development. Master these patterns, and you'll have a reproducible development environment that any team member can start with a single command.


Comments (3)

David Mar 10, 2026 · 17:08

Could you elaborate on this topic in a follow-up post?

Greta Feb 27, 2026 · 20:08

Thanks for your comment — glad it helped!

Hannah Mar 07, 2026 · 23:08

Exactly! I had the same thought.

Elena Mar 11, 2026 · 17:08

This is exactly what I was looking for, thank you!

Felix Mar 12, 2026 · 17:08

I have a question: does this also apply to older versions?


Leave a comment

An unhandled error has occurred. Reload ๐Ÿ—™

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.