Always set image: next to build: — you get a predictable tag you can push, scan, or kind load later.
Configuration
environment, env_file, interpolation
services:
api:
env_file:
- .env # read by Compose AND passed to the container
environment:
APP_THEME: ${APP_THEME:-emerald}
APP_GREETING: ${APP_GREETING:-Welcome to Pinboard (Compose)}
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
${VAR} is substituted by Compose, from the shell and from .env, before the file is parsed
${VAR:-default} falls back; ${VAR:?message} fails loudly if it is unset
To pass a literal $ into the container, double it: $${POSTGRES_USER}
.env holds credentials, is git-ignored, and ships as .env.example
docker compose up -d --wait # create everything, block until healthy/started
docker compose ps # state, health, published ports
docker compose logs -f api # follow one service (no -f: dump and exit)
docker compose exec db psql -U pinboard -d pinboard
docker compose restart api
docker compose stop # keep containers, keep data
docker compose down # remove containers + the project network
docker compose down -v # …and named volumes: the notes are gone
down -v is the destructive one. down keeps pinboard-data; down -v deletes it.
More of the same thing
Scaling a service
docker compose up -d --scale api=3 --wait
docker compose ps
# pinboard-api-1 pinboard-api-2 pinboard-api-3
for i in $(seq 6); do
docker compose exec -T web wget -qO- http://api:8080/api/info
done # the "hostname" field rotates between the three containers
Compose DNS returns all three container IPs for api — clients round-robin
A service with a fixed ports mapping cannot be scaled: only one container can own host port 8080
This is manual scaling on one machine. Nothing rebalances, nothing reacts to load
Optional pieces
Profiles
Services in a profile are not started unless the profile is on
Keep debug tools, seeders and test runners in the same file without slowing everyone down
Enable with --profile or the COMPOSE_PROFILES environment variable
docker compose --profile tools up -d
COMPOSE_PROFILES=tools docker compose up -d
docker compose watch syncing the Pinboard web folder — then touch the Dockerfile and watch it rebuild.
What you'll see · sync in milliseconds, rebuild in secondsFiles · src/compose.override.yaml, src/pinboard-web/htmlDuration · 5 min
Composition
Multiple files & overrides
compose.yaml + compose.override.yaml are merged automatically
-f takes explicit control — first file is the base, later files override
Scalars are replaced, mappings are merged, most sequences are appended
Paths stay relative to the first file
# dev: base + override, automatically
docker compose up -d --wait
# prod-ish: base + an explicit second file
docker compose \
-f compose.yaml \
-f compose.prod.yaml \
up -d --wait
Keep the base file environment-neutral. Put ports, bind mounts and develop: rules in the override.
Trust, then verify
compose config renders the truth
docker compose config # merged + interpolated + validated
docker compose config --services # just the names
docker compose config --images # what would be pulled or built
docker compose -f compose.yaml -f compose.prod.yaml config > rendered.yaml
Shows the file after variable substitution — the fastest way to catch an empty ${POSTGRES_PASSWORD}
Fails with a clear error on invalid YAML or unknown keys: a free lint step in CI
Reveals exactly how your override files merged, so you stop guessing
Automation
Compose in CI
docker compose config -q # lint the file
docker compose build --pull
docker compose up -d --wait # non-zero exit if never healthy
docker compose run --rm --no-deps tests # exit code = test result
docker compose logs --no-color > compose-logs.txt
docker compose down -v --remove-orphans # always, even on failure
--wait plus health checks removes every sleep 30 from your pipeline
Alternative: up --abort-on-container-exit --exit-code-from tests
Set name: or -p per build so parallel jobs never collide
The ceiling
What Compose cannot do → Kubernetes
What you need
Compose
Kubernetes
Run across many machines
One Docker host. Full stop.
A scheduler places Pods on any node in the cluster
Survive a machine dying
Restart policy — on that same host
Controllers reschedule the workload elsewhere
Zero-downtime updates
up stops the old container, then starts the new one
Deployment rolling update, readiness gates, rollout undo
Scale with demand
--scale api=3, typed by a human
HorizontalPodAutoscaler on CPU or custom metrics
Secrets at scale
A .env file on someone's laptop
Secret objects, RBAC, encryption at rest, external stores
Compose is excellent for one machine: development, CI, small single-host deployments. Everything above needs an orchestrator.
The landscape
Where Swarm, Nomad and Kubernetes fit
Docker Swarm
Built into the engine, reads Compose files with docker stack deploy. Easiest step up — but a small ecosystem and little momentum.
HashiCorp Nomad
One binary, schedules containers and plain binaries or VMs. Simple and solid; a niche compared to K8s.
Kubernetes
The de-facto standard: every cloud offers it managed, and the whole CNCF ecosystem targets its API. Sessions 05–10.
Compose files do not become Kubernetes manifests. Converters like kompose give you a starting point, never a production result.
Lab 04 · 30–40 min
Pinboard in one file
Turn yesterday's wall of docker run flags into a compose.yaml with health checks and dependency conditions, bring it up with one command, and edit the site live.
You'll practise:compose.yaml · .env · healthcheck · depends_on: service_healthy · up -d --wait · compose watch · --scale api=3 · down vs down -v
Session 04
Key takeaways
One file, one command
The Compose Specification — no version:, docker compose with a space, up -d --wait.
Health drives startup
healthcheck plus depends_on: condition: service_healthy ends the startup race.
Great on one host
watch, overrides and config for the dev loop and CI. Multi-host, self-healing and autoscaling are out of scope.
Next up
Session 05 — Kubernetes architecture, kubectl and Pods.
Questions?
Up next — Session 05: Kubernetes — architecture, kubectl, Pods. One host was never going to be enough.