Lab 3 wired two containers together by hand. Compose writes that down. The part worth learning properly is depends_on with a condition, because the naive version does not do what its name suggests.
1. The stack from lab 3, declared
mkdir -p ~/labs/compose && cd ~/labs/compose
cat > compose.yaml <<'YAML'
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: labpass
POSTGRES_DB: labdb
volumes:
- dbdata:/var/lib/postgresql/data
# No ports: only other services need to reach it.
app:
image: postgres:16-alpine
depends_on:
- db
environment:
PGPASSWORD: labpass
command: >
sh -c "psql -h db -U postgres -d labdb -c 'select 1' && echo APP_OK"
volumes:
dbdata:
YAML
docker compose config --quiet && echo "valid"
docker compose upIt fails. depends_on with a plain list waits for the container to start, not for the service inside it to be ready — and Postgres takes a few seconds to accept connections after its process exists.
This is the single most common Compose bug, and "add a sleep" is the single most common wrong fix.
Verify
2. Fix it with a healthcheck and a condition
cat > compose.yaml <<'YAML'
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: labpass
POSTGRES_DB: labdb
volumes:
- dbdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d labdb"]
interval: 2s
timeout: 3s
retries: 15
start_period: 5s
app:
image: postgres:16-alpine
depends_on:
db:
condition: service_healthy
environment:
PGPASSWORD: labpass
command: >
sh -c "psql -h db -U postgres -d labdb -c 'select 1' && echo APP_OK"
volumes:
dbdata:
YAML
docker compose upNow app starts only once db reports healthy. start_period is the grace window during which a failing check does not count against retries — for a database that initialises on first boot, omitting it is why the healthcheck itself fails.
Verify
3. Read the state
docker compose up -d db
sleep 8
docker compose ps
docker compose ps --format json | head -c 300; echo
docker inspect "$(docker compose ps -q db)" \
--format '{{.State.Health.Status}}'
docker inspect "$(docker compose ps -q db)" \
--format '{{range .State.Health.Log}}{{.ExitCode}} {{end}}'docker compose ps shows health alongside status. The Health.Log is the last few probe results with exit codes — which is where you look when a service is stuck in starting and you need to know what the probe is actually returning.
Verify
4. Configuration: env files, not hardcoded values
cat > .env <<'ENV'
POSTGRES_PASSWORD=labpass
POSTGRES_DB=labdb
PGPORT=5432
ENV
cat > compose.override.yaml <<'YAML'
services:
db:
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-labdb}
ports:
- "${PGPORT}:5432"
YAML
docker compose config | head -n 20Two substitution forms worth knowing. ${VAR:?message} fails the config if the variable is unset — use it for anything required, so a missing secret is a startup error rather than a silent empty string. ${VAR:-default} supplies a fallback.
compose.override.yaml is merged automatically on top of compose.yaml. That is the supported way to add development-only settings — the port exposure above — without touching the file you deploy.
Always check the merged result:
docker compose config | grep -A3 "ports:"Verify
5. Logs, restart, and tearing down properly
docker compose logs --tail 5 db
docker compose restart db
docker compose down
docker volume ls | grep -c compose_dbdata
docker compose down --volumes
docker volume ls | grep -c compose_dbdata || echo "0 — volume removed"down stops and removes containers and the network, and keeps volumes. That is the right default: it means down followed by up does not lose your database. --volumes is the explicit "and delete the data" — the flag to be careful with, and the one you want in CI.
Verify
What Compose does not do
It is a single-host tool. There is no scheduling, no rolling update, no restart across a fleet. It is excellent for development, integration tests, and a small single-server deployment; the moment you need more than one host, the concepts transfer to Kubernetes but the file does not.
Clean up
docker compose down --volumes 2>/dev/null
cd ~ && rm -rf ~/labs/composeWhere this goes next
The stack is declared. Next: the routine for when one of these containers will not start at all.