Real applications are several containers: a web service, a database, a cache, maybe a worker. Typing a long docker run for each is slow and error-prone. Docker Compose describes them all in one YAML file so docker-compose up creates the network, volumes and containers consistently, on a laptop or a single server.
Versions in use on this date. Docker Compose 1.26.2 (the Python-based docker-compose tool) with Docker Engine 19.03.12. Redis 6.0 was released in May 2020. Compose files use version: "3.8" syntax with this release; conditional depends_on health waits are not part of the 3.x format, so the example handles start-up ordering in the application and with restart policies.
The stack
flowchart TB
U[Users] -->|HTTPS 443| PX[nginx proxy<br/>published port]
PX --> API[api service<br/>Node.js, 2 replicas by scale]
API --> DB[(postgres:12<br/>volume pgdata)]
API --> RD[redis 6 cache<br/>volume redisdata]
W[worker service] --> RD
W --> DB
subgraph NET[Compose network app_default - names resolve]
PX
API
DB
RD
W
end
docker-compose.yml
version: "3.8"
services:
db:
image: postgres:12
restart: unless-stopped
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:6.0
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redisdata:/data
api:
build: ./api
image: myorg/api:${APP_VERSION:-dev}
restart: unless-stopped
environment:
DATABASE_URL: postgres://postgres:${DB_PASSWORD}@db:5432/app
REDIS_URL: redis://redis:6379
NODE_ENV: production
depends_on: [db, redis]
expose: ["3000"]
deploy:
resources:
limits: { memory: 512M }
proxy:
image: nginx:1.18
restart: unless-stopped
ports: ["80:80"]
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on: [api]
volumes:
pgdata:
redisdata:
.env next to the file (not committed to Git):
DB_PASSWORD=use-a-long-random-value
APP_VERSION=1.4.2
Everyday commands
docker-compose config # validate and show the merged config
docker-compose up -d # create and start in the background
docker-compose ps
docker-compose logs -f --tail 100 api
docker-compose exec db psql -U postgres app
docker-compose up -d --scale api=2 # needs no fixed container_name/ports on the api service
docker-compose pull && docker-compose up -d # update images
docker-compose stop; docker-compose start
docker-compose down # remove containers and network, keep volumes
docker-compose down -v # ALSO deletes volumes (data loss)
Compose names resources with a project prefix (the directory name by default), which is why two copies of the same stack in different directories do not collide. Set -p name or COMPOSE_PROJECT_NAME to choose it.
Start-up order and readiness
depends_on only controls start order, not readiness: Postgres may take several seconds to accept connections after its container starts. Handle it in three ways:
- Make the application retry its database connection with backoff (best, and also helps in production restarts).
- Use
restart: unless-stopped, so an app that failed early simply restarts. - Run a small wait script (
wait-for-it.sh) as the entrypoint.
The healthcheck on the database still helps: docker-compose ps shows healthy, and you can use it in monitoring.
Configuration practices
- Keep secrets out of the file:
.envis fine for a lab; for production use Docker secrets (Swarm) or inject from a vault at deploy time. - Use
docker-compose.override.ymlfor development settings (bind-mounted source, debug ports); the production file stays clean. - Do not publish the database port (
5432:5432) unless you need it, and then bind to127.0.0.1. - Pin image tags and keep
restartpolicies explicit. - Back up named volumes (see the volumes post), and test the restore.
Where Compose fits
Compose is excellent for development, CI test environments and small single-host deployments. It does not spread containers across servers or reschedule them when a host dies; for that you move to an orchestrator such as Kubernetes. The Compose file is still a useful starting point, because the same services, volumes and environment variables become your Kubernetes manifests.
Troubleshooting
ERROR: Version in "./docker-compose.yml" is unsupported: your Compose binary is older than theversion:key; upgrade Compose.- Service cannot reach another: use the service name as hostname (
db), notlocalhost. - Stale image used:
docker-compose build --no-cache apiordocker-compose pull. - Port already allocated: another container or process owns the host port.