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:
| Section | Purpose |
|---|---|
services | Each container to run — its image or build instructions, ports, environment, dependencies |
build | Build from a local Dockerfile, instead of pulling a pre-built image |
ports | Same host:container mapping as docker run -p |
environment | Environment variables passed into the container |
depends_on | Startup 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 |
networks | A 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
| Symptom | Likely cause | What to check |
|---|---|---|
API container fails on first docker compose up, works on retry | depends_on without a health-check condition — API started before the database was actually ready | Add 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 port | docker ps across all projects; change the host-side port mapping if needed |
| Service can't resolve another service by name | Both aren't on the same networks entry | Confirm both services list the same custom network explicitly |
| Environment variable is empty inside the container | .env file missing, or variable name mismatch | docker compose config to see the fully resolved values actually being passed in |
Data disappeared after docker compose down | Ran with -v, removing named volumes | Avoid -v unless a clean slate is actually intended; back up meaningful local dev data separately if needed |
| Rebuilt image, but container still runs old code | docker compose up -d without --build reuses the existing image | docker compose up -d --build to force a rebuild |
| SQL Server container exits immediately | Missing ACCEPT_EULA=Y, or insufficient memory allocated to the Docker/OrbStack VM | Check docker compose logs db for the actual startup error; SQL Server typically needs at least 2GB allocated |
8. Quick Reference
| Category | Command | Purpose |
|---|---|---|
| Start | docker compose up -d | Start all services, detached |
| Stop | docker compose down | Stop and remove containers, keep volumes |
| Stop | docker compose down -v | Also remove named volumes — wipes data |
| Rebuild | docker compose up -d --build | Rebuild images and restart |
| Logs | docker compose logs -f service | Follow one service's logs |
| Shell | docker compose exec service bash | Open a shell inside a running service |
| Status | docker compose ps | List this project's containers |
| Debug | docker compose config | Print the fully resolved compose file |
| Scale | docker compose up -d --scale service=3 | Run multiple instances of one service |
| Health | depends_on: condition: service_healthy | Wait 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.