20.2.1.1 Compose Multi Service
A focused guide to Compose Multi Service, connecting core concepts with practical Docker and container operations.
A multi-service Compose application defines several containers that work together as a system. Each container is responsible for one service — web server, database, cache, message queue, background worker — and Compose manages their shared network, volumes, startup order, and lifecycle as a single unit. This is the primary pattern for running realistic application stacks in development and on single-host production servers.
Anatomy of a Multi-Service compose.yml
A three-service application — Node.js API, PostgreSQL database, Redis cache — defined in a single Compose file:
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/app
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d app"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
volumes:
- cache_data:/data
volumes:
db_data:
cache_data:
How Services Find Each Other
Compose creates a private Docker network for the application and registers each service name as a DNS hostname. The api service connects to PostgreSQL using the hostname db and to Redis using the hostname cache. These names resolve inside the Compose network automatically — no hardcoded IP addresses, no manual network configuration.
The connection strings in the environment variables reflect this:
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/app
REDIS_URL: redis://cache:6379
db and cache are the service names from the compose.yml. The ports (5432, 6379) are the container-internal ports, not any host-published ports.
Starting the Stack
docker compose up -d
Compose pulls any missing images (postgres:15-alpine, redis:7-alpine), builds the api service from the local Dockerfile, creates the named volumes (db_data, cache_data), creates the network, and starts all containers. The depends_on conditions ensure the database passes its healthcheck before the API starts.
Verifying All Services
docker compose ps
NAME IMAGE STATUS PORTS
myapp-api-1 myapp-api running 0.0.0.0:3000->3000/tcp
myapp-db-1 postgres:15-alpine running(healthy) 5432/tcp
myapp-cache-1 redis:7-alpine running 6379/tcp
The (healthy) status next to db indicates the healthcheck is passing.
Watching Logs Across All Services
docker compose logs -f
Streams logs from all three services simultaneously, each line prefixed with the service name:
myapp-api-1 | Server listening on port 3000
myapp-db-1 | database system is ready to accept connections
myapp-cache-1 | Ready to accept connections
To watch logs for one service only:
docker compose logs -f api
Restarting a Single Service
During development, when you change the application code and rebuild:
docker compose up -d --build api
Compose rebuilds the api image and restarts only that container, leaving db and cache running undisturbed.
Running Database Migrations
Use docker compose exec to run a command inside a running service container:
docker compose exec api npm run migrate
This runs the migration command inside the api container, where the DATABASE_URL environment variable is already set and the container has network access to db.
Scaling a Service
To run multiple instances of a stateless service:
docker compose up -d --scale api=3
Compose starts three containers for the api service. Note that port publishing (ports: - "3000:3000") conflicts with scaling when the same host port is mapped — you would need to remove the fixed port mapping and use a load balancer or proxy to distribute traffic.
Stopping and Cleaning Up
Stop all services without removing them:
docker compose stop
Stop and remove containers and networks, keeping volumes:
docker compose down
Stop and remove everything including volumes (deletes data):
docker compose down -v
Separate Concerns with Multiple Compose Files
Keep the base compose.yml clean for production use, and add development-only overrides in compose.override.yml, which Compose loads automatically:
compose.override.yml:
services:
api:
volumes:
- .:/app
environment:
NODE_ENV: development
command: npm run dev
In development, the source code directory is bind-mounted into the container so changes are reflected immediately without rebuilding the image. In production (where the override file is not present), the image's built-in code is used.
Environment Variable Management
Use a .env file in the project directory for secrets and environment-specific values:
POSTGRES_PASSWORD=devsecret123
APP_ENV=development
Compose reads .env automatically. Add .env to .gitignore so secrets are never committed. Provide a .env.example file with placeholder values that can be committed safely.
Service Topology Visualization
The api service publishes port 3000 to the host. The db and cache services have no published ports — they are reachable only from within the Compose network, reducing the host's exposed attack surface.
Common Patterns in Multi-Service Compose
Database initialization: Mount SQL files into the PostgreSQL container's /docker-entrypoint-initdb.d/ directory to run initialization scripts on first startup:
db:
image: postgres:15-alpine
volumes:
- db_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
Service restart policy: Add restart: unless-stopped to services that should recover from failures automatically:
services:
api:
restart: unless-stopped
db:
restart: unless-stopped
Named networks: To isolate groups of services from each other, define explicit networks and assign services to them, preventing services from communicating across security boundaries.