Danh sách bài viết

Bài 35: Docker Compose — quản lý multi-service (API + DB + Vector DB)

Docker Compose v2 cho phép định nghĩa toàn bộ AI stack — FastAPI, Postgres, Redis, Qdrant — trong một file YAML và khởi động cùng lúc với một lệnh. Bài này trình bày cấu trúc compose.yaml, cách service giao tiếp với nhau qua tên container, healthcheck, volume, environment variables, profiles, GPU access, và các pitfall thường gặp.

27/05/2026
2 lượt xem
1

Mục tiêu bài học

Sau bài này bạn sẽ:

  • ✅ Hiểu Docker Compose làm gì và khi nào không dùng được
  • ✅ Viết được compose.yaml cho AI stack (FastAPI + Postgres + Redis + Qdrant)
  • ✅ Cấu hình healthcheck để service phụ thuộc chờ đúng lúc
  • ✅ Quản lý secret qua file .env không commit vào git
  • ✅ Dùng profiles để tách service dev / monitoring
  • ✅ Nhận biết và tránh các pitfall phổ biến
2

Docker Compose là gì và giới hạn của nó

Docker Compose là tool định nghĩa và chạy nhiều container như một đơn vị thống nhất. Thay vì gõ nhiều lệnh docker run riêng lẻ, bạn khai báo tất cả service trong một file YAML rồi khởi động bằng một lệnh:

docker compose up -d

Compose tự tạo network, mount volume, truyền environment variables, và thiết lập thứ tự khởi động theo depends_on.

Use case phù hợp

  • Dev local: chạy toàn bộ dependency (DB, cache, vector DB) giống production mà không cài mỗi thứ riêng trên host.
  • CI/CD testing: spin up stack đầy đủ để chạy integration test, rồi teardown.
  • Small production (single host): một VPS chạy 3–5 service, traffic vừa phải.

Giới hạn — KHÔNG dùng Compose khi

  • Cần scale ngang qua nhiều host — đó là việc của Kubernetes hoặc Docker Swarm.
  • Cần rolling deploy zero-downtime với health-gated rollback.
  • Service count lên tới hàng chục, cần namespace / RBAC / network policy.

Compose v2 — tên lệnh và tên file thay đổi

Compose v1 là binary riêng (docker-compose, viết bằng Python). Từ Docker Desktop 3.6+ và Docker Engine 20.10+, Compose v2 được tích hợp vào CLI Docker dưới dạng plugin:

# Compose v1 (cũ, không nên dùng)
docker-compose up

# Compose v2 (hiện tại)
docker compose up

Tên file chuẩn cũng thay đổi: từ docker-compose.yml sang compose.yaml. File cũ vẫn work, nhưng compose.yaml là tên được ưu tiên theo Compose Specification.

3

Cài đặt và verify

macOS / Windows

Docker Desktop bao gồm Compose v2 sẵn. Cài Docker Desktop là đủ.

Linux

# Ubuntu / Debian — cài compose plugin
sudo apt update
sudo apt install docker-compose-plugin

# Hoặc nếu dùng Docker Engine cài tay, thêm plugin:
# Xem hướng dẫn tại docs.docker.com/compose/install/linux/

Verify

docker compose version
# Docker Compose version v2.27.0

Nếu lệnh trả về lỗi "unknown command", bạn đang dùng Docker Engine không có plugin — cài thêm docker-compose-plugin.

4

Cấu trúc file compose.yaml

Một file compose.yaml tối thiểu gồm ba khối: services, volumes, và (tùy chọn) networks.

services:
  api:
    build: .                          # build từ Dockerfile trong thư mục hiện tại
    ports:
      - "8000:8000"                   # host_port:container_port
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/app
    depends_on:
      - db

  db:
    image: postgres:16                # pull từ Docker Hub
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data   # named volume

volumes:
  pgdata:                             # Docker tạo và quản lý volume này

Các trường quan trọng

Trường Ý nghĩa
build Đường dẫn đến thư mục chứa Dockerfile (hoặc object {context, dockerfile})
image Pull image sẵn từ registry thay vì build
ports Publish port ra host. Không khai báo thì service chỉ accessible trong network nội bộ
environment Biến môi trường truyền vào container
depends_on Thứ tự khởi động; kết hợp condition để chờ healthcheck
volumes Mount volume (named hoặc bind mount)
restart no / always / unless-stopped / on-failure
healthcheck Lệnh kiểm tra service đã ready chưa
5

