Docker Compose defines and runs multi-container applications. Instead of manually starting an API container, a SQL Server container, and a Redis container with individual docker run commands (and remembering all the network flags, volume mounts, and environment variables), a docker-compose.yml file declares the entire stack. One command — docker compose up — starts everything in the right order. This is the standard local development environment for the ASP.NET Core Web API in this series: the developer runs the stack, codes against it, and tears it down cleanly.
Docker Compose for the Full Stack
// ── docker-compose.yml ─────────────────────────────────────────────────────
version: '3.8'
services:
# ── ASP.NET Core Web API ──────────────────────────────────────────────────
blogapp-api:
build:
context: .
dockerfile: src/BlogApp.Api/Dockerfile
container_name: blogapp-api
ports:
- "8080:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__Default=Server=sqlserver;Database=BlogApp_Dev;User Id=sa;Password=Dev@Password123;TrustServerCertificate=True
- ConnectionStrings__Redis=redis:6379
- JwtSettings__SecretKey=dev-only-secret-key-at-least-32-chars
- JwtSettings__Issuer=http://localhost:8080
depends_on:
sqlserver:
condition: service_healthy
redis:
condition: service_healthy
# ── SQL Server ────────────────────────────────────────────────────────────
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
container_name: blogapp-sqlserver
ports:
- "1433:1433" # expose for SSMS access from host
environment:
- SA_PASSWORD=Dev@Password123
- ACCEPT_EULA=Y
- MSSQL_PID=Developer # free Developer edition
volumes:
- sqlserver-data:/var/opt/mssql # persist data across restarts
healthcheck:
test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-S", "localhost",
"-U", "sa", "-P", "Dev@Password123", "-Q", "SELECT 1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s # SQL Server takes ~20s to start
# ── Redis ─────────────────────────────────────────────────────────────────
redis:
image: redis:7-alpine
container_name: blogapp-redis
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
volumes:
sqlserver-data: # named volume — survives docker compose down
depends_on with condition: service_healthy waits for a service’s healthcheck to pass before starting dependent services. Without this, the API container starts before SQL Server is ready and the application fails to connect. A simple depends_on: sqlserver (without condition) only waits for the container to start, not for SQL Server to be ready to accept connections — the healthcheck condition is the correct way to ensure readiness.docker-compose.override.yml for development-specific settings that should not be in the base docker-compose.yml. Docker Compose automatically merges the base file with the override file. The override can add volume mounts for hot reload, expose additional debug ports, or set development-only environment variables. The base file serves as the template for CI and production; the override file is for developer convenience and can be gitignored if it contains sensitive values.docker-compose.yml SA_PASSWORD value (or any hardcoded password) in production. The compose file shown is strictly for local development. For production, passwords and secrets come from environment variables, Docker Secrets, or a vault service. Also note that named volumes (sqlserver-data) persist data across docker compose down but are deleted by docker compose down -v — use with awareness in development.Common Docker Compose Commands
// ── Starting and stopping ─────────────────────────────────────────────────
docker compose up -d // start all services in background
docker compose up --build -d // rebuild images then start
docker compose down // stop and remove containers (data volumes preserved)
docker compose down -v // stop and remove containers AND volumes (destroys data!)
// ── Viewing logs ──────────────────────────────────────────────────────────
docker compose logs -f // follow all service logs
docker compose logs -f blogapp-api // follow one service
// ── Running one-off commands ──────────────────────────────────────────────
docker compose exec blogapp-api dotnet ef database update // run EF migrations
docker compose exec sqlserver sqlcmd -S localhost -U sa -P Dev@Password123
// ── Scaling services ──────────────────────────────────────────────────────
docker compose up --scale blogapp-api=3 -d // run 3 API instances (needs load balancer)
Common Mistakes
Mistake 1 — Not using healthchecks in depends_on (race condition on startup)
❌ Wrong — API starts before SQL Server is ready, fails to connect, crashes:
depends_on:
- sqlserver # only waits for container start, not SQL Server readiness
✅ Correct — use condition: service_healthy with a defined healthcheck.
Mistake 2 — Not using named volumes for SQL Server data (data lost on compose down)
❌ Wrong — anonymous volume or no volume; all database data lost when container stops.
✅ Correct — use a named volume (sqlserver-data:/var/opt/mssql) for persistent development data.