Skip to content

Module 3: Orchestration with Docker Compose (Hợp tấu Container)

🎓 Instructor Profile

Kỹ sư Raizo (Phó CTO HPN) - Chuyên gia kiến trúc hệ thống, người tin rằng "Một container lẻ loi là một container buồn". Cùng Giáo sư Tom giải mã sự kỳ diệu của việc phối hợp dịch vụ.

Chào mừng các bạn đến với thế giới của Orchestration. Nếu Dockerfile là cách bạn tạo ra một "ngôi sao" (Container), thì Docker Compose chính là người nhạc trưởng (Conductor) điều khiển cả dàn nhạc giao hưởng.


🎻 Phần 1: From Solo to Symphony (Từ Đơn lẻ đến Hợp xướng)

Nỗi đau của Developer (The Pain Point)

Hãy tưởng tượng bạn đang phát triển một Web App hiện đại. Bạn cần bật:

  1. Frontend (React/Vue)
  2. Backend (Node/Go/Python)
  3. Database (PostgreSQL)
  4. Cache (Redis)
  5. Worker (Xử lý tác vụ nền)

Bạn phải mở 5 tab Terminal, gõ 5 lệnh docker run dài ngoằng nhớ nhớ quên quên. Sai một tham số là hệ thống "gãy".

Giải pháp: Infrastructure as Code (IaC)

docker-compose.yml ra đời. Nó là bản tuyên ngôn của hệ thống (Manifest). Chỉ với một file duy nhấtmột lệnh duy nhất, cả thế giới của bạn sẽ được dựng lên.

"Define and run multi-container Docker applications."


📐 Phần 2: The YAML Blueprint (Bản vẽ kỹ thuật)

Docker Compose sử dụng định dạng YAML (YAML Ain't Markup Language). Cấu trúc của nó rất trong sáng, dễ đọc.

💡 Compose V2 vs V1

Hiện tại, Docker đã chuyển sang Compose V2 (được viết lại bằng Go, tích hợp thẳng vào Docker CLI).

  • V1 (Cũ): docker-compose up (có gạch nối).
  • V2 (Mới): docker compose up (dùng dấu cách). PENALGO dạy chuẩn V2.

Cấu trúc giải phẫu

yaml
# Phiên bản schema (thường dùng 3.8 hoặc mới nhất)
version: "3.8"

services:
  # --- Dịch vụ 1: Web App ---
  web:
    build: .             # Build từ Dockerfile ở thư mục hiện tại
    ports:
      - "8000:5000"      # Mapping port Host:Container
    depends_on:
      - db               # Chỉ chạy sau khi 'db' khởi động

  # --- Dịch vụ 2: Database ---
  db:
    image: postgres:15-alpine
    volumes:
      - db_data:/var/lib/postgresql/data  # 📦 Persist data
    environment:
      POSTGRES_PASSWORD: ${DB_PASS}       # 🛡️ Dùng biến môi trường

volumes:
  db_data:               # Khai báo volume dùng chung

networks:
  app_network:           # 🔗 Kết nối mạng (Tuỳ chọn, Docker tự tạo default network)

🌐 Phần 3: Networking & Service Discovery (Ma thuật kết nối)

Tư duy Architect: Làm thế nào Frontend tìm thấy Backend, hay Backend tìm thấy Database khi IP của container thay đổi liên tục mỗi khi khởi động lại?

Docker Compose giải quyết bằng Internal DNS. Trong cùng một mạng Compose (default network), các dịch vụ gọi nhau bằng Service Name.

  • Để kết nối tới Database Postgres, Backend chỉ cần dùng host: db (Tên service khai báo trong YAML).
  • Connection String: postgres://user:pass@db:5432/dbname (Tuyệt đối không dùng IP như 172.18.0.2).

3.1 Production Example: Full-Stack Application

Ví dụ thực tế một stack production với 4 services — Nginx reverse proxy, Node.js API, PostgreSQL, và Redis:

yaml
version: "3.8"

services:
  # --- Reverse Proxy ---
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      api:
        condition: service_healthy
    networks:
      - frontend
    restart: unless-stopped

  # --- API Server ---
  api:
    build:
      context: .
      dockerfile: Dockerfile
      target: runner
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://${DB_USER}:${DB_PASS}@db:5432/${DB_NAME}
      - REDIS_URL=redis://:${REDIS_PASS}@redis:6379
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 10s
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - frontend
      - backend
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "0.5"

  # --- Database ---
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASS}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - pg_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - backend
    restart: unless-stopped

  # --- Cache ---
  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASS} --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASS}", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - backend
    restart: unless-stopped

