Skip to main content

Docker Compose

Volume 3 — Modern Development

A real local development environment usually needs more than one container — an API, a database, maybe a cache. Docker Compose defines all of them, and how they relate, in a single file, and brings the whole set up or down with one command.

1. What Compose Solves

Without Compose, running a multi-container setup means remembering (or scripting) a series of individual docker run commands, in the right order, with matching network and volume flags every time. Compose replaces all of that with one declarative file and one command.

docker compose up -d # start everything defined in docker-compose.yml
docker compose down # stop and remove everything it started

2. Anatomy of a Compose File

version: "3.9"

services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "5000:8080"
environment:
- ConnectionStrings__Default=Server=db;Database=RoundTrip;User Id=sa;Password=${DB_PASSWORD}
- ASPNETCORE_ENVIRONMENT=Development
depends_on:
db:
condition: service_healthy
networks:
- traxs-local

db:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
- ACCEPT_EULA=Y
- MSSQL_SA_PASSWORD=${DB_PASSWORD}
ports:
- "1433:1433"
volumes:
- sql-data:/var/opt/mssql
healthcheck:
test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-S", "localhost", "-U", "sa", "-P", "${DB_PASSWORD}", "-Q", "SELECT 1"]
interval: 10s
timeout: 5s
retries: 5
networks:
- traxs-local

redis:
image: redis:7-alpine
ports:
- "6379:6379"
networks:
- traxs-local

volumes:
sql-data:

networks:
traxs-local:
SectionPurpose
servicesEach container to run — its image or build instructions, ports, environment, dependencies
buildBuild from a local Dockerfile, instead of pulling a pre-built image
portsSame host:container mapping as docker run -p
environmentEnvironment variables passed into the container
depends_onStartup ordering — with condition: service_healthy, waits for the dependency's health check to pass, not just for its process to start
volumes (top-level)Named volumes, persisting data beyond a single docker compose down
networksA shared network so services can reach each other by service name

The service name doubles as its hostname. Inside the api container, the connection string above points at Server=db — not an IP address, not localhost — because Compose automatically makes db resolvable to the database container from anywhere else on the same network.

3. Why depends_on Alone Isn't Enough

Without a condition, depends_on only waits for the dependency's container process to start — not for the application inside it (like SQL Server) to actually be ready to accept connections. This is one of the most common sources of "works the second time, fails on a fresh docker compose up" bugs: the API container starts and immediately tries to connect before SQL Server has finished its own startup.

depends_on:
db:
condition: service_healthy # waits for db's healthcheck to pass, not just for it to start

Pairing depends_on: condition: service_healthy with an actual healthcheck block on the dependency (as shown in Section 2) is the fix — Compose won't start the dependent service until the health check genuinely succeeds.

4. Environment Variables and .env Files

Hardcoding secrets like DB_PASSWORD directly in docker-compose.yml means they end up committed to the repo. Instead, reference them with ${VARIABLE} syntax and provide the actual values in a .env file (gitignored) sitting next to the compose file:

# .env — never commit this file
DB_PASSWORD=LocalDevOnly!2026
docker compose config # prints the fully resolved compose file, with variables substituted — useful for confirming what's actually being used

5. Everyday Commands

docker compose up -d # start everything, detached
docker compose down # stop and remove containers (keeps named volumes)
docker compose down -v # also remove named volumes — wipes the database data too
docker compose ps # list this project's containers and their status
docker compose logs -f api # follow logs for one specific service
docker compose exec api bash # open a shell inside a running service's container
docker compose build # rebuild images without starting containers
docker compose up -d --build # rebuild and restart in one step — the standard "I changed the Dockerfile" command
docker compose restart api # restart a single service without touching the others

docker compose down -v is worth calling out specifically: the -v deletes named volumes, including your local database's actual data. It's the right command when you deliberately want a clean slate, and the wrong one to run out of habit.

6. Scaling a Service Locally

docker compose up -d --scale api=3

Runs three instances of the api service simultaneously — useful for locally testing behavior that depends on multiple backend instances (like session handling or load distribution, covered in the Load Balancers & Reverse Proxies chapter) without needing actual production infrastructure to observe it.

7. Troubleshooting Playbook

SymptomLikely causeWhat to check
API container fails on first docker compose up, works on retrydepends_on without a health-check condition — API started before the database was actually readyAdd a healthcheck block to the database service and condition: service_healthy to the dependent service
"port is already allocated"Another Compose project or local process already bound to that host portdocker ps across all projects; change the host-side port mapping if needed
Service can't resolve another service by nameBoth aren't on the same networks entryConfirm both services list the same custom network explicitly
Environment variable is empty inside the container.env file missing, or variable name mismatchdocker compose config to see the fully resolved values actually being passed in
Data disappeared after docker compose downRan with -v, removing named volumesAvoid -v unless a clean slate is actually intended; back up meaningful local dev data separately if needed
Rebuilt image, but container still runs old codedocker compose up -d without --build reuses the existing imagedocker compose up -d --build to force a rebuild
SQL Server container exits immediatelyMissing ACCEPT_EULA=Y, or insufficient memory allocated to the Docker/OrbStack VMCheck docker compose logs db for the actual startup error; SQL Server typically needs at least 2GB allocated

8. Quick Reference

CategoryCommandPurpose
Startdocker compose up -dStart all services, detached
Stopdocker compose downStop and remove containers, keep volumes
Stopdocker compose down -vAlso remove named volumes — wipes data
Rebuilddocker compose up -d --buildRebuild images and restart
Logsdocker compose logs -f serviceFollow one service's logs
Shelldocker compose exec service bashOpen a shell inside a running service
Statusdocker compose psList this project's containers
Debugdocker compose configPrint the fully resolved compose file
Scaledocker compose up -d --scale service=3Run multiple instances of one service
Healthdepends_on: condition: service_healthyWait for a dependency's health check, not just its start

Part of the Traxs Engineering Handbook — Volume 3: Modern Development. Companion chapters in this volume: Docker Fundamentals, REST APIs, Authentication & Authorization, JSON & YAML, Postman Guide.