Docker Compose — Practice Projects — Exercises

4 minute read

Five hands-on challenges that test your ability to design and build multi-container applications from scratch.

Exercise 1 — Monitoring Stack

Task: Create a compose.yml that runs Prometheus, Grafana, and a Node Exporter for host metrics collection.

Requirements:

  • Prometheus scrapes Node Exporter every 15 seconds
  • Grafana connects to Prometheus as a data source
  • Grafana is accessible on port 3000
  • Prometheus is accessible on port 9090
  • All data persists in named volumes
Show Solution
services:
  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - prom-data:/prometheus
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
    networks:
      - monitor-net

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    volumes:
      - grafana-data:/var/lib/grafana
    environment:
      GF_SECURITY_ADMIN_PASSWORD: admin
    depends_on:
      - prometheus
    networks:
      - monitor-net

  node-exporter:
    image: prom/node-exporter:latest
    expose:
      - "9100"
    networks:
      - monitor-net

volumes:
  prom-data:
  grafana-data:

networks:
  monitor-net:
    driver: bridge

You also need prometheus.yml:

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'node-exporter'
    static_configs:
      - targets: ['node-exporter:9100']

Exercise 2 — GitLab Runner Environment

Task: Create a Compose setup that runs a mock CI environment with a test runner service, a database for tests, and a Redis for test caching.

Requirements:

  • Test runner builds from a local Dockerfile
  • Database starts with health check
  • Redis starts with health check
  • Test runner depends on both being healthy
  • Use an .env file for all credentials
Show Solution

.env:

DB_NAME=testdb
DB_USER=testuser
DB_PASS=testpass
REDIS_URL=redis://redis:6379
services:
  test-runner:
    build: ./runner
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      DATABASE_URL: postgres://${DB_USER}:${DB_PASS}@db:5432/${DB_NAME}
      REDIS_URL: ${REDIS_URL}
    networks:
      - ci-net

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASS}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - ci-net

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - ci-net

networks:
  ci-net:
    driver: bridge

Exercise 3 — Multi-Language Microservices

Task: Create a Compose file that runs three microservices: a Python API, a Node.js API, and a Go API, all behind an Nginx reverse proxy.

Requirements:

  • Each service exposes different endpoints
  • Nginx routes based on URL path (/python/, /node/, /go/)
  • All services share a common network
  • Nginx is the only service with a published port
Show Solution
services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - python-api
      - node-api
      - go-api
    networks:
      - micro-net

  python-api:
    build: ./python-service
    expose:
      - "8000"
    networks:
      - micro-net

  node-api:
    build: ./node-service
    expose:
      - "3000"
    networks:
      - micro-net

  go-api:
    build: ./go-service
    expose:
      - "8080"
    networks:
      - micro-net

networks:
  micro-net:
    driver: bridge

nginx.conf:

server {
    listen 80;

    location /python/ {
        proxy_pass http://python-api:8000/;
    }
    location /node/ {
        proxy_pass http://node-api:3000/;
    }
    location /go/ {
        proxy_pass http://go-api:8080/;
    }
}

Exercise 4 — Database Replication

Task: Set up a PostgreSQL primary-replica pair using Compose.

Requirements:

  • Primary accepts read/write connections
  • Replica streams from primary
  • Both have health checks
  • Primary data persists in a named volume
Show Solution
services:
  primary:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret
      POSTGRES_INITDB_ARGS: "--wal-segsize=64"
    command: >
      postgres
      -c wal_level=replica
      -c max_wal_senders=3
      -c hot_standby=on
    volumes:
      - primary-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U admin"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - db-net

  replica:
    image: postgres:16-alpine
    depends_on:
      primary:
        condition: service_healthy
    environment:
      PGUSER: admin
      PGPASSWORD: secret
    command: >
      bash -c "
        rm -rf /var/lib/postgresql/data/* &&
        pg_basebackup -h primary -D /var/lib/postgresql/data -U admin -Fp -Xs -P &&
        echo 'primary_conninfo = host=primary port=5432 user=admin password=secret' >> /var/lib/postgresql/data/postgresql.auto.conf &&
        touch /var/lib/postgresql/data/standby.signal &&
        postgres
      "
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U admin"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 30s
    networks:
      - db-net

volumes:
  primary-data:

networks:
  db-net:
    driver: bridge

Exercise 5 — Complete SaaS Template

Task: Design a compose.yml for a SaaS application with: frontend, API, worker, database, cache, queue, and object storage.

Requirements:

  • Frontend builds from ./frontend
  • API builds from ./api
  • Worker builds from ./worker and shares API’s environment
  • PostgreSQL for persistence
  • Redis for caching
  • RabbitMQ for message queue
  • MinIO for object storage
  • Use health checks on all infrastructure services
  • Use profiles: monitoring for Grafana, debug for Adminer
Show Solution
services:
  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    depends_on:
      - api
    networks:
      - app-net

  api:
    build: ./api
    ports:
      - "8000:8000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
    environment:
      DATABASE_URL: postgres://appuser:apppass@postgres:5432/appdb
      REDIS_URL: redis://redis:6379
      AMQP_URL: amqp://guest:guest@rabbitmq:5672
      S3_ENDPOINT: http://minio:9000
    networks:
      - app-net

  worker:
    build: ./worker
    depends_on:
      rabbitmq:
        condition: service_healthy
    environment:
      DATABASE_URL: postgres://appuser:apppass@postgres:5432/appdb
      REDIS_URL: redis://redis:6379
      AMQP_URL: amqp://guest:guest@rabbitmq:5672
    networks:
      - app-net

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: apppass
    volumes:
      - pg-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - app-net

  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - app-net

  rabbitmq:
    image: rabbitmq:3-management-alpine
    ports:
      - "15672:15672"
    volumes:
      - rabbit-data:/var/lib/rabbitmq
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "check_running"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-net

  minio:
    image: minio/minio:latest
    command: server /data --console-address ":9001"
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - minio-data:/data
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-net

  grafana:
    image: grafana/grafana:latest
    profiles:
      - monitoring
    ports:
      - "3001:3000"
    networks:
      - app-net

  adminer:
    image: adminer:latest
    profiles:
      - debug
    ports:
      - "8080:8080"
    networks:
      - app-net

volumes:
  pg-data:
  redis-data:
  rabbit-data:
  minio-data:

networks:
  app-net:
    driver: bridge