Docker from Basics to Production · Containerize Real Applications
A Development Stack with Docker Compose
Run an API, PostgreSQL and Redis together with Compose: service names, healthchecks, volumes and live code reloading.
What you'll build
A compose.yaml that starts PostgreSQL and Redis with healthchecks and named volumes, plus an API container that connects to both by service name once they are healthy. One command brings the stack up; one takes it down.
The idea
- Service names are hostnames on the project network: the API connects to
db:5432andcache:6379. - Healthchecks +
condition: service_healthymake the API wait for databases that are actually ready. - Named volumes keep database data between
downandup(unless you pass-v). - For development, bind-mount the source into the API container and run with auto-reload;
docker compose watchcan sync or rebuild on file changes. - Publish database ports only to
127.0.0.1if you publish them at all.
The code
trap 'docker compose -p rc-stack down -v > /dev/null 2>&1' EXIT
cat > compose.yaml <<'EOF'
services:
db:
image: postgres:18-alpine
environment:
POSTGRES_PASSWORD: devpassword
POSTGRES_DB: shop
volumes: [pgdata:/var/lib/postgresql]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d shop"]
interval: 1s
retries: 30
cache:
image: redis:8-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 1s
retries: 30
api:
image: postgres:18-alpine # stands in for your API image; it has psql for this demo
environment:
PGPASSWORD: devpassword
command:
- sh
- -c
- |
psql -h db -U postgres -d shop -tAc "create table orders(id int); insert into orders values (1),(2);" > /dev/null
echo "orders in db: $(psql -h db -U postgres -d shop -tAc 'select count(*) from orders')"
echo "cache says: $(printf 'PING\r\n' | nc -w 1 cache 6379 | tr -d '\r+')"
depends_on:
db: {condition: service_healthy}
cache: {condition: service_healthy}
volumes:
pgdata:
EOF
docker compose -p rc-stack up -d db cache --wait > /dev/null 2>&1
docker compose -p rc-stack run --rm api 2>/dev/null
docker compose -p rc-stack ps --format '{{.Service}}: {{.Health}}' | sort
echo "volume kept for next time: $(docker volume ls --filter name=rc-stack_pgdata --format '{{.Name}}')"
Output
orders in db: 2 cache says: PONG cache: healthy db: healthy volume kept for next time: rc-stack_pgdata
Run this on your computer — see “Get Started” for setup.
Common mistakes
depends_onwithout a condition, so the API starts before the database accepts connections.- Connecting to
localhostfrom one container to reach another; use the service name. - Committing real credentials in
compose.yaml; use a git-ignored.envfile for anything sensitive. - Running
docker compose down -vby habit and losing your local data.
Key points
- Compose gives every service a DNS name on a project network.
- Healthchecks plus
service_healthyconditions order startup by readiness. - Named volumes persist data; bind mounts and
watchspeed up development.
Further reading: Compose file reference · Compose watch
Practice questions
How does a Compose service connect to a database service called db?
Answer
By the hostname db on the project network that Compose creates; never localhost, which would be the service's own container.
What does docker compose up --wait do?
Answer
Starts the services in the background and waits until they are running and healthy (for services with healthchecks) before returning, which is useful in scripts and CI.
Where should local development secrets for Compose live?
Answer
In a git-ignored .env file or env_file referenced by compose.yaml, not committed to the compose file itself.
Open in the app to mark lessons complete, take notes and track your progress.