volumes:
  pg_data:
  redis_data:

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # Backend network không truy cập được từ bên ngoài

💡 Defense in Depth: Network backendinternal: true — DB và Redis hoàn toàn không thể truy cập từ ngoài Docker network. Chỉ service api (thuộc cả 2 networks) mới kết nối được.

3.2 Network Topology Diagram

API Server nằm trên cả hai networks: nhận request từ Nginx qua frontend, và kết nối DB/Redis qua backend. DB và Redis hoàn toàn cô lập khỏi thế giới bên ngoài.


🔐 Phần 4: Environment & Secrets (Quản lý bí mật)

☠️ SECURITY ALERT

Tuyệt đối KHÔNG BAO GIỜ hardcode mật khẩu, API Key trực tiếp trong file docker-compose.yml và commit lên Git. Đây là lỗi sơ đẳng nhất khiến server bị hack.

Best Practice: Sử dụng .env

Docker Compose tự động đọc file .env đặt cùng thư mục.

  1. Tạo file .env:

    ini
    DB_USER=admin
    DB_PASS=SieuMatKhau123!@#
  2. Sử dụng trong YAML:

    yaml
    environment:
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASS}
  3. Thêm vào .gitignore:

    text
    .env

🚦 Phần 5: The Startup Order Problem (Vấn đề thứ tự khởi động)

Đây là kiến thức Deep Tech quan trọng.

Vấn đề: Bạn dùng depends_on: - db cho Web App. Docker đảm bảo container db được BẬT lên trước web. Nhưng: Docker không biết khi nào Database bên trong sẵn sàng nhận kết nối (Ready). Postgres có thể mất 10s để khởi động tiến trình. Web App chạy lên ngay -> Kết nối DB -> Lỗi "Connection Refused" -> Crash. 💥

Giải pháp: Healthchecks (Khám sức khỏe)

Sử dụng healthcheck kết hợp condition: service_healthy.

yaml
services:
  # Database có cơ chế tự khám bệnh
  db:
    image: postgres:15-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U admin"] # Lệnh kiểm tra
      interval: 5s
      timeout: 5s
      retries: 5
  
  # Web App kiên nhẫn đợi bác sĩ bảo "Khỏe" mới chạy
  web:
    build: .
    depends_on:
      db:
        condition: service_healthy  # 🟢 CHÌA KHÓA CỦA VẤN ĐỀ

🛠️ Phần 6: Essential Commands (Bộ công cụ chỉ huy)

LệnhTác dụngChú thích
docker compose up -dDựng toàn bộ stack và chạy ngầm (Detached).Lệnh dùng nhiều nhất.
docker compose downTắt và xóa containers, networks.Giữ lại Volumes (Dữ liệu an toàn).
docker compose down -vTắt và XÓA SẠCH cả Volumes.⚠️ Cẩn thận: Mất dữ liệu vĩnh viễn. Chỉ dùng khi muốn reset sạch sẽ.
docker compose logs -f [service]Xem log thời gian thực của dịch vụ cụ thể.VD: docker compose logs -f web
docker compose exec [service] [cmd]Chui vào container đang chạy để debug.VD: docker compose exec db psql -U admin

🏆 Challenge: The Resilient Stack

Nhiệm vụ: Viết file docker-compose.yml dựng hệ thống sau:

  1. Redis Service:
    • Image: redis:alpine
    • Có mật khẩu (Lấy từ biến môi trường ${REDIS_PASS}).
    • Có Healthcheck (Dùng lệnh redis-cli ping).
  2. Web Service:
    • Image: nginx:alpine
    • Port: 8080:80
    • Yêu cầu: Chỉ được phép khởi động SAU KHI Redis báo trạng thái "Healthy".

Gợi ý lệnh test sức khỏe Redis: redis-cli -a $REDIS_PASSWORD ping | grep PONG


⚠️ Production Pitfalls (Bẫy thực chiến)

⚠️ Cạm bẫy

Vấn đề: Map port database/redis ra host: ports: - "5432:5432" hoặc ports: - "6379:6379".

Hậu quả: Database/Cache bị expose ra internet hoặc mạng nội bộ. Bất kỳ ai có network access đều có thể kết nối trực tiếp. Đã có vô số vụ hack Redis/MongoDB vì lỗi này.

