Docker Compose Complete Guide 2025 — Multi-Container Apps Made Simple
Advertisement
Introduction
Why This Matters
Running a modern web application locally requires more than just your app process. A typical stack includes the application server, a relational database, a cache like Redis, a message queue, and perhaps a search engine. Manually starting, networking, and configuring all these services is error-prone and hard to document.
Docker Compose solves this by describing your entire stack in a single YAML file that every team member can run with one command: docker compose up. It eliminates "it works on my machine" for entire development environments, not just individual containers. CI pipelines use Docker Compose to spin up integration test environments in seconds.
In 2025, Docker Compose v2 is bundled with Docker Desktop and is the standard for local development across teams of all sizes. Learning Compose is also a gentle introduction to the declarative configuration concepts behind Kubernetes manifests.
Docker Compose File Structure
# docker-compose.yml
version: '3.8' # Compose file format version
services: # Container definitions
api: # Service name (also DNS hostname on the network)
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=development
depends_on:
- db
db:
image: postgres:16-alpine
volumes: # Named volumes (persist data across restarts)
pgdata:
networks: # Custom networks (optional; Compose creates one by default)
backend:Complete Real-World Example
A Node.js API with PostgreSQL, Redis, and Nginx reverse proxy:
version: '3.8'
services:
nginx:
image: nginx:1.27-alpine
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- api
api:
build:
context: .
dockerfile: Dockerfile
environment:
- NODE_ENV=development
- DATABASE_URL=postgres://app:secret@db:5432/appdb
- REDIS_URL=redis://redis:6379
volumes:
- ./src:/app/src
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
redis:
image: redis:7-alpine
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
worker:
build: .
command: node worker.js
environment:
- DATABASE_URL=postgres://app:secret@db:5432/appdb
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
restart: unless-stopped
volumes:
pgdata:
redis_data:Essential Docker Compose Commands
# Start all services in background
docker compose up -d
# Start and rebuild images before starting
docker compose up -d --build
# Stop services (keep containers and volumes)
docker compose stop
# Stop and remove containers (keep volumes)
docker compose down
# Stop and remove containers AND volumes
docker compose down -v
# View logs from all services
docker compose logs -f
# View logs from a specific service
docker compose logs -f api
# Scale a service
docker compose up -d --scale api=3
# Execute a command in a running service
docker compose exec api /bin/sh
docker compose exec db psql -U app -d appdb
# Restart a single service
docker compose restart api
# View service status
docker compose psEnvironment Variables and .env Files
Docker Compose automatically loads a .env file from the project directory:
# .env (do not commit this file)
POSTGRES_USER=app
POSTGRES_PASSWORD=super_secret
POSTGRES_DB=appdb
API_PORT=3000
NODE_ENV=developmentReference these variables in docker-compose.yml:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
api:
ports:
- "${API_PORT}:3000"
environment:
- NODE_ENV=${NODE_ENV}For different environments, layer multiple Compose files:
# docker-compose.override.yml is auto-merged with docker-compose.yml in dev
# For production:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -dHealth Checks and Startup Dependencies
depends_on alone only waits for the container to start, not for the service inside it to be ready. Always pair with health checks for databases:
services:
api:
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30sNetworking in Docker Compose
All services share a default bridge network and resolve each other by service name:
services:
api:
environment:
# 'db' and 'redis' are hostnames — no IPs needed
- DATABASE_URL=postgres://app:pass@db:5432/mydb
- REDIS_URL=redis://redis:6379For network isolation between service tiers:
services:
nginx:
networks: [frontend]
api:
networks: [frontend, backend]
db:
networks: [backend] # not accessible directly from nginx
networks:
frontend:
backend:Compose for CI/CD Integration Tests
# docker-compose.test.yml
version: '3.8'
services:
test:
build:
context: .
target: test
environment:
- DATABASE_URL=postgres://test:test@db:5432/testdb
depends_on:
db:
condition: service_healthy
command: npm test
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test"]
interval: 5s
retries: 10# In GitHub Actions or GitLab CI
docker compose -f docker-compose.test.yml up --abort-on-container-exit --exit-code-from test
docker compose -f docker-compose.test.yml down -vCommon Mistakes
- Using
depends_onwithout health check conditions — the service crashes because the database is not ready - Hardcoding passwords in docker-compose.yml instead of using
.envor secrets - Not cleaning up volumes after CI test runs (
down -v) — stale data causes flaky tests - Mounting the entire project directory into containers — this exposes
.envand.gitinside the container - Not using
restart: unless-stoppedfor long-running dev services that crash on startup errors
Best Practices
- Commit a
.env.exampleto Git with placeholder values; add.envto.gitignore - Use
service_healthyconditions for all database-dependent services - Create
docker-compose.override.ymlfor dev-specific settings (hot reload volumes, debug ports) - Tag your images with specific versions or commit SHAs, never
latest - Use Compose
profilesto group optional services (monitoring, mail catcher) that are not always needed - Keep the base
docker-compose.ymllean and layer environment differences with override files
Key Takeaways
- Docker Compose defines entire multi-container stacks in one YAML file, enabling reproducible one-command environments
- All services share a default network and resolve each other by service name — no manual IP configuration needed
depends_onwithcondition: service_healthyprevents race conditions; plaindepends_onis not enough for databases.envfiles supply environment variables automatically — never commit secrets to version controldocker compose down -vremoves named volumes; use only when you need a clean database state- Override files (
-f docker-compose.prod.yml) let you layer dev, test, and production configs cleanly - Compose is the standard CI pattern for integration tests: start dependencies, run tests, capture exit code, tear down
- Named volumes persist across container restarts; removing the container does not remove the volume data
Advertisement