Ví dụ AI stack đầy đủ — RAG chatbot

Stack dưới đây chạy bốn service: FastAPI app, Qdrant (vector DB), Redis (session cache), và Postgres (metadata storage). Đây là cấu hình thực tế có thể dùng cho dev local và small single-host production.

# compose.yaml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - QDRANT_URL=http://qdrant:6333
      - REDIS_URL=redis://redis:6379/0
      - POSTGRES_URL=postgresql://app:secret@db:5432/ragdb
    depends_on:
      qdrant:
        condition: service_healthy
      redis:
        condition: service_started
      db:
        condition: service_healthy
    restart: unless-stopped

  qdrant:
    image: qdrant/qdrant:v1.11.0
    ports:
      - "6333:6333"     # REST API
      - "6334:6334"     # gRPC
    volumes:
      - qdrant_data:/qdrant/storage
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:6333/healthz"]
      interval: 30s
      timeout: 5s
      retries: 3

  redis:
    image: redis:7-alpine
    command: redis-server --save 60 1 --loglevel warning
    volumes:
      - redis_data:/data
    restart: unless-stopped

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: ragdb
    volumes:
      - pg_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d ragdb"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  qdrant_data:
  redis_data:
  pg_data:

networks:
  default:
    driver: bridge

Giải thích một số điểm

  • qdrant/qdrant:v1.11.0: pin version cụ thể, không dùng latest trong bất kỳ môi trường nào. latest có thể pull image khác nhau trên hai máy, gây hành vi không nhất quán.
  • condition: service_healthy: Compose chờ container báo healthy trước khi start service phụ thuộc. Nếu dùng service_started, container chỉ cần đang chạy — không đảm bảo DB đã accept connection.
  • restart: unless-stopped: tự restart khi container crash, trừ khi bạn dừng thủ công. Thích hợp cho production. Không dùng cho container training — nếu training xong, container exit bình thường, unless-stopped sẽ không restart.
  • ${OPENAI_API_KEY}: giá trị lấy từ shell environment hoặc file .env cùng thư mục. Key không được hardcode vào file YAML.
6

Service-to-service networking

Tất cả service trong cùng một file compose.yaml tự động nằm trong cùng một Docker network. Docker cung cấp DNS nội bộ: tên service chính là hostname mà các service khác dùng để kết nối.

# Trong code FastAPI, kết nối đến Qdrant:
# ĐÚNG — dùng tên service làm hostname
QDRANT_URL = "http://qdrant:6333"

# SAI — localhost là loopback của container API, không phải container Qdrant
QDRANT_URL = "http://localhost:6333"

Cách DNS nội bộ hoạt động

  • Mỗi service có container name mặc định: <project>-<service>-1 (ví dụ: myapp-db-1).
  • Docker Compose thêm DNS alias bằng tên service (không phải container name) vào network.
  • Khi api lookup qdrant, DNS nội bộ resolve thành IP của container qdrant.

Port mapping — khi nào cần, khi nào không

Trường ports publish port ra host machine. Nếu service chỉ cần giao tiếp với các service khác trong cùng stack, không cần ports:

services:
  db:
    image: postgres:16
    # Không khai báo ports — DB chỉ accessible từ các service trong stack
    # Nếu cần kết nối từ host (pgAdmin, psql), thêm:
    # ports:
    #   - "5432:5432"

Hạn chế expose port ra ngoài sẽ giảm attack surface trong production.

7

Lệnh thường dùng

Khởi động và dừng stack

# Start toàn bộ stack (foreground, log hiển thị trực tiếp)
docker compose up

# Start ở background (detached)
docker compose up -d

# Rebuild image trước khi start (dùng khi thay đổi code / Dockerfile)
docker compose up --build -d

# Dừng và xóa container (volume giữ nguyên)
docker compose down

# Dừng + xóa container + xóa volume (mất data — cẩn thận)
docker compose down -v

Quan sát và debug

# Xem log của toàn stack (follow)
docker compose logs -f

# Xem log của một service cụ thể
docker compose logs -f api

# Danh sách service và trạng thái
docker compose ps

# Mở shell vào container đang chạy
docker compose exec api bash

# Chạy lệnh một lần (container tạm, xóa sau khi xong)
docker compose run --rm api python manage.py migrate

Quản lý service đơn lẻ

# Restart một service không restart toàn stack
docker compose restart api

# Dừng một service
docker compose stop db

# Start lại service đã dừng
docker compose start db