Giải pháp:

  • Chỉ expose port cho service cần public (Nginx/Load Balancer)
  • Dùng internal network cho DB/Cache (xem Production Example ở trên)
  • Debug bằng docker compose exec thay vì mở port ra ngoài

⚠️ Cạm bẫy

Vấn đề: Dùng depends_on: - db và nghĩ rằng Database đã ready khi app khởi động.

Hậu quả: depends_on chỉ đảm bảo container được tạo trước, KHÔNG đảm bảo service bên trong đã sẵn sàng. PostgreSQL cần 5-15 giây để initialize. App kết nối ngay → "Connection Refused" → crash loop.

Giải pháp: Luôn dùng healthcheck + condition: service_healthy (Xem Phần 5). Đây là bắt buộc cho production.

⚠️ Cạm bẫy

Vấn đề: Không cấu hình healthcheck → Docker không biết service có thực sự hoạt động không.

Hậu quả:

  • Container status running nhưng app bên trong đã crash/hang → không ai biết
  • depends_on với condition: service_healthy không thể sử dụng
  • Orchestrator (Swarm/K8s) không thể tự restart service lỗi

Giải pháp: Mỗi service phải có healthcheck phù hợp:

yaml
# Web App
healthcheck:
  test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
# PostgreSQL
healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres"]
# Redis
healthcheck:
  test: ["CMD", "redis-cli", "ping"]

📝 Quiz: Kiểm tra kiến thức

🧠 Quiz

Câu 1: Trong Docker Compose, các service giao tiếp với nhau bằng cách nào?

  • [ ] A. Thông qua IP address cố định được gán khi container khởi động
  • [x] B. Thông qua Service Name nhờ Internal DNS của Docker network
  • [ ] C. Thông qua shared volume giữa các container
  • [ ] D. Thông qua environment variables truyền IP cho nhau

💡 Giải thích: Docker Compose tạo Internal DNS tự động. Các service gọi nhau bằng tên service (VD: db, redis, api). Tuyệt đối không dùng IP address vì IP thay đổi mỗi khi restart container.

🧠 Quiz

Câu 2: Sự khác biệt chính giữa docker compose downdocker compose down -v là gì?

  • [ ] A. -v hiển thị verbose output
  • [ ] B. -v xóa thêm images đã build
  • [x] C. -v xóa thêm named volumes — mất dữ liệu persist vĩnh viễn
  • [ ] D. Không có sự khác biệt nào

💡 Giải thích: docker compose down chỉ xóa containers và networks, giữ lại volumes (dữ liệu DB an toàn). Thêm -v sẽ xóa cả volumes — toàn bộ dữ liệu PostgreSQL, Redis biến mất vĩnh viễn. Chỉ dùng khi muốn reset hoàn toàn.

🧠 Quiz

Câu 3: Tại sao nên dùng internal: true cho backend network?

  • [ ] A. Để tăng tốc độ truyền dữ liệu giữa containers
  • [ ] B. Để giảm memory usage của Docker engine
  • [x] C. Để ngăn services trên network đó bị truy cập từ bên ngoài và không truy cập ra internet
  • [ ] D. Để Docker tự động encrypt traffic giữa containers

💡 Giải thích: internal: true tạo network cô lập — containers trên network này không giao tiếp ra ngoài và không bị truy cập từ bên ngoài. Đây là defense in depth: DB và Cache chỉ nói chuyện được với API server, không ai khác.


Docker Compose Production Checklist

✅ Checklist triển khai

Checklist trước khi dùng Compose cho staging/production:

  • [ ] Tất cả secrets dùng biến môi trường từ .env (không hardcode)
  • [ ] .env đã thêm vào .gitignore
  • [ ] Mỗi service có healthcheck phù hợp
  • [ ] depends_on dùng condition: service_healthy
  • [ ] Chỉ expose port cần thiết ra host (Nginx/LB)
  • [ ] Database và Cache trên internal network
  • [ ] Volumes cho persistent data (DB, uploads)
  • [ ] restart: unless-stopped cho tất cả services
  • [ ] Resource limits (deploy.resources.limits) cho mỗi service
  • [ ] Đã test docker compose down && docker compose up -d — stack khởi động thành công

Chúc các bạn thành công trong việc điều phối dàn nhạc container của mình! 🎼