Docker Compose for Local .NET Development

Most .NET applications depend on external services — a database, a cache, a message broker. Docker Compose lets you define these dependencies as code and spin up a complete local environment with a single command.

A Basic Compose File

Here's a docker-compose.yml for an ASP.NET Core API that depends on PostgreSQL and Redis:

docker-compose.yml
services:
  api:
    build:
      context: .
      dockerfile: src/MyApi/Dockerfile
    ports:
      - "5000:8080"
    environment:
      - ConnectionStrings__Default=Host=postgres;Database=myapp;Username=postgres;Password=devpass
      - Redis__ConnectionString=redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started

  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: myapp
      POSTGRES_PASSWORD: devpass
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

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

volumes:
  pgdata:

Run docker compose up and your entire stack comes up. The depends_on with condition: service_healthy ensures the API waits for PostgreSQL to be ready before starting.

Development Without Containerising Your App

Often you want the infrastructure in containers but your .NET app running locally with hot reload. Use a compose file for just the dependencies:

docker-compose.infra.yml
services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: myapp
      POSTGRES_PASSWORD: devpass
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

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

  seq:
    image: datalust/seq:latest
    environment:
      ACCEPT_EULA: "Y"
    ports:
      - "5341:5341"
      - "8081:80"

volumes:
  pgdata:

Start the infrastructure with docker compose -f docker-compose.infra.yml up -d, then run your API with dotnet watch:

terminal
dotnet watch run --project src/MyApi

This gives you the best of both worlds: containerised dependencies with full hot reload and debugger support for your .NET code.

Using Compose Profiles

If some services are only needed occasionally, use profiles:

config.yaml
services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: myapp
      POSTGRES_PASSWORD: devpass
    ports:
      - "5432:5432"

  mailhog:
    image: mailhog/mailhog
    profiles: ["email"]
    ports:
      - "1025:1025"
      - "8025:8025"

  jaeger:
    image: jaegertracing/all-in-one:latest
    profiles: ["tracing"]
    ports:
      - "16686:16686"
      - "4317:4317"

PostgreSQL starts with every docker compose up. MailHog only starts when you explicitly request it: docker compose --profile email up.

Environment Files

Keep credentials out of your compose file with .env:

POSTGRES_PASSWORD=devpass
POSTGRES_DB=myapp

Reference them in docker-compose.yml:

config.yaml
services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

Database Initialisation

You can seed your database by mounting SQL scripts:

config.yaml
  postgres:
    image: postgres:17
    environment:
      POSTGRES_DB: myapp
      POSTGRES_PASSWORD: devpass
    volumes:
      - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
      - pgdata:/var/lib/postgresql/data

PostgreSQL automatically executes any .sql files in /docker-entrypoint-initdb.d/ when the database is first created.

Running EF Core Migrations

You can run Entity Framework Core migrations against your containerised database:

terminal
# Start just the database
docker compose up postgres -d

# Run migrations from your project
dotnet ef database update --project src/MyApi

Or automate it with an init service in compose:

config.yaml
  migrate:
    build:
      context: .
      dockerfile: src/MyApi/Dockerfile
      target: build
    command: dotnet ef database update --project src/MyApi
    depends_on:
      postgres:
        condition: service_healthy
    profiles: ["migrate"]

Run migrations on demand with docker compose --profile migrate run migrate.

Compose Watch

Docker Compose 2.22+ supports watch mode for automatic rebuilds:

config.yaml
services:
  api:
    build:
      context: .
    develop:
      watch:
        - action: rebuild
          path: ./src
          target: /src

Run docker compose watch and your container rebuilds automatically when source files change. This is useful when you need your app running inside a container (for networking or platform-specific testing) but still want a fast feedback loop.

Summary

Docker Compose is the simplest way to manage local development dependencies. The key insight is that you don't have to containerise everything — run your infrastructure in containers and your .NET code locally for the best development experience. Use profiles for optional services, health checks for startup ordering, and volumes for data persistence.