# Scale service (Compose không có load balancer tích hợp — scale chủ yếu cho test)
docker compose up -d --scale api=2
8

Volume: 3 loại

1. Named volume

Docker tạo và quản lý ở /var/lib/docker/volumes/. Persist qua các lần docker compose down, chỉ xóa khi dùng -v hoặc docker volume rm.

services:
  db:
    volumes:
      - pg_data:/var/lib/postgresql/data   # named volume

volumes:
  pg_data:    # khai báo ở top-level

Dùng cho: database data, model weights, Qdrant storage — bất kỳ thứ gì cần persist.

2. Bind mount

Mount thư mục từ host vào container. Thay đổi file trên host phản ánh ngay trong container.

services:
  api:
    volumes:
      - ./src:/app/src         # code dev — hot reload
      - ./data:/app/data       # dataset hoặc file cần xử lý

Dùng cho: code khi dev (cùng với hot reload), file config, dataset nhỏ.

Cảnh báo: bind mount . (toàn bộ thư mục project) vào /app sẽ ghi đè site-packages của image, gây ImportError. Chỉ mount thư mục con cụ thể.

3. tmpfs

Mount in-memory, không persist khi container dừng.

services:
  api:
    tmpfs:
      - /tmp
      - /app/cache

Dùng cho: file tạm, cache không cần giữ lại, khi cần I/O nhanh và không quan tâm persistence.

9

Environment variables: 4 cách

Cách 1: Inline trong compose.yaml

services:
  api:
    environment:
      - DEBUG=true
      - LOG_LEVEL=info

Dùng cho: giá trị không nhạy cảm, config cố định.

Cách 2: Reference từ shell

services:
  api:
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}   # lấy từ shell environment
      - PORT=${PORT:-8000}                 # fallback về 8000 nếu không set

Compose đọc biến từ shell process đang chạy lệnh docker compose up.

Cách 3: File .env tự động (khuyến nghị)

Tạo file .env cùng thư mục với compose.yaml. Compose tự load mà không cần khai báo gì thêm:

# .env
OPENAI_API_KEY=sk-...
POSTGRES_PASSWORD=mysecret
QDRANT_API_KEY=
# compose.yaml — Compose tự thay thế ${OPENAI_API_KEY}
services:
  api:
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}

Bắt buộc thêm .env vào .gitignore. Cung cấp .env.example với tên biến nhưng giá trị trống để người khác biết cần set gì.

Cách 4: env_file cho nhiều môi trường

services:
  api:
    env_file:
      - .env.base           # biến chung
      - .env.production     # override cho production

Các file sau override biến trùng tên từ file trước. Dùng khi cần tách config theo môi trường rõ ràng hơn.

10

Profiles — chạy subset service

Profile cho phép nhóm service tùy chọn. Service không có profiles luôn được start. Service có profiles chỉ start khi bạn bật profile tương ứng.

services:
  api:
    build: .
    ports:
      - "8000:8000"
    # không có profiles — luôn start

  db:
    image: postgres:16
    # không có profiles — luôn start

  prometheus:
    image: prom/prometheus:v2.52.0
    profiles: [monitoring]
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana:10.4.2
    profiles: [monitoring]
    ports:
      - "3000:3000"
    depends_on:
      - prometheus
# Dev thông thường — chỉ start api + db
docker compose up -d

# Khi cần xem metrics
docker compose --profile monitoring up -d

# Tắt toàn bộ kể cả monitoring
docker compose --profile monitoring down

Ngoài monitoring, pattern phổ biến: profile debug cho pgAdmin / RedisInsight, profile worker cho Celery worker chỉ cần trong một số test.

11

Override file — tách config theo môi trường

Compose merge nhiều file YAML theo thứ tự chỉ định. Key trùng thì file sau thắng; list (ports, volumes, environment) được nối.

# compose.yaml — base config (dùng cả dev lẫn prod)
# compose.override.yaml — tự merge khi không chỉ định -f (dành cho dev local)
# compose.prod.yaml — dùng tường minh cho production

Ví dụ tách dev / prod:

# compose.yaml (base)
services:
  api:
    build: .
    environment:
      - LOG_LEVEL=info

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ragdb
    volumes:
      - pg_data:/var/lib/postgresql/data

volumes:
  pg_data:
# compose.override.yaml (dev — tự load khi chạy docker compose up)
services:
  api:
    build:
      target: development         # multi-stage target có dev tool
    volumes:
      - ./src:/app/src            # bind mount để hot reload
    environment:
      - LOG_LEVEL=debug
    ports:
      - "8000:8000"

  db:
    ports:
      - "5432:5432"               # expose ra host để dùng pgAdmin
