⏱ Duration: 5 Hours
📚 Learning Objectives
- Understand Docker Compose and its benefits
- Write docker-compose.yml files
- Deploy multi-container applications
- Manage services with compose commands
- Use environment variables and secrets
📖 Core Concepts (2 Hours)
What is Docker Compose?
Docker Compose is a tool for defining and running multi-container applications. One YAML file replaces multiple docker run commands.
Without Compose:
docker network create myapp
docker run -d --name db --network myapp -e POSTGRES_PASSWORD=secret postgres
docker run -d --name api --network myapp -e DB_HOST=db -p 3000:3000 myapi
docker run -d --name web --network myapp -p 80:80 nginx
With Compose (docker-compose.yml):
version: '3.8'
services:
db:
image: postgres
environment:
POSTGRES_PASSWORD: secret
api:
build: ./api
environment:
DB_HOST: db
ports:
- "3000:3000"
web:
image: nginx
ports:
- "80:80"
# One command: docker compose up
docker-compose.yml Structure
version: '3.8' # Compose file version
services: # Container definitions
service_name:
image: image:tag # Use existing image
# OR
build: ./path # Build from Dockerfile
ports:
- "host:container"
environment:
- VAR=value
volumes:
- ./host:/container
- named_volume:/path
depends_on:
- other_service
networks:
- custom_network
volumes: # Named volumes
named_volume:
networks: # Custom networks
custom_network:
Essential Compose Options
services:
webapp:
# Image options
image: nginx:alpine
build:
context: ./app
dockerfile: Dockerfile.prod
args:
- VERSION=1.0
# Container options
container_name: my-webapp
restart: always # no, on-failure, unless-stopped, always
# Networking
ports:
- "80:80"
- "443:443"
expose:
- "3000" # Internal only
# Environment
environment:
NODE_ENV: production
API_KEY: ${API_KEY} # From .env file
env_file:
- .env
- .env.local
# Storage
volumes:
- ./src:/app/src # Bind mount
- node_modules:/app/node_modules # Named volume
# Dependencies
depends_on:
- db
- redis
# Resource limits
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
# Health check
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"]
interval: 30s
timeout: 10s
retries: 3
Compose Commands
# Start services
docker compose up # Foreground
docker compose up -d # Detached (background)
docker compose up --build # Rebuild images
# Stop services
docker compose down # Stop and remove containers
docker compose down -v # Also remove volumes
docker compose down --rmi all # Also remove images
# Manage services
docker compose start # Start existing containers
docker compose stop # Stop without removing
docker compose restart # Restart all services
docker compose pause/unpause
# View status
docker compose ps # List containers
docker compose logs # View logs
docker compose logs -f service # Follow specific service
docker compose top # Running processes
# Execute commands
docker compose exec service bash # Shell into service
docker compose run service cmd # Run one-off command
# Scaling (for stateless services)
docker compose up -d --scale web=3
Environment Variables
# .env file (auto-loaded)
POSTGRES_USER=admin
POSTGRES_PASSWORD=secretpassword
API_PORT=3000
# docker-compose.yml
services:
db:
image: postgres
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
api:
build: ./api
ports:
- "${API_PORT}:3000"
env_file:
- .env
- .env.production
# Override with shell environment
export POSTGRES_PASSWORD=newsecret
docker compose up
🔬 Hands-on Lab (2.5 Hours)
Lab 1: Simple Multi-Container App
- Create a web app with Redis backend
- Define services in docker-compose.yml
- Use service names for networking
# Lab 1: Flask + Redis counter app
mkdir compose-lab && cd compose-lab
# app.py
cat > app.py << 'EOF'
from flask import Flask
import redis
import os
app = Flask(__name__)
cache = redis.Redis(host=os.environ.get('REDIS_HOST', 'redis'), port=6379)
@app.route('/')
def hello():
count = cache.incr('hits')
return f'Hello! This page has been viewed {count} times.\n'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
EOF
# requirements.txt
cat > requirements.txt << 'EOF'
flask==3.0.0
redis==5.0.0
EOF
# Dockerfile
cat > Dockerfile << 'EOF'
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
EOF
# docker-compose.yml
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
web:
build: .
ports:
- "5000:5000"
environment:
- REDIS_HOST=redis
depends_on:
- redis
redis:
image: redis:alpine
EOF
# Start the application
docker compose up -d
# Test it
curl http://localhost:5000
curl http://localhost:5000
curl http://localhost:5000
# View logs
docker compose logs
# Check status
docker compose ps
# Stop and clean up
docker compose down
Lab 2: Full Stack Application
- Node.js API + PostgreSQL + Adminer
- Use named volumes for data persistence
- Configure health checks
# Lab 2: Full stack with database
mkdir fullstack-app && cd fullstack-app
# Create API directory
mkdir api && cd api
# api/package.json
cat > package.json << 'EOF'
{
"name": "api",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"express": "^4.18.2",
"pg": "^8.11.0"
}
}
EOF
# api/index.js
cat > index.js << 'EOF'
const express = require('express');
const { Pool } = require('pg');
const app = express();
const pool = new Pool({
host: process.env.DB_HOST || 'db',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
database: process.env.DB_NAME || 'myapp'
});
app.get('/', async (req, res) => {
try {
const result = await pool.query('SELECT NOW()');
res.json({ status: 'ok', time: result.rows[0].now });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.get('/health', (req, res) => res.json({ status: 'healthy' }));
app.listen(3000, () => console.log('API running on port 3000'));
EOF
# api/Dockerfile
cat > Dockerfile << 'EOF'
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
EOF
cd ..
# docker-compose.yml
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
api:
build: ./api
ports:
- "3000:3000"
environment:
DB_HOST: db
DB_USER: postgres
DB_PASSWORD: ${DB_PASSWORD:-postgres}
DB_NAME: myapp
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
adminer:
image: adminer
ports:
- "8080:8080"
depends_on:
- db
volumes:
postgres_data:
EOF
# Start everything
docker compose up -d
# Wait for services
sleep 15
# Test API
curl http://localhost:3000
# Adminer available at http://localhost:8080
# Server: db, User: postgres, Password: postgres, Database: myapp
# View all logs
docker compose logs
# Check health status
docker compose ps
# Cleanup
docker compose down -v
cd .. && rm -rf fullstack-app
Lab 3: Development Environment
- Use bind mounts for live reloading
- Override settings for development
- Use docker-compose.override.yml
# Lab 3: Dev environment with live reload
mkdir dev-env && cd dev-env
# Simple Node app
cat > package.json << 'EOF'
{
"name": "dev-app",
"scripts": { "start": "node index.js", "dev": "nodemon index.js" },
"dependencies": { "express": "^4.18.2" },
"devDependencies": { "nodemon": "^3.0.0" }
}
EOF
cat > index.js << 'EOF'
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Hello Dev!'));
app.listen(3000, () => console.log('Running on 3000'));
EOF
# docker-compose.yml (production)
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
app:
image: node:18-alpine
working_dir: /app
command: npm start
ports:
- "3000:3000"
EOF
# docker-compose.override.yml (auto-loaded for dev)
cat > docker-compose.override.yml << 'EOF'
version: '3.8'
services:
app:
command: sh -c "npm install && npm run dev"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
EOF
# Start dev environment (override auto-applies)
docker compose up
# In another terminal, edit index.js
# Changes reflect immediately!
# For production (ignore override)
docker compose -f docker-compose.yml up
# Cleanup
docker compose down
cd .. && rm -rf dev-env
✅ Day 4 Checklist
- Can write docker-compose.yml files
- Understand services, volumes, networks
- Can deploy multi-container apps
- Know docker compose up/down/logs
- Can use environment variables and .env files
- Understand depends_on and health checks