Docker Compose: Quản Lý Ứng Dụng Đa Container Mà Không Phát Điên

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

Ngày Mọi Thứ Sụp Đổ Trước Buổi Stand-Up

Hãy thử tưởng tượng: team bạn vừa onboard một developer mới. Họ clone repo, làm theo README, và ba mươi phút sau vẫn đang vật lộn với port conflict, biến môi trường bị thiếu, và một database không chịu khởi động đúng thứ tự. Stand-up còn năm phút nữa. Mọi team đều gặp phải điều này sớm hay muộn.

Đó là câu chuyện của chúng tôi, khoảng hai năm trước. Chúng tôi có một Node.js API, một PostgreSQL database, một Redis cache, và một Nginx reverse proxy — bốn container, tất cả được quản lý bằng một đống lệnh docker run riêng lẻ nhồi vào một shell script mà chẳng ai muốn đụng vào.

Vấn đề không phải là các container. Vấn đề là orchestration. Khiến chúng khởi động đúng thứ tự, giao tiếp với nhau trên đúng network, và giữ nhất quán giữa môi trường local dev và staging — đó là nơi mọi thứ tan vỡ.

Tại Sao Các Lệnh docker run Riêng Lẻ Không Ổn

Khi bạn quản lý từng container một, về cơ bản bạn đang giải quyết cùng một vấn đề bằng băng keo mỗi lần:

  • Thứ tự khởi động: Container API của bạn bị crash vì database chưa sẵn sàng.
  • Networking: Các container không thể tìm thấy nhau trừ khi bạn tự tay tạo và gắn network.
  • Environment drift: Dev dùng port và credentials khác với staging, và chẳng ai ghi lại.
  • Teardown: Dọn dẹp có nghĩa là phải tự tay tìm container ID, volume và network.

Bạn có thể viết một bash script. Một số team làm vậy. Chúng trông gọn gàng được khoảng một tuần — rồi ai đó thêm một service, và đột nhiên xuất hiện đủ loại điều kiện kiểm tra xem network đã tồn tại chưa, xử lý lỗi cho các trường hợp thất bại một phần, các flag mà chẳng ai còn nhớ. Script thoái hóa theo thời gian.

docker run được thiết kế cho single container. Ứng dụng đa container cần một công cụ khác.

Docker Compose Thực Sự Là Gì (Và Không Phải Là Gì)

Docker Compose cho phép bạn định nghĩa toàn bộ ứng dụng đa container trong một file YAML duy nhất, rồi quản lý mọi thứ bằng các lệnh đơn giản. Networking, thứ tự khởi động, volume mount, biến môi trường — tất cả đều declarative, tất cả ở một chỗ.

Nó không phải là thay thế cho Kubernetes. Chạy hàng trăm service trên nhiều node với yêu cầu auto-scaling? Bạn cần thứ gì đó mạnh hơn. Nhưng với local development, production deployment quy mô nhỏ, và môi trường CI, Compose đáp ứng đúng mức độ phức tạp cần thiết.

Từ Docker Desktop v3.3, Compose V2 được tích hợp trực tiếp vào Docker. Bạn gọi nó là docker compose (không có dấu gạch nối), dù CLI cũ docker-compose vẫn hoạt động nếu bạn đã cài riêng.

Xây Dựng compose.yaml Thực Tế

Đây là cấu hình chúng tôi sử dụng cho một web app stack điển hình: một API service, một PostgreSQL database, và một Redis cache. Đây gần với những gì chúng tôi chạy trong production, với credentials được đơn giản hóa cho ví dụ.

services:
  api:
    build: ./api
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/appdb
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    restart: unless-stopped

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

  cache:
    image: redis:7-alpine
    volumes:
      - redisdata:/data

volumes:
  pgdata:
  redisdata:

Ba quyết định trong file này đáng để hiểu trước khi bạn copy một cách mù quáng.

Kết Hợp depends_on + healthcheck

Chỉ dùng depends_on: db thôi chỉ chờ container khởi động — không phải chờ PostgreSQL thực sự sẵn sàng nhận kết nối. Thêm healthcheck và set condition: service_healthy, và container API sẽ không khởi động cho đến khi Postgres vượt qua health probe. Chúng tôi đã mất hàng giờ vì startup race condition. Kết hợp này loại bỏ chúng hoàn toàn.

Named Volume vs. Bind Mount

Named volume (pgdata, redisdata) được quản lý bởi Docker và tồn tại qua các lần restart container. Database luôn nên dùng named volume. Bind mount — ánh xạ một thư mục local như ./data:/var/lib/postgresql/data — hữu ích khi bạn cần truy cập file trực tiếp trong quá trình phát triển, nhưng chúng gây ra vấn đề về permission trên Linux và không nên dùng cho production database.

