A Real Multi-Service Example
Time to put the chapters together: a small stack with a web frontend, a database, and a cache — plus an optional database admin tool. Every concept from the guide appears exactly once, doing the job it was made for.
The file
compose.yaml:
services:
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD before starting}
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
cache:
image: redis:7-alpine
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
web:
image: nginx:alpine
restart: unless-stopped
ports:
- "${WEB_PORT:-8080}:80"
volumes:
- ./html:/usr/share/nginx/html:ro
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
dbadmin:
image: adminer:latest
profiles: [tools]
ports:
- "127.0.0.1:8081:8080"
depends_on:
db:
condition: service_healthy
volumes:
pgdata:
And html/index.html next to it (bind-mounted
read-only into nginx):
<!DOCTYPE html>
<html><head><title>Our stack is up</title></head>
<body><h1>web + db + cache, all from one file</h1></body></html>
Reading it back, concept by concept
| Concept | Where it appears |
|---|---|
| Services with pinned images | postgres:16, redis:7-alpine, nginx:alpine |
| Named volume for data | pgdata keeps the database across restarts |
| Bind mount for your files | ./html — edit the page, reload, no rebuild |
| Env interpolation + fail-fast secret | ${DB_PASSWORD:?...} refuses to start without it |
| Healthchecks + readiness | web waits for healthy db and cache, not just started |
| Port publishing with a default | ${WEB_PORT:-8080}:80 |
| Profiles | dbadmin only starts with --profile tools |
| Localhost-only admin port | 127.0.0.1:8081:8080 — not exposed to the LAN |
Running it
export DB_PASSWORD="pick-something-long"
docker compose config --quiet && echo "valid" # always check first
docker compose up -d
docker compose ps
Check (verified with Compose v5.6.0):
docker compose config --services
cache
db
web
The admin tool stays off until asked:
docker compose --profile tools up -d # now dbadmin starts too
Then: http://localhost:8080 serves your page, and
http://localhost:8081 opens Adminer (log in with user
app, password $DB_PASSWORD, server db — the
service name, per Networks).
Tear it down:
docker compose down # containers + networks gone; pgdata kept
Swapping in your real app
This example uses nginx as a stand-in frontend so every
piece runs from public images. When you have a real app,
web becomes either image: your-registry/your-app:1.2
or a build: section pointing at your Dockerfile — the
rest of the file (database, cache, volumes, healthchecks)
doesn't change. That's the point of the exercise: the
stack shape is stable while the pieces evolve.
Next: Updating and Recreating