Thinh Nguyen's site
← Back to blog

Technical notes

Docker Compose Thực Chiến: Đừng Nhớ Khái Niệm, Hãy Nhớ Use Case

6 min read By Thinh Nguyen

Quên đi mớ lý thuyết khô khan. Bài viết này hướng dẫn sử dụng Docker Compose thông qua các bài toán thực tế bạn sẽ gặp mỗi ngày.

#docker #docker-compose

Nếu bạn đã biết dùng docker run cho một container đơn lẻ, chắc chắn bạn sẽ thấy “mệt mỏi” khi dự án của bạn cần tới 3, 4 thứ chạy cùng lúc: một cái Backend, một cái Database MySQL, và một cái Redis cache.

Chẳng lẽ mỗi lần làm việc bạn lại phải gõ 3 dòng lệnh docker run dài ngoằng, tạo network để chúng gọi nhau, nhớ từng port, từng biến môi trường? Đó là lúc Docker Compose xuất hiện. Thay vì gõ lệnh thủ công, ta khai báo tất cả vào một file duy nhất, rồi gõ 1 lệnh duy nhất để gọi tất cả thức dậy.

Bài viết này sẽ không dài dòng giải thích khái niệm hàn lâm. Chúng ta sẽ đi thẳng vào cấu trúc cơ bản và các Use Case (tình huống sử dụng) thực tế nhất.


1. Cấu trúc cơ bản của file docker-compose.yml

Bản chất của Docker Compose là thay thế các câu lệnh docker run bằng một file cấu hình dạng YAML. Cấu trúc thường xoay quanh 3 phần chính:

version: '3.8' # Phiên bản cú pháp Compose

services: # Nơi khai báo các "container" bạn muốn chạy
  
  # Tên dịch vụ 1 (vd: database)
  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: secret
    ports:
      - "5432:5432"

  # Tên dịch vụ 2 (vd: web backend)
  backend:
    build: . # Build image từ Dockerfile trong thư mục hiện tại
    ports:
      - "3000:3000"
    depends_on:
      - db # Đảm bảo db chạy trước khi backend chạy

volumes: # Nơi khai báo ổ cứng ảo để lưu trữ dữ liệu không bị mất khi xóa container
  db_data:

networks: # (Tùy chọn) Nơi khai báo mạng riêng để phân lập các container
  my_network:

Bạn chỉ cần nhớ: Services là các ứng dụng/container, Ports là cổng kết nối, Environment là biến môi trường, Volumes là chỗ lưu data, và Networks là hạ tầng mạng để quy định ai được quyền nói chuyện với ai.


2. Các Use Case Thường Gặp Nhất Với Docker Compose

Case 1: Dựng môi trường “Fullstack” (Backend + DB + Cache) chỉ với 1 lệnh

Tình huống: Bạn vừa join vào một dự án mới. Dự án này chạy Node.js cho backend, PostgreSQL cho database, và Redis để làm cache. Ở công ty cũ, bạn phải mất cả buổi sáng để cài đặt thư viện và config môi trường.

Cách giải quyết: Sử dụng một file docker-compose.yml tổng quát dưới đây đặt ở thư mục gốc của project.

version: '3.8'

services:
  # 1. Ứng dụng của bạn
  api:
    build: . # Tự build image từ source code của bạn
    container_name: my_backend_api
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=postgres_db # Trỏ thẳng đến tên service của database
      - REDIS_HOST=redis_cache
      - DB_USER=myuser
      - DB_PASS=mypassword
    depends_on:
      - postgres_db
      - redis_cache
    # Mount source code vào để sửa code là nhận luôn (Hot Reload)
    volumes:
      - ./:/app

  # 2. Database PostgreSQL
  postgres_db:
    image: postgres:14-alpine
    container_name: my_postgres
    environment:
      - POSTGRES_USER=myuser
      - POSTGRES_PASSWORD=mypassword
      - POSTGRES_DB=mydb
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data # Lưu dữ liệu thực tế vào volume

  # 3. Redis Cache
  redis_cache:
    image: redis:alpine
    container_name: my_redis
    ports:
      - "6379:6379"

volumes:
  pgdata: # Khai báo volume để dữ liệu PostgreSQL không bốc hơi khi tắt Compose

Chỉ cần đưa file này cho bất kỳ ai (developer, QC), họ chỉ việc mở terminal ở thư mục dự án và gõ:

docker-compose up -d

Bùm! Môi trường sẵn sàng trong 1 phút. Cả 3 container tự động nhận diện và nói chuyện được với nhau bằng tên dịch vụ (ví dụ api có thể kết nối với database thông qua host có tên là postgres_db).

