Contents
Why Docker Compose
Real applications have multiple services: backend, frontend, database, Redis, task queue. Starting each one manually with docker run is tedious. Compose describes all the services in one file and brings them up with a single command. (Basics are in Docker for beginners, images in the Dockerfile article.)
docker-compose.yml structure
version: '3.9'
services: # List of services (containers)
app:
build: .
ports:
- "3000:3000"
networks: # Custom networks (optional)
backend:
volumes: # Named volumes for persistent data
pgdata:
Full example: app + PostgreSQL + Redis
version: '3.9'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
NODE_ENV: production
DATABASE_URL: postgres://user:secret@db:5432/myapp
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy # Wait until db is ready
cache:
condition: service_started
networks:
- backend
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
cache:
image: redis:7-alpine
volumes:
- redisdata:/data
networks:
- backend
networks:
backend:
driver: bridge
volumes:
pgdata:
redisdata:
Environment variables via .env
Don't hard-code secrets in docker-compose.yml. Use a .env file (and don't forget to add it to .gitignore):
# .env
POSTGRES_USER=user
POSTGRES_PASSWORD=supersecret
POSTGRES_DB=myapp
APP_PORT=3000
# docker-compose.yml
services:
db:
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
app:
ports:
- "${APP_PORT}:3000"
⚠️ Add
.envto.gitignore! For CI/CD use a.env.examplewith placeholders.
Health checks — wait for the service to be ready
Without depends_on + condition the backend starts before the database is up, gets a connection error and crashes.
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s # Check every 10 seconds
timeout: 5s # Per-check timeout
retries: 5 # After 5 failures — unhealthy
start_period: 30s # Don't count failures during the first 30 seconds
Profiles — different configs for dev and prod
services:
app:
build: .
# No profile — always starts
pgadmin:
image: dpage/pgadmin4
profiles:
- dev # Dev mode only
ports:
- "5050:80"
nginx:
image: nginx:alpine
profiles:
- prod # Production only
# Start only the dev profile
docker-compose --profile dev up -d
# Start the prod profile
docker-compose --profile prod up -d
Common commands
# Start everything in the background
docker-compose up -d
# Tail logs from all services
docker-compose logs -f
# Logs of a specific service
docker-compose logs -f app
# Run a command inside a running container
docker-compose exec app sh
# Stop without removing data
docker-compose stop
# Stop and remove containers (volumes data stays)
docker-compose down
# Remove everything including volumes (CAREFUL — wipes the DB!)
docker-compose down -v
💡 For local dev, create a
docker-compose.override.yml— it's auto-merged with the main file and lets you mount code for hot-reload without touching the main compose file.
Discussion
No comments yet. Be the first.