Docker Compose is one of the most powerful and useful Docker tools. Let's dive in!
Part 1: What is Docker Compose?
The Problem Docker Compose Solves
Imagine you built a multi-container application:
Your Application needs:
├── Web server (Nginx)
├── API server (Python Flask)
├── Database (PostgreSQL)
├── Cache (Redis)
└── Message Queue (RabbitMQ)
5 containers to manage!
Without Docker Compose:
# Create networks
docker network create frontend
docker network create backend
# Start database
docker run -d \
--name postgres \
--network backend \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=myapp \
-v postgres-data:/var/lib/postgresql/data \
postgres:15
# Start Redis
docker run -d \
--name redis \
--network backend \
redis:7
# Start API
docker run -d \
--name api \
--network backend \
-e DATABASE_URL=postgresql://postgres:secret@postgres:5432/myapp \
-e REDIS_URL=redis://redis:6379 \
my-api
docker network connect frontend api
# Start Web
docker run -d \
--name web \
--network frontend \
-p 80:80 \
my-web
# That's a LOT of commands! 😰
# And you need to remember all of them!
# Starting, stopping, updating... nightmare!
Docker Compose Solution
With Docker Compose, ONE file describes everything:
# docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:15
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:7
api:
build: ./api
environment:
DATABASE_URL: postgresql://postgres:secret@postgres:5432/myapp
REDIS_URL: redis://redis:6379
depends_on:
- postgres
- redis
web:
build: ./web
ports:
- "80:80"
depends_on:
- api
volumes:
postgres-data:
Now just run:
docker-compose up
That's it! All 5 containers started with proper configuration! ✓
What is Docker Compose?
Simple Definition:
Docker Compose = Tool for defining and running
multi-container Docker applications
Key features:
├── Define everything in YAML file
├── Single command to start/stop all containers
├── Automatic network creation
├── Volume management
├── Service dependencies
└── Easy scaling
Think of it as:
Recipe Book (docker-compose.yml):
├── Lists all ingredients (services)
├── Preparation steps (configuration)
├── Cooking order (dependencies)
└── Final presentation (ports, networks)
One command to cook the entire meal! 🍽️
Part 2: Installing Docker Compose
Checking if Docker Compose is Installed
Docker Desktop includes Docker Compose!
docker-compose --version
Output:
Docker Compose version v2.24.5
✓ Already installed with Docker Desktop!
Docker Compose v1 vs v2
Two versions exist:
Docker Compose v1:
├── Separate tool
├── Command: docker-compose (with hyphen)
└── Older version
Docker Compose v2:
├── Integrated into Docker CLI
├── Command: docker compose (space, no hyphen)
└── Newer, faster version
Both work, but v2 is recommended:
# v1 syntax (old)
docker-compose up
# v2 syntax (new, recommended)
docker compose up
For this tutorial, we'll use v2 syntax (docker compose), but v1 also works!
Part 3: Docker Compose File Basics
Creating Your First docker-compose.yml
Docker Compose uses YAML format.
YAML Basics (Quick!):
# Comments start with #
# Key-value pairs
name: value
# Nested structure (indentation matters!)
parent:
child: value
another_child: value
# Lists
items:
- item1
- item2
- item3
# Multi-line strings
description: |
This is a
multi-line
string
⚠️ Important: YAML is VERY sensitive to indentation! Use spaces, not tabs!
Basic docker-compose.yml Structure
version: '3.8' # Compose file version
services: # Define containers
service1:
# Configuration for service1
service2:
# Configuration for service2
volumes: # Define volumes (optional)
volume1:
networks: # Define networks (optional)
network1:
Example 1: Single Service (Nginx)
docker-compose.yml:
version: '3.8'
services:
web:
image: nginx:alpine
ports:
- "8080:80"
That's it! Now run:
docker compose up
Output:
[+] Running 1/1
✔ Container project-web-1 Started
Attaching to web-1
web-1 | /docker-entrypoint.sh: Configuration complete
web-1 | nginx: [notice] starting nginx...
Open browser: http://localhost:8080
✓ Nginx running!
To stop:
# Press Ctrl+C
# Or in another terminal:
docker compose down
Understanding Service Names
In docker-compose.yml:
services:
web: # ← This is the service name
Docker Compose creates container with name:
project-web-1
↑ ↑ ↑
│ │ └── Instance number
│ └── Service name
└── Project name (directory name)
Part 4: Service Configuration Options
Common Service Options
Let's explore all important options:
1. image - Use Existing Image
services:
db:
image: postgres:15
# Uses official PostgreSQL image from Docker Hub
2. build - Build from Dockerfile
services:
api:
build: ./api
# Builds from Dockerfile in ./api directory
Or with more options:
services:
api:
build:
context: ./api # Directory with Dockerfile
dockerfile: Dockerfile.prod # Custom Dockerfile name
args: # Build arguments
VERSION: 1.0
3. ports - Port Mapping
services:
web:
image: nginx
ports:
- "8080:80" # Host:Container
- "8443:443"
Format:
ports:
- "HOST_PORT:CONTAINER_PORT"
4. environment - Environment Variables
services:
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
POSTGRES_USER: admin
Or from file:
services:
api:
image: my-api
env_file:
- .env # Load from .env file
.env file:
DATABASE_URL=postgresql://localhost/mydb
API_KEY=abc123
DEBUG=true
5. volumes - Data Persistence
Named volume:
services:
db:
image: postgres:15
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data: # Define volume
Bind mount:
services:
web:
image: nginx
volumes:
- ./html:/usr/share/nginx/html # Host:Container
Multiple volumes:
services:
app:
image: my-app
volumes:
- app-data:/data # Named volume
- ./config:/app/config # Bind mount
- ./logs:/app/logs # Another bind mount
6. depends_on - Service Dependencies
services:
web:
image: nginx
depends_on:
- api # Start api before web
api:
image: my-api
depends_on:
- db # Start db before api
db:
image: postgres
Start order: db → api → web
⚠️ Note: depends_on only waits for container to START, not for it to be READY!
7. networks - Custom Networks
services:
web:
image: nginx
networks:
- frontend
api:
image: my-api
networks:
- frontend
- backend
db:
image: postgres
networks:
- backend
networks:
frontend:
backend:
8. restart - Restart Policy
services:
api:
image: my-api
restart: always
# Options: no, always, on-failure, unless-stopped
Options:
no = Never restart
always = Always restart (even after reboot)
on-failure = Restart only if exit code != 0
unless-stopped = Always restart unless manually stopped
9. command - Override Default Command
services:
db:
image: postgres
command: postgres -c max_connections=200
# Overrides default command
10. container_name - Custom Container Name
services:
db:
image: postgres
container_name: my-postgres-db
# Instead of default: project-db-1
Part 5: Complete Example - Web Application
Building a Full Application
Let's create: Web Frontend + API Backend + PostgreSQL Database
Project Structure
my-app/
├── docker-compose.yml
├── web/
│ ├── Dockerfile
│ └── index.html
├── api/
│ ├── Dockerfile
│ ├── app.py
│ └── requirements.txt
└── .env
Step 1: Create Project Directory
mkdir my-app
cd my-app
mkdir web api
Step 2: Create API
api/app.py:
from flask import Flask, jsonify
import psycopg2
import os
import time
app = Flask(__name__)
def get_db():
# Wait for database to be ready
max_retries = 30
for i in range(max_retries):
try:
conn = psycopg2.connect(
host=os.getenv('DB_HOST', 'db'),
database=os.getenv('DB_NAME', 'myapp'),
user=os.getenv('DB_USER', 'postgres'),
password=os.getenv('DB_PASSWORD', 'secret')
)
return conn
except psycopg2.OperationalError:
if i < max_retries - 1:
time.sleep(1)
else:
raise
@app.route('/api/status')
def status():
return jsonify({
'status': 'ok',
'message': 'API is running!'
})
@app.route('/api/db-check')
def db_check():
try:
db = get_db()
cursor = db.cursor()
cursor.execute('SELECT version()')
version = cursor.fetchone()[0]
db.close()
return jsonify({
'status': 'ok',
'database': version
})
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
api/requirements.txt:
flask==3.0.0
psycopg2-binary==2.9.9
api/Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
Step 3: Create Web Frontend
web/index.html:
<!DOCTYPE html>
<html>
<head>
<title>My Docker Compose App</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #0056b3;
}
#result {
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
white-space: pre-wrap;
}
</style>
</head>
<body>
<div class="container">
<h1>🐳 Docker Compose Demo App</h1>
<p>This demonstrates a multi-container application with Docker Compose!</p>
<div>
<button onclick="checkAPI()">Check API Status</button>
<button onclick="checkDB()">Check Database</button>
</div>
<div id="result"></div>
</div>
<script>
async function checkAPI() {
const result = document.getElementById('result');
result.textContent = 'Loading...';
try {
const response = await fetch('/api/status');
const data = await response.json();
result.textContent = JSON.stringify(data, null, 2);
} catch (error) {
result.textContent = 'Error: ' + error.message;
}
}
async function checkDB() {
const result = document.getElementById('result');
result.textContent = 'Loading...';
try {
const response = await fetch('/api/db-check');
const data = await response.json();
result.textContent = JSON.stringify(data, null, 2);
} catch (error) {
result.textContent = 'Error: ' + error.message;
}
}
</script>
</body>
</html>
web/nginx.conf:
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
}
location /api/ {
proxy_pass http://api:5000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
web/Dockerfile:
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf
Step 4: Create docker-compose.yml
docker-compose.yml:
version: '3.8'
services:
# PostgreSQL Database
db:
image: postgres:15-alpine
container_name: myapp-postgres
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- backend
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
# API Backend
api:
build: ./api
container_name: myapp-api
environment:
DB_HOST: db
DB_NAME: myapp
DB_USER: postgres
DB_PASSWORD: secret
depends_on:
db:
condition: service_healthy
networks:
- frontend
- backend
restart: unless-stopped
# Web Frontend
web:
build: ./web
container_name: myapp-web
ports:
- "8080:80"
depends_on:
- api
networks:
- frontend
restart: unless-stopped
volumes:
postgres-data:
networks:
frontend:
backend:
Step 5: Run the Application
Start everything:
docker compose up
Or run in background:
docker compose up -d
Output:
[+] Running 5/5
✔ Network myapp_frontend Created
✔ Network myapp_backend Created
✔ Volume "myapp_postgres-data" Created
✔ Container myapp-postgres Started
✔ Container myapp-api Started
✔ Container myapp-web Started
Open browser: http://localhost:8080
Click buttons to test! ✓
Part 6: Docker Compose Commands
Essential Commands
Start services:
# Start in foreground (see logs)
docker compose up
# Start in background (detached)
docker compose up -d
# Rebuild images and start
docker compose up --build
# Start specific service
docker compose up web
Stop services:
# Stop (keeps containers)
docker compose stop
# Stop and remove containers
docker compose down
# Stop, remove containers, volumes, and networks
docker compose down -v
# Remove everything including images
docker compose down --rmi all
View logs:
# All services
docker compose logs
# Follow logs (real-time)
docker compose logs -f
# Specific service
docker compose logs api
# Last 100 lines
docker compose logs --tail=100
List services:
docker compose ps
Output:
NAME IMAGE STATUS PORTS
myapp-web myapp-web Up 0.0.0.0:8080->80/tcp
myapp-api myapp-api Up
myapp-postgres postgres:15 Up
Execute commands in service:
# Open shell in service
docker compose exec api bash
# Run command
docker compose exec db psql -U postgres
# Run as different user
docker compose exec -u root api bash
View service configuration:
docker compose config
Shows resolved configuration with all variables substituted.
Restart services:
# Restart all
docker compose restart
# Restart specific service
docker compose restart api
Scale services:
# Run 3 instances of api
docker compose up -d --scale api=3
Build images:
# Build all images
docker compose build
# Build specific service
docker compose build api
# Build without cache
docker compose build --no-cache
Pull images:
# Pull all images
docker compose pull
# Pull specific service
docker compose pull db
Part 7: Environment Variables and .env Files
Using .env File
Create .env file:
# .env
DB_NAME=myapp
DB_USER=postgres
DB_PASSWORD=supersecret
API_PORT=5000
WEB_PORT=8080
docker-compose.yml:
version: '3.8'
services:
db:
image: postgres:15
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
api:
build: ./api
environment:
DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
ports:
- "${API_PORT}:5000"
web:
build: ./web
ports:
- "${WEB_PORT}:80"
Variables automatically loaded from .env! ✓
Multiple Environment Files
# Use different env file
docker compose --env-file .env.production up
# Override with another file
docker compose --env-file .env.local up
Passing Environment Variables
# From command line
DB_PASSWORD=newsecret docker compose up
# System environment variables
export DB_PASSWORD=newsecret
docker compose up
Part 8: Profiles (Conditional Services)
What are Profiles?
Run different sets of services for different scenarios.
Example:
version: '3.8'
services:
# Always run
web:
image: nginx
ports:
- "80:80"
api:
image: my-api
depends_on:
- db
db:
image: postgres
# Only for development
adminer:
image: adminer
profiles:
- dev
ports:
- "8080:8080"
# Only for debugging
debug-tools:
image: nicolaka/netshoot
profiles:
- debug
command: sleep infinity
Usage:
# Start only core services
docker compose up
# Start with dev profile (includes adminer)
docker compose --profile dev up
# Start with debug profile
docker compose --profile debug up
# Start with multiple profiles
docker compose --profile dev --profile debug up
Part 9: Healthchecks
Adding Healthchecks
Healthcheck = Test if service is actually ready
services:
db:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s # Check every 10 seconds
timeout: 5s # Fail if takes > 5 seconds
retries: 3 # Try 3 times before giving up
start_period: 30s # Grace period on startup
api:
build: ./api
depends_on:
db:
condition: service_healthy # Wait for db to be healthy!
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
Benefits:
Without healthcheck:
├── depends_on waits for container to start
├── But container might not be ready yet!
└── API tries to connect to DB → Fails! ✗
With healthcheck:
├── depends_on waits for service to be HEALTHY
├── Container started AND ready
└── API connects successfully ✓
Part 10: Advanced Example - Full Stack Application
Complete Real-World Example
docker-compose.yml:
version: '3.8'
services:
# Nginx Reverse Proxy
nginx:
image: nginx:alpine
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
- web
- api
networks:
- frontend
restart: always
# Frontend (React)
web:
build:
context: ./frontend
args:
NODE_ENV: production
container_name: react-app
environment:
- REACT_APP_API_URL=http://localhost/api
networks:
- frontend
restart: always
# Backend API (Node.js)
api:
build: ./backend
container_name: nodejs-api
environment:
NODE_ENV: production
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: ${DB_NAME}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
REDIS_HOST: redis
REDIS_PORT: 6379
JWT_SECRET: ${JWT_SECRET}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
networks:
- frontend
- backend
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: postgres-db
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
- ./database/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- backend
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
# Redis Cache
redis:
image: redis:7-alpine
container_name: redis-cache
command: redis-server --appendonly yes
volumes:
- redis-data:/data
networks:
- backend
restart: always
# Database Admin (Development only)
adminer:
image: adminer
container_name: db-admin
ports:
- "8080:8080"
depends_on:
- postgres
networks:
- backend
profiles:
- dev
restart: unless-stopped
volumes:
postgres-data:
driver: local
redis-data:
driver: local
networks:
frontend:
driver: bridge
backend:
driver: bridge
.env:
# Database
DB_NAME=myapp
DB_USER=appuser
DB_PASSWORD=strongpassword123
# JWT
JWT_SECRET=your-secret-key-change-in-production
# Node
NODE_ENV=production
Usage:
# Production (no adminer)
docker compose up -d
# Development (with adminer)
docker compose --profile dev up -d
# View logs
docker compose logs -f
# Stop
docker compose down
Part 11: Docker Compose Best Practices
1. Use Specific Image Tags
Bad:
services:
db:
image: postgres # Latest version, unpredictable!
Good:
services:
db:
image: postgres:15-alpine # Specific version
2. Use .env for Sensitive Data
Bad:
services:
db:
environment:
POSTGRES_PASSWORD: hardcoded-password # Never do this!
Good:
services:
db:
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD} # From .env file
Add .env to .gitignore!
3. Use Named Volumes
Bad:
volumes:
- ./data:/var/lib/postgresql/data # Bind mount
Good:
volumes:
- postgres-data:/var/lib/postgresql/data # Named volume
volumes:
postgres-data:
4. Add Healthchecks
services:
api:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
5. Use Restart Policies
services:
web:
restart: unless-stopped # Auto-restart on failure
6. Separate Networks
networks:
frontend: # Public-facing services
backend: # Internal services (database, cache)
7. Order Services Properly
services:
db: # Database first
api: # API depends on db
depends_on:
- db
web: # Web depends on api
depends_on:
- api
Summary
What We Learned:
✅ What Docker Compose is and why it's useful
✅ docker-compose.yml file structure
✅ Service configuration options
✅ Building multi-container applications
✅ Docker Compose commands
✅ Environment variables and .env files
✅ Profiles for different scenarios
✅ Healthchecks
✅ Networks and volumes in Compose
✅ Real-world examples
✅ Best practices
Key Takeaways:
1. Docker Compose = Multi-container management tool
2. One YAML file describes entire application
3. Single command to start/stop everything
4. Automatic networking between services
5. Perfect for development and simple deployments
6. Use service names for container communication
7. Always use .env for sensitive data
8. Add healthchecks for reliable startups
Common Commands:
docker compose up -d # Start in background
docker compose down # Stop and remove
docker compose logs -f # Follow logs
docker compose ps # List services
docker compose exec api bash # Access service shell
docker compose build # Rebuild images
docker compose restart # Restart services
🎉 Excellent! You now know Docker Compose!
You can now:
- Manage multi-container applications easily
- Define entire stacks in one file
- Use Docker Compose for development
- Deploy simple production applications
No comments:
Post a Comment