# compose.prod.yaml (production)
services:
  api:
    image: registry.example.com/myapp:${IMAGE_TAG}
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
# Dev (tự merge override)
docker compose up -d

# Production (chỉ dùng base + prod, bỏ qua override)
docker compose -f compose.yaml -f compose.prod.yaml up -d
12

GPU access

Để container truy cập GPU NVIDIA, host cần:

  • NVIDIA driver (driver version ≥ 520 cho CUDA 12.x).
  • NVIDIA Container Toolkit: nvidia-container-toolkit package.

Trong compose.yaml, khai báo GPU qua deploy.resources.reservations.devices:

services:
  train:
    image: pytorch/pytorch:2.3.0-cuda12.1-cudnn8-runtime
    command: python train.py
    volumes:
      - ./:/workspace
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1          # số GPU; dùng "all" để dùng hết
              capabilities: [gpu]
# Verify container thấy GPU
docker compose run --rm train nvidia-smi

Một số lưu ý:

  • count: all không giới hạn GPU — container thấy tất cả GPU trên host. Dùng device_ids để pin GPU cụ thể.
  • Cú pháp deploy.resources chỉ work với docker compose, không phải Docker Swarm deploy theo nghĩa cluster.
  • NVIDIA Container Toolkit phải được cài trước khi chạy lệnh; Compose không tự cài driver.
13

Common pitfalls

1. Dùng localhost thay vì tên service

Lỗi phổ biến nhất. localhost trong container trỏ về loopback của chính container đó, không phải host hay container khác.

# SAI
client = QdrantClient(url="http://localhost:6333")

# ĐÚNG — dùng tên service
client = QdrantClient(url="http://qdrant:6333")

2. depends_on không dùng healthcheck

depends_on: [db] chỉ đảm bảo container db đã start, không đảm bảo Postgres đã accept connection. App có thể fail với connection refused trong vài giây đầu.

# Thiếu — app có thể crash trước khi DB ready
depends_on:
  - db

# Đúng — chờ đến khi DB healthy
depends_on:
  db:
    condition: service_healthy

Kết hợp với healthcheck trên service db (xem bước 5).

3. Bind mount ghi đè site-packages

# SAI — ghi đè toàn bộ /app kể cả site-packages đã cài trong image
volumes:
  - .:/app

# ĐÚNG — chỉ mount thư mục source code
volumes:
  - ./src:/app/src

4. docker compose down -v trong production

Flag -v xóa named volume — mất toàn bộ data database. Chỉ dùng khi cần reset môi trường hoàn toàn (dev, CI). Trong production, không bao giờ chạy down -v mà không backup trước.

5. Secret hardcode trong compose.yaml

# SAI — commit lên git → lộ secret
environment:
  - OPENAI_API_KEY=sk-abc123xyz...

# ĐÚNG — đọc từ .env (không commit)
environment:
  - OPENAI_API_KEY=${OPENAI_API_KEY}

6. Build chậm vì không dùng BuildKit

Docker BuildKit tăng tốc build đáng kể nhờ parallel build, layer caching thông minh. Bật bằng biến môi trường hoặc cấu hình Docker daemon:

DOCKER_BUILDKIT=1 docker compose up --build
# Hoặc đặt trong .env để luôn bật

Docker Desktop 4.x và Docker Engine 23+ bật BuildKit mặc định.

7. Network conflict giữa nhiều stack

Hai stack khác nhau đều dùng default network với cùng subnet có thể xung đột. Đặt tên network rõ ràng:

networks:
  default:
    name: myapp_network
    driver: bridge
14

Tóm tắt

  • compose.yaml là tên file chuẩn của Compose v2; lệnh là docker compose (không có dấu gạch ngang)
  • ✅ Service giao tiếp nhau qua tên service làm hostname — không dùng localhost
  • depends_on với condition: service_healthy đảm bảo DB sẵn sàng trước khi app start
  • ✅ Named volume persist data; bind mount dùng cho code dev; tmpfs cho cache in-memory
  • ✅ Secret qua .env + .gitignore, không hardcode trong YAML
  • ✅ Profiles tách service tùy chọn (monitoring, debug) khỏi service bắt buộc
  • ✅ Override file tách config dev / production mà không duplicate toàn bộ YAML
  • ✅ GPU access qua deploy.resources.reservations.devices + NVIDIA Container Toolkit