Case 2: Dọn dẹp cục bộ, “Reset” toàn bộ dự án về trạng thái ban đầu

Tình huống: Trong lúc dev, bạn lỡ tay chọc ngoáy làm hỏng data trong database, hoặc ứng dụng chạy sai lệch. Bạn muốn “đập đi xây lại” toàn bộ database và các container từ đầu như một tờ giấy trắng.

Cách giải quyết: Xóa bỏ toàn bộ hệ thống bằng cờ -v (xóa luôn cả volumes).

# Tắt tất cả container và XÓA luôn data cũ (Reset hoàn toàn)
docker-compose down -v

# Sau đó dựng lại 1 hệ thống mới tinh
docker-compose up -d

Ghi chú: Nếu không thêm cờ -v, lệnh down sẽ chỉ tắt và xóa container chứ không xóa database volume, lần sau bạn up lên thì data cũ vẫn còn nguyên.

Case 3: Theo dõi Log của “Nhiều ứng dụng” cùng một lúc

Tình huống: Người dùng báo lỗi lúc gọi API, bạn không biết lỗi nằm ở Backend (Node.js) hay do Database (Postgres) từ chối kết nối. Nếu dùng docker run, bạn phải mở 2 terminal để theo dõi log từng cái.

Cách giải quyết: Bạn có thể xem log của toàn bộ hệ thống (tất cả các service gộp chung) bằng lệnh:

docker-compose logs -f

Mỗi dòng log sẽ được gắn tag màu sắc và tên service ở đầu (VD: api | error..., postgres_db | connection lost...) giúp bạn trace lỗi cực nhanh.

Nếu log quá loạn và bạn chỉ muốn xem log của riêng một service (ví dụ service api):

docker-compose logs -f api

Case 4: Cần vào trong 1 container để chạy lệnh (VD: Chạy Migration)

Tình huống: Các container đang chạy ngầm (-d). Bạn cần chạy lệnh migrate database của Backend (ví dụ npm run migrate hoặc php artisan migrate).

Cách giải quyết: Thay vì phải dùng docker ps tìm ID dài ngoằng, Compose cho phép bạn gọi thẳng tên service:

docker-compose exec api npm run migrate

Lệnh trên được hiểu là: Trong cụm compose này, hãy chui vào service tên là api và chạy lệnh npm run migrate cho tôi. Hoặc nếu bạn muốn chui hẳn vào shell của container đó:

docker-compose exec api sh

Case 5: Bảo mật hệ thống bằng cách chia tách Network (Mạng)

Tình huống: Hệ thống của bạn gồm 3 lớp: Frontend (React), Backend (Node.js) và Database (MySQL). Theo quy chuẩn bảo mật, bạn chỉ cho phép Frontend gọi Backend, và Backend gọi Database. Bạn tuyệt đối không muốn Frontend có bất kỳ đường nối trực tiếp nào tới Database để tránh nguy cơ rò rỉ dữ liệu.

Cách giải quyết: Mặc định nếu bạn không khai báo networks, Compose sẽ gom tất cả vào chung 1 mạng (ai cũng gọi được nhau). Để xử lý bài toán này, ta chia thành 2 mạng riêng biệt: frontend_netbackend_net.

version: '3.8'

services:
  frontend:
    image: my-react-app
    ports:
      - "80:80"
    networks:
      - frontend_net # Chỉ join mạng frontend
      
  backend:
    image: my-node-api
    networks:
      - frontend_net # Join mạng frontend để nghe React gọi
      - backend_net  # Join mạng backend để gọi DB

  database:
    image: mysql:8
    networks:
      - backend_net # Chỉ join mạng backend. Frontend sẽ "mù", không thể chạm tới đây!

networks: # Nơi khai báo tạo mạng
  frontend_net:
  backend_net:

Bằng cách phân chia network như vậy, bạn đã thiết lập một lớp bảo mật nội bộ cực kỳ an toàn giữa các dịch vụ!


Lời Kết

Bằng cách tư duy theo Use Case, bạn sẽ thấy Docker Compose thực chất chỉ là “người quản gia” tự động hóa các thao tác thủ công. Thay vì phải làm thợ cơ khí tự tay thiết lập network, vặn từng con ốc (docker run, docker network connect), bạn chỉ cần vẽ ra bản thiết kế (docker-compose.yml), đưa cho quản gia và yêu cầu: docker-compose up -d.

Mong rằng bài viết này giúp bạn hình dung ngay được khi nào và làm sao để áp dụng Compose vào dự án thực tế của mình!

Tags

#docker #docker-compose