🐳 Week 4: Containers & Docker

Day 4: Docker Compose

⏱ 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