Service Discovery qua DNS

DATABASE_URL của API dùng db làm hostname. Compose tự động tạo một network cho project của bạn và đăng ký từng service theo tên — vì vậy các container tìm thấy nhau bằng tên service, không cần cấu hình network thủ công.

Các Lệnh Hàng Ngày Bạn Thực Sự Sẽ Dùng

# Khởi động mọi thứ ở chế độ detached
docker compose up -d

# Khởi động và build lại image (sau khi thay đổi code)
docker compose up -d --build

# Kiểm tra những gì đang chạy
docker compose ps

# Theo dõi log của một service cụ thể
docker compose logs -f api

# Chạy lệnh một lần trong container của service
docker compose exec db psql -U app -d appdb

# Dừng mọi thứ (giữ nguyên volume)
docker compose down

# Tùy chọn hủy diệt: xóa container, network VÀ volume
docker compose down -v

Lệnh cuối cùng đó — down -v — xóa toàn bộ dữ liệu database của bạn. Hữu ích khi bạn muốn bắt đầu hoàn toàn mới trong quá trình phát triển, nhưng cực kỳ nguy hiểm nếu chạy mà không suy nghĩ trong môi trường sai.

Quản Lý Cấu Hình Theo Môi Trường

Sự khác biệt về môi trường là một trong những nguyên nhân phổ biến nhất gây ra xung đột giữa dev, staging và production. Cách tiếp cận gọn nhất là kết hợp một compose.yaml cơ sở với các file override:

# Development (tự động dùng compose.yaml + compose.override.yaml)
docker compose up -d

# Staging hoặc production (chọn file cụ thể)
docker compose -f compose.yaml -f compose.prod.yaml up -d

File compose.override.yaml cho môi trường development có thể bật hot-reloading và expose debug port:

services:
  api:
    volumes:
      - ./api/src:/app/src
    environment:
      NODE_ENV: development
      DEBUG: "*"

File override production giữ mọi thứ được kiểm soát — không volume mount, không debug flag, có resource limit đầy đủ.

Secret có file riêng của nó. Đặt một file .env trong cùng thư mục với compose.yaml và Compose sẽ tự động nhận nó:

# .env
POSTGRES_PASSWORD=actualstrongpassword
API_SECRET_KEY=yoursecretkey
# compose.yaml
services:
  db:
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

Commit một file .env.example với giá trị placeholder vào repo, thêm .env vào .gitignore. Cách đó giải quyết bài toán “credentials ở đâu” mà không để lộ chúng vào version control.

Những Gì Chúng Tôi Thực Sự Chạy Trong Production

Chúng tôi đã chạy setup này trong production suốt 18 tháng cho ba dự án. Pipeline deploy rất đơn giản: push lên main, SSH vào server, pull image mới nhất, chạy docker compose up -d. Không có overhead của Kubernetes, không có sự phức tạp của cloud-native.

Hai đến năm kỹ sư, traffic hàng trăm nghìn request mỗi tháng — setup này xử lý tốt. Ba bổ sung tạo ra sự khác biệt cho tính ổn định trong production:

  • restart: unless-stopped trên mọi service — container tự động restart sau khi crash hoặc server khởi động lại.
  • Resource limit qua deploy.resources.limits — ngăn một tiến trình chạy mất kiểm soát làm thiếu tài nguyên cho các container khác.
  • Log driver — Driver json-file mặc định của Docker lưu log không giới hạn kích thước. Hãy set max-sizemax-file, nếu không disk của bạn sẽ đầy sớm thôi.
services:
  api:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    deploy:
      resources:
        limits:
          memory: 512m
          cpus: '0.5'

Kết Luận: Khi Nào Compose Là Lựa Chọn Đúng

Sau hơn một năm chạy trên nhiều dự án, đây là những nơi Compose thực sự phát huy giá trị:

  • Môi trường local development — giúp thành viên mới chạy được trong vài phút thay vì vài giờ.
  • Production deployment quy mô nhỏ đến vừa trên một host đơn hoặc một cluster nhỏ.
  • CI pipeline — khởi động full stack cho integration test, rồi dọn dẹp gọn gàng.
  • Môi trường staging cần phản ánh production mà không tốn chi phí của full orchestration.

Khi bạn vượt qua giới hạn của nó, các kỹ năng chuyển thẳng sang Kubernetes. Các khái niệm — service, network, volume, health check — đều giống nhau. Compose là nơi bạn học chúng mà không phải trả giá bằng sự phức tạp.

Developer mới mất ba mươi phút để setup? Sau khi chúng tôi thêm compose.yaml vào repo, thời gian onboarding giảm xuống dưới năm phút. Một lệnh, mọi thứ chạy, sẵn sàng làm việc. Đó mới là giá trị thực sự.

Share: