Docker Compose: Quản lý Multi-Container Applications từ Thực Chiến

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

Làm ngay trong 5 phút: Stack hoàn chỉnh chạy được liền

Thay vì giải thích lý thuyết, mình sẽ bắt đầu bằng thứ thực tế nhất — một stack app + database chạy được ngay. Tạo file docker-compose.yml:

version: '3.8'

services:
  db:
    image: mysql:8.0
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: myapp
      MYSQL_USER: app_user
      MYSQL_PASSWORD: app_pass
    volumes:
      - db_data:/var/lib/mysql

  app:
    image: wordpress:latest
    restart: unless-stopped
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: myapp
      WORDPRESS_DB_USER: app_user
      WORDPRESS_DB_PASSWORD: app_pass
    depends_on:
      - db

volumes:
  db_data:

Chạy stack:

docker compose up -d

Kiểm tra trạng thái:

docker compose ps
docker compose logs -f app

Mở http://localhost:8080 — WordPress đang chạy với MySQL. Cả quá trình mất chưa đầy 30 giây. So sánh với cách thủ công: một lệnh docker run cho MySQL với 5-6 flag, một lệnh khác cho WordPress với 8-10 flag, rồi tự tạo network để hai container nhìn thấy nhau… Compose gói tất cả vào một file, một lệnh.

Giải thích chi tiết: Hiểu để không bị bẫy

depends_on không phải là “chờ service sẵn sàng”

Cái bẫy này mình đã tự bước vào lần đầu setup production. depends_on chỉ đảm bảo container khởi động theo thứ tự — không đảm bảo service bên trong đã sẵn sàng nhận kết nối. MySQL thường cần 15-30 giây để init và chấp nhận connection, nhưng app container đã start xong trước đó rồi.

Fix đúng cách bằng healthcheck:

services:
  db:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  app:
    depends_on:
      db:
        condition: service_healthy

Networks: Containers “nhìn thấy” nhau thế nào

Docker Compose tự tạo một bridge network riêng cho mỗi project. Các container giao tiếp qua tên service — không phải IP. Đó là lý do bạn dùng WORDPRESS_DB_HOST: db thay vì IP address cứng: IP có thể thay đổi mỗi lần restart, tên service thì không.

Khi stack lớn hơn, bạn sẽ muốn tách traffic. Ví dụ nginx cần nói chuyện với cả frontend lẫn backend, nhưng database không được phép ra ngoài internet:

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # Không có internet access

services:
  nginx:
    networks:
      - frontend
      - backend

  api:
    networks:
      - backend

  db:
    networks:
      - backend

internal: true là mẹo hay để isolate database — container db không thể tự outbound ra ngoài internet, giảm attack surface đáng kể.

Volumes: Named vs Bind Mount

services:
  app:
    volumes:
      # Named volume — Docker quản lý, persist khi container bị xóa
      - app_data:/var/www/html/uploads

      # Bind mount — mount thư mục từ host (tiện khi dev)
      - ./config/nginx.conf:/etc/nginx/nginx.conf:ro

      # tmpfs — chỉ trong RAM, mất khi container stop (dùng cho cache)
      - type: tmpfs
        target: /tmp/cache

volumes:
  app_data:

Quy tắc đơn giản: bind mount cho config files khi dev — sửa nginx.conf trên host là apply ngay, không cần rebuild image. Named volume cho data cần persist như database và uploads — Docker quản lý, tồn tại ngay cả khi bạn xóa và tạo lại container.

Nâng cao: Patterns dùng hàng ngày trên production

Tách môi trường với .env file

Không bao giờ hardcode credentials trong docker-compose.yml. Dùng .env và thêm nó vào .gitignore ngay:

# .env
MYSQL_ROOT_PASSWORD=super_secret_pass
MYSQL_DATABASE=production_db
APP_PORT=8080
APP_ENV=production
# docker-compose.yml
services:
  db:
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}

  app:
    ports:
      - "${APP_PORT}:80"
    environment:
      APP_ENV: ${APP_ENV}

Override files cho dev và prod

Pattern này cực kỳ tiện khi team vừa dev vừa deploy production từ cùng một codebase. Giữ config chung trong docker-compose.yml, tạo file override riêng cho từng môi trường:

# docker-compose.override.yml (tự động load khi chạy 'docker compose up')
services:
  app:
    volumes:
      - .:/var/www/html  # Mount source code khi dev
    environment:
      APP_DEBUG: "true"
# docker-compose.prod.yml
services:
  app:
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
    restart: always
# Deploy production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Resource limits — bảo vệ cả cluster

Trên cluster chạy 30+ container, mình hay gặp tình huống này: một service bị memory leak âm thầm ăn hết RAM, kéo cả node xuống. Không ai hay vì không có gì ngăn nó cả. Apply resource limits xong, chuyện đó không tái diễn — và resource usage tổng giảm khoảng 40% nhờ mỗi service chỉ được dùng đúng phần của mình.

services:
  api:
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 256M
        reservations:
          cpus: '0.25'
          memory: 128M

Profiles: Chỉ chạy service khi cần

services:
  app:  # Không có profile = luôn chạy
  db:   # Không có profile = luôn chạy

  adminer:
    image: adminer
    profiles: ["tools"]  # Chỉ chạy khi gọi với --profile tools
    ports:
      - "8081:8080"
# Chạy app + db bình thường
docker compose up -d

# Bật thêm adminer khi cần debug database
docker compose --profile tools up -d

Tips thực tế: Những lệnh hay dùng mà ít ai nhớ

Lệnh hữu ích trong công việc hàng ngày

# Xem log nhiều service cùng lúc, filter theo keyword
docker compose logs -f --tail=100 app db | grep ERROR

# Exec vào container đang chạy
docker compose exec app bash

# Scale service mà không down các service khác
docker compose up -d --scale worker=3

# Rebuild và redeploy chỉ 1 service
docker compose up -d --no-deps --build app

# Xem resource usage realtime
docker compose stats

# Pull image mới nhất và restart
docker compose pull && docker compose up -d

Đặt tên project — tránh conflict sau 3 tháng

Docker Compose đặt tên container theo pattern {project}_{service}_{replica}. Project name mặc định là tên thư mục. Vấn đề: hai project khác nhau cùng đặt thư mục là app sẽ share network và conflict container name — một lỗi rất khó debug khi nó xảy ra lúc 2 giờ sáng. Khai báo name ngay đầu file là xong:

name: myapp-production

services:
  ...

Cleanup định kỳ để không hết disk

# Xóa containers và networks của project (giữ volumes)
docker compose down

# Xóa luôn volumes — cẩn thận, mất data!
docker compose down -v

# Dọn dẹp toàn bộ Docker: stopped containers, unused images, volumes
docker system prune -a --volumes

Mình đặt cron chạy docker system prune -f hàng tuần trên server — tiết kiệm được cả chục GB disk space mỗi tháng mà không cần can thiệp thủ công.

Share: