Learn Docker Compose from A to Z: concepts, docker-compose.yml structure, essential directives, and multi-container deployment. Includes real-world examples and best practices.

No table of contents available for this article
Imagine you're developing a complete web application with:
Frontend: React running on port 3000
Backend: Node.js API running on port 5000
Database: PostgreSQL running on port 5432
Cache: Redis running on port 6379
Reverse Proxy: Nginx running on port 80
To run this application with plain Docker, you would need to:
bash
# Create network
docker network create my-app-network
# Run each container
docker run -d --name postgres --network my-app-network -e POSTGRES_PASSWORD=secret postgres:15
docker run -d --name redis --network my-app-network redis:7-alpine
docker run -d --name backend --network my-app-network -e DB_HOST=postgres my-backend:latest
docker run -d --name frontend --network my-app-network my-frontend:latest
docker run -d --name nginx --network my-app-network -p 80:80 my-nginx:latestProblems:
Must remember and type many long commands
Difficult to manage startup order (database must run before backend)
Hard to share configuration with the team
No easy way to start/stop the entire application
Docker Compose was created to solve all these problems.
Docker Compose is a tool that allows you to define and run multi-container Docker applications. Instead of running individual docker run commands, you define the entire application in a single YAML file (docker-compose.yml), then use just one command to start everything.
yaml
# docker-compose.yml
services:
frontend:
image: my-frontend:latest
ports:
- "3000:3000"
backend:
image: my-backend:latest
environment:
- DB_HOST=postgres
depends_on:
- postgres
postgres:
image: postgres:15
environment:
- POSTGRES_PASSWORD=secretbash
# Start the entire application
docker compose up -d
# Stop the entire application
docker compose downOne command replaces dozens of commands!
Instead of remembering dozens of Docker commands, you only need one configuration file and a few simple commands.
The docker-compose.yml file is code, which can be:
Stored in Git alongside source code
Reviewed via Pull Requests
Track change history
Rollback when needed
Everyone on the team runs the same docker compose up command and gets an identical environment.
Onboarding a new team member? Clone the repo and run docker compose up. Done.
Docker Compose automatically handles startup order based on depends_on.
Docker Compose is already integrated. No additional installation needed.
Method 1: Install via Docker Engine (recommended)
bash
# Docker Compose V2 is integrated into Docker CLI
sudo apt-get update
sudo apt-get install docker-compose-plugin
# Verify
docker compose versionMethod 2: Install standalone
bash
# Download binary
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# Make executable
sudo chmod +x /usr/local/bin/docker-compose
# Verify
docker-compose --versionNote on versions:
V1 (legacy): Command is docker-compose (with hyphen)
V2 (current): Command is docker compose (without hyphen)
This article uses V2 syntax.
The docker-compose.yml file has this basic structure:
yaml
# Version (not required from Compose V2)
version: "3.9"
# Define services (containers)
services:
service-name:
# Service configuration
...
# Define networks (optional)
networks:
network-name:
# Network configuration
...
# Define volumes (optional)
volumes:
volume-name:
# Volume configuration
...
# Define configs (optional)
configs:
config-name:
# Config configuration
...
# Define secrets (optional)
secrets:
secret-name:
# Secret configuration
...Services is where you define the containers to run. Each service corresponds to one container.
yaml
services:
database:
image: postgres:15-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: admin
POSTGRES_PASSWORD: secretyaml
services:
backend:
build:
context: ./backend # Directory containing Dockerfile
dockerfile: Dockerfile # Dockerfile name (default is Dockerfile)
args: # Build arguments
NODE_ENV: production
image: my-backend:latest # Name for the built imageShorthand syntax:
yaml
services:
backend:
build: ./backend # If Dockerfile is at ./backend/Dockerfileyaml
services:
web:
image: nginx:1.25-alpineyaml
services:
app:
build:
context: .
dockerfile: Dockerfile.prod
args:
- BUILD_ENV=production
target: production # Multi-stage build targetyaml
services:
web:
container_name: my-web-app # Instead of auto-generated nameyaml
services:
web:
ports:
- "80:80" # HOST:CONTAINER
- "443:443"
- "3000-3005:3000-3005" # Range of ports
- "127.0.0.1:8080:80" # Bind to localhost onlyyaml
services:
backend:
expose:
- "5000" # Only other services in the same network can accessyaml
services:
app:
environment:
- NODE_ENV=production
- DB_HOST=postgres
- DB_PORT=5432
# Or object format
environment:
NODE_ENV: production
DB_HOST: postgresyaml
services:
app:
env_file:
- .env
- .env.local# .env
NODE_ENV=production
DB_HOST=postgres
DB_PASSWORD=supersecretyaml
services:
app:
volumes:
# Named volume
- db-data:/var/lib/postgresql/data
# Bind mount (host path : container path)
- ./src:/app/src
# Read-only bind mount
- ./config:/app/config:ro
# Anonymous volume
- /app/node_modules
volumes:
db-data: # Declare named volumeyaml
services:
frontend:
networks:
- frontend-network
backend:
networks:
- frontend-network
- backend-network
database:
networks:
- backend-network
networks:
frontend-network:
backend-network:yaml
services:
backend:
depends_on:
- postgres
- redis
postgres:
image: postgres:15
redis:
image: redis:7Note: depends_on only ensures containers start in order, not that the service inside is ready. To check service readiness, use healthcheck:
yaml
services:
backend:
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5yaml
services:
web:
restart: "no" # Don't restart (default)
restart: always # Always restart
restart: on-failure # Restart when exit code is non-zero
restart: unless-stopped # Restart unless manually stoppedyaml
services:
app:
image: node:18
command: npm run dev
# Or array format
command: ["npm", "run", "dev"]yaml
services:
app:
entrypoint: /app/docker-entrypoint.shyaml
services:
app:
working_dir: /appyaml
services:
app:
user: "1000:1000" # UID:GIDyaml
services:
web:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40syaml
services:
web:
deploy:
replicas: 3
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
cpus: "0.25"
memory: 256Myaml
services:
app:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"By default, Docker Compose creates a network for all services in the file. Services can call each other by service name.
yaml
services:
backend:
image: my-backend
# Can call database using hostname "postgres"
postgres:
image: postgres:15yaml
services:
frontend:
networks:
- public
backend:
networks:
- public
- private
database:
networks:
- private
networks:
public:
driver: bridge
private:
driver: bridge
internal: true # No internet accessyaml
networks:
existing-network:
external: true
name: my-existing-networkyaml
services:
database:
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:
driver: localyaml
services:
app:
volumes:
- ./src:/app/src # Development: sync code
- ./logs:/app/logs # Share logs with hostyaml
volumes:
shared-data:
external: true
name: my-shared-volumebash
# Start all services
docker compose up
# Run in background (detached mode)
docker compose up -d
# Start only specific services
docker compose up -d backend postgres
# Rebuild images before starting
docker compose up -d --build
# Force recreate containers
docker compose up -d --force-recreatebash
# Stop all services
docker compose stop
# Stop and remove containers, networks
docker compose down
# Also remove volumes
docker compose down -v
# Also remove images
docker compose down --rmi allbash
# List running containers
docker compose ps
# List all containers (including stopped)
docker compose ps -abash
# Logs for all services
docker compose logs
# Logs for specific service
docker compose logs backend
# Follow logs (real-time)
docker compose logs -f
# Limit number of lines
docker compose logs --tail=100bash
# Run command in running container
docker compose exec backend bash
docker compose exec postgres psql -U postgres
# Run command in new container
docker compose run --rm backend npm testbash
# Build all services
docker compose build
# Build specific service
docker compose build backend
# Build without cache
docker compose build --no-cachebash
# View merged configuration
docker compose config
# Pull latest images
docker compose pull
# Restart services
docker compose restart
# Scale services (run multiple instances)
docker compose up -d --scale backend=3yaml
services:
app:
image: myapp:${TAG:-latest} # Default is "latest"
environment:
- DB_HOST=${DB_HOST}
- DB_PASSWORD=${DB_PASSWORD:?DB_PASSWORD is required} # RequiredDocker Compose automatically reads the .env file in the same directory:
# .env
TAG=v1.2.3
DB_HOST=postgres
DB_PASSWORD=supersecretbash
# Override configuration
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -dyaml
# docker-compose.yml (base)
services:
app:
build: .
environment:
- NODE_ENV=development
# docker-compose.prod.yml (override)
services:
app:
environment:
- NODE_ENV=production
restart: alwaysyaml
version: "3.9"
services:
# Frontend - React
frontend:
build: ./frontend
ports:
- "3000:3000"
volumes:
- ./frontend/src:/app/src # Hot reload
environment:
- REACT_APP_API_URL=http://localhost:5000
depends_on:
- backend
# Backend - Node.js/Express
backend:
build: ./backend
ports:
- "5000:5000"
volumes:
- ./backend/src:/app/src
environment:
- NODE_ENV=development
- MONGO_URI=mongodb://mongo:27017/myapp
- REDIS_URL=redis://redis:6379
depends_on:
- mongo
- redis
# Database - MongoDB
mongo:
image: mongo:7
volumes:
- mongo-data:/data/db
environment:
- MONGO_INITDB_ROOT_USERNAME=admin
- MONGO_INITDB_ROOT_PASSWORD=secret
# Cache - Redis
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
# Admin UI - Mongo Express
mongo-express:
image: mongo-express
ports:
- "8081:8081"
environment:
- ME_CONFIG_MONGODB_ADMINUSERNAME=admin
- ME_CONFIG_MONGODB_ADMINPASSWORD=secret
- ME_CONFIG_MONGODB_URL=mongodb://admin:secret@mongo:27017/
depends_on:
- mongo
volumes:
mongo-data:
redis-data:yaml
version: "3.9"
services:
# Nginx
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./src:/var/www/html:ro
depends_on:
- php
# PHP-FPM
php:
build:
context: ./docker/php
dockerfile: Dockerfile
volumes:
- ./src:/var/www/html
environment:
- DB_CONNECTION=mysql
- DB_HOST=mysql
- DB_PORT=3306
- DB_DATABASE=laravel
- DB_USERNAME=laravel
- DB_PASSWORD=secret
depends_on:
mysql:
condition: service_healthy
# MySQL
mysql:
image: mysql:8.0
volumes:
- mysql-data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=rootsecret
- MYSQL_DATABASE=laravel
- MYSQL_USER=laravel
- MYSQL_PASSWORD=secret
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
# Redis
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
# Queue Worker
queue:
build:
context: ./docker/php
volumes:
- ./src:/var/www/html
command: php artisan queue:work
depends_on:
- php
- redis
volumes:
mysql-data:
redis-data:yaml
version: "3.9"
services:
# API Gateway - Nginx
gateway:
image: nginx:1.25-alpine
ports:
- "80:80"
volumes:
- ./gateway/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- user-service
- product-service
- order-service
# User Service
user-service:
build: ./services/user
expose:
- "3001"
environment:
- DATABASE_URL=postgresql://postgres:secret@user-db:5432/users
depends_on:
- user-db
networks:
- frontend
- user-network
user-db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=users
- POSTGRES_PASSWORD=secret
volumes:
- user-db-data:/var/lib/postgresql/data
networks:
- user-network
# Product Service
product-service:
build: ./services/product
expose:
- "3002"
environment:
- MONGO_URI=mongodb://product-db:27017/products
depends_on:
- product-db
networks:
- frontend
- product-network
product-db:
image: mongo:7
volumes:
- product-db-data:/data/db
networks:
- product-network
# Order Service
order-service:
build: ./services/order
expose:
- "3003"
environment:
- DATABASE_URL=postgresql://postgres:secret@order-db:5432/orders
- USER_SERVICE_URL=http://user-service:3001
- PRODUCT_SERVICE_URL=http://product-service:3002
depends_on:
- order-db
- user-service
- product-service
networks:
- frontend
- order-network
order-db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=orders
- POSTGRES_PASSWORD=secret
volumes:
- order-db-data:/var/lib/postgresql/data
networks:
- order-network
# Message Queue - RabbitMQ
rabbitmq:
image: rabbitmq:3-management-alpine
ports:
- "15672:15672" # Management UI
environment:
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=secret
networks:
- frontend
networks:
frontend:
user-network:
product-network:
order-network:
volumes:
user-db-data:
product-db-data:
order-db-data:Profiles allow you to define service groups and run them only when needed:
yaml
services:
frontend:
image: my-frontend
profiles:
- frontend
backend:
image: my-backend
# No profile = always runs
debug-tools:
image: debug-tools
profiles:
- debug
test-runner:
image: test-runner
profiles:
- testbash
# Only run services without profiles
docker compose up -d
# Run with frontend profile
docker compose --profile frontend up -d
# Run multiple profiles
docker compose --profile frontend --profile debug up -dyaml
# ❌ Not recommended - hardcoded password
services:
db:
environment:
- POSTGRES_PASSWORD=mysecretpassword
# ✅ Recommended - use environment variables
services:
db:
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}# .env (don't commit to git)
DB_PASSWORD=mysecretpasswordyaml
# ✅ Meaningful names
volumes:
postgres-data:
redis-cache:
app-uploads:
networks:
frontend-network:
backend-network:yaml
services:
app:
depends_on:
db:
condition: service_healthy
db:
healthcheck:
test: ["CMD", "pg_isready"]
interval: 5s
timeout: 5s
retries: 5├── docker-compose.yml # Base configuration
├── docker-compose.override.yml # Development (auto-loaded)
├── docker-compose.prod.yml # Production
└── docker-compose.test.yml # Testingbash
# Development (automatically loads override)
docker compose up -d
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -dyaml
services:
app:
deploy:
resources:
limits:
cpus: "1"
memory: 1G
reservations:
cpus: "0.5"
memory: 512Myaml
services:
app:
restart: unless-stopped
db:
restart: alwaysyaml
services:
app:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"Ensure you have a .dockerignore file to avoid copying unnecessary files into the image.
Error: Bind for 0.0.0.0:3000 failed: port is already allocatedSolution:
bash
# Find process using the port
lsof -i :3000
# Kill the process or change port in docker-compose.ymlPermission denied: '/var/lib/postgresql/data'Solution:
yaml
services:
db:
user: "1000:1000" # Or
volumes:
- ./data:/var/lib/postgresql/databash
# Or fix permissions on host
sudo chown -R 1000:1000 ./dataSolution:
bash
# Check logs for the cause
docker compose logs service-name
# Check exit code
docker compose ps -aSolution:
yaml
# Ensure same network
services:
app:
networks:
- backend
db:
networks:
- backend
networks:
backend:bash
# Use service name as hostname
# app connects to db using hostname "db", not "localhost"bash
# Rebuild and recreate
docker compose up -d --build --force-recreate
# Or down then up again
docker compose down
docker compose up -d --buildDocker Compose is an essential tool when working with multi-container applications. It helps:
Simplify management of multiple containers
Standardize development environment for the entire team
Automate dependency and networking setup
Document infrastructure as code
Start with simple examples, then gradually apply advanced features like profiles, healthchecks, and multi-file configuration to build professional development and production environments.
Docker Compose Overview: https://docs.docker.com/compose/
Compose File Reference: https://docs.docker.com/compose/compose-file/
Compose CLI Reference: https://docs.docker.com/compose/reference/
Networking in Compose: https://docs.docker.com/compose/networking/
Environment Variables in Compose: https://docs.docker.com/compose/environment-variables/