
Introduction
Modern applications rarely rely on a single component. You might have a backend API, a frontend client, a database, a cache layer, and a message queue—all needing to run together. Managing each service manually is messy, inconsistent, and time-consuming. New team members spend hours just getting their local environment working.
That’s where Docker Compose comes in. It lets you define and run multi-container applications with a single YAML file, making local development faster, cleaner, and consistent across the entire team. “Works on my machine” becomes a thing of the past when everyone runs identical containerized environments.
In this comprehensive guide, we’ll explore Docker Compose from basic setup to advanced patterns like health checks, profiles, secrets management, and integration with development workflows.
What Is Docker Compose?
Docker Compose is a tool that helps developers manage multi-container Docker applications. Instead of starting each container separately with long docker run commands, you define all services in one file called docker-compose.yml and bring them up together.
Why It’s Useful
- Simplifies multi-service setup: Define once, run anywhere
- Ensures consistency: Same environment for all developers
- One-command operations:
docker compose upstarts everything - Networking out of the box: Services can communicate by name
- Volume management: Persist data between container restarts
- CI/CD integration: Run the same setup in pipelines
Setting Up Docker Compose
Docker Compose comes pre-bundled with Docker Desktop. Verify your installation:
# Check Docker and Compose versions
docker --version
docker compose version
# Output example:
# Docker version 24.0.7, build afdd53b
# Docker Compose version v2.23.3
Creating a Complete Compose File
Here’s a comprehensive example for a typical web application stack:
# docker-compose.yml
version: "3.9"
services:
# Backend API service
backend:
build:
context: ./backend
dockerfile: Dockerfile
target: development # Multi-stage build target
container_name: app_backend
ports:
- "5000:5000"
environment:
- NODE_ENV=development
- DATABASE_URL=postgres://postgres:password@db:5432/app_db
- REDIS_URL=redis://redis:6379
- JWT_SECRET=${JWT_SECRET:-dev_secret_key}
volumes:
# Mount source code for hot reload
- ./backend/src:/app/src:ro
# Named volume for node_modules (prevents local override)
- backend_node_modules:/app/node_modules
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- app_network
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Frontend service
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.dev
container_name: app_frontend
ports:
- "3000:3000"
environment:
- VITE_API_URL=http://localhost:5000
- CHOKIDAR_USEPOLLING=true # For hot reload in Docker
volumes:
- ./frontend/src:/app/src
- ./frontend/public:/app/public
- frontend_node_modules:/app/node_modules
depends_on:
- backend
networks:
- app_network
stdin_open: true # For React dev server
tty: true
# PostgreSQL database
db:
image: postgres:16-alpine
container_name: app_postgres
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: app_db
volumes:
# Persist database data
- postgres_data:/var/lib/postgresql/data
# Initialize database with schema
- ./database/init:/docker-entrypoint-initdb.d:ro
networks:
- app_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
# Redis for caching and sessions
redis:
image: redis:7-alpine
container_name: app_redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
networks:
- app_network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# Adminer for database management (dev only)
adminer:
image: adminer:latest
container_name: app_adminer
ports:
- "8080:8080"
depends_on:
- db
networks:
- app_network
profiles:
- debug # Only starts with --profile debug
# Mailhog for email testing (dev only)
mailhog:
image: mailhog/mailhog:latest
container_name: app_mailhog
ports:
- "1025:1025" # SMTP
- "8025:8025" # Web UI
networks:
- app_network
profiles:
- debug
# Named volumes for data persistence
volumes:
postgres_data:
driver: local
redis_data:
driver: local
backend_node_modules:
frontend_node_modules:
# Custom network for service communication
networks:
app_network:
driver: bridge
Dockerfile Examples for Development
# backend/Dockerfile
# Multi-stage build for development and production
# Base stage with common dependencies
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
# Development stage
FROM base AS development
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
# Production build stage
FROM base AS build
RUN npm ci --only=production
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./
EXPOSE 5000
CMD ["node", "dist/index.js"]
# frontend/Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# Vite dev server
EXPOSE 3000
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
Running Your Services
# Start all services in foreground (see logs)
docker compose up
# Start in detached mode (background)
docker compose up -d
# Start with rebuild (after Dockerfile changes)
docker compose up --build
# Start specific services only
docker compose up backend db
# Start with debug profile (includes Adminer, Mailhog)
docker compose --profile debug up
# Stop all services
docker compose down
# Stop and remove volumes (clean slate)
docker compose down -v
# View logs
docker compose logs -f # All services
docker compose logs -f backend # Specific service
# Scale a service (multiple instances)
docker compose up -d --scale backend=3
Managing Environments
Use environment files to separate configuration from code:
# .env (automatically loaded)
COMPOSE_PROJECT_NAME=myapp
POSTGRES_PASSWORD=secure_password
JWT_SECRET=super_secret_jwt_key
API_PORT=5000
# .env.development
DATABASE_URL=postgres://postgres:password@db:5432/app_dev
LOG_LEVEL=debug
# .env.production
DATABASE_URL=postgres://postgres:${POSTGRES_PASSWORD}@db:5432/app_prod
LOG_LEVEL=info
# docker-compose.override.yml (automatically merged in development)
version: "3.9"
services:
backend:
build:
target: development
volumes:
- ./backend:/app
environment:
- DEBUG=true
command: npm run dev
frontend:
volumes:
- ./frontend:/app
# Run with specific environment file
docker compose --env-file .env.development up
# Run with production override
docker compose -f docker-compose.yml -f docker-compose.prod.yml up
Essential Commands Reference
# Container management
docker compose ps # List containers
docker compose ps -a # Include stopped containers
docker compose top # Show running processes
# Logs and debugging
docker compose logs -f # Follow all logs
docker compose logs -f --tail=100 # Last 100 lines, then follow
docker compose logs --since 1h # Logs from last hour
# Executing commands in containers
docker compose exec backend sh # Open shell in backend
docker compose exec db psql -U postgres # Connect to PostgreSQL
docker compose exec backend npm test # Run tests
# Run one-off commands in new container
docker compose run --rm backend npm run migrate
docker compose run --rm backend npm run seed
# Resource management
docker compose images # List images used
docker compose port backend 5000 # Show port mapping
# Cleanup
docker compose down # Stop and remove containers
docker compose down -v # Also remove volumes
docker compose down --rmi all # Also remove images
docker system prune -a # Clean all unused Docker resources
Development Workflow Integration
# Makefile for common operations
.PHONY: up down logs shell migrate test
up:
docker compose up -d
up-debug:
docker compose --profile debug up -d
down:
docker compose down
clean:
docker compose down -v --rmi local
logs:
docker compose logs -f
shell-backend:
docker compose exec backend sh
shell-db:
docker compose exec db psql -U postgres -d app_db
migrate:
docker compose exec backend npm run migrate
seed:
docker compose exec backend npm run seed
test:
docker compose exec backend npm test
lint:
docker compose exec backend npm run lint
rebuild:
docker compose up -d --build
restart:
docker compose restart $(service)
Health Checks and Dependencies
# Proper service dependency with health checks
services:
backend:
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s # Give app time to start
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d app_db"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
Networking Patterns
# Services communicate via container names
# backend can reach db at: postgres://db:5432
# frontend can reach backend at: http://backend:5000
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- backend
- frontend
networks:
- app_network
# nginx.conf
# upstream backend {
# server backend:5000;
# }
# upstream frontend {
# server frontend:3000;
# }
Common Mistakes to Avoid
Not using named volumes for node_modules: Mounting the entire project directory overwrites node_modules installed in the container. Use a named volume to preserve it.
Ignoring health checks: Using depends_on without conditions only waits for containers to start, not for services to be ready. Always use health checks.
Storing secrets in docker-compose.yml: Never commit sensitive data. Use .env files (and add them to .gitignore) or Docker secrets.
Not cleaning up resources: Orphan volumes and images accumulate over time. Run docker system prune periodically.
Using latest tags: Always specify version tags for images to ensure reproducible builds.
Running as root in containers: Create non-root users in your Dockerfiles for security.
Conclusion
Docker Compose simplifies local development by allowing developers to orchestrate multiple services with one configuration file. It’s perfect for replicating production setups locally without complex scripts or manual steps. Once you master Compose, managing microservices, databases, caches, and supporting services becomes effortless.
Start with a basic setup for your stack, add health checks and proper dependency handling, then gradually incorporate profiles for optional services and environment-specific overrides. The investment in proper containerization pays dividends in consistent environments and faster onboarding.
To learn how to deploy these containers at scale, check out Kubernetes 101: Deploying and Managing Containerised Apps. For detailed documentation, visit the Docker Compose documentation.