Docker Compose

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

Docker Compose

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 Co...