Bài 40.1: Docker Compose configuration

Chào mừng bạn đến với bài học về Docker Compose, một công cụ mạnh mẽ giúp định nghĩa và chạy các ứng dụng đa container của Docker. Nếu bạn đã làm quen với Docker và thấy việc quản lý nhiều container (ví dụ: một container cho database, một cho backend API, một cho frontend, một cho cache...) bằng các lệnh docker run riêng lẻ khá phức tạptốn thời gian, thì Docker Compose chính là vị cứu tinh của bạn!

Docker Compose cho phép bạn mô tả toàn bộ kiến trúc ứng dụng của mình trong một tệp tin duy nhất, thường là docker-compose.yml. Sau đó, chỉ với một lệnh đơn giản, bạn có thể khởi tạo, cấu hình và kết nối tất cả các dịch vụ (services) cần thiết. Điều này không chỉ giúp đơn giản hóa quy trình làm việc mà còn đảm bảo môi trường phát triển và triển khai của bạn luôn nhất quán.

docker-compose.yml: Trái tim của ứng dụng đa container

Tệp tin docker-compose.yml (hoặc docker-compose.yaml) là nơi bạn định nghĩa tất cả các thành phần (services) của ứng dụng, cách chúng hoạt động, kết nối với nhau và các tài nguyên mà chúng cần (như cổng, volumes, network). Đây là một tệp tin cấu hình theo định dạng YAML, nổi tiếng với cú pháp đơn giảndễ đọc.

Một cấu trúc cơ bản của tệp tin này sẽ trông như sau:

version: '3.8' # Xác định phiên bản của cú pháp Docker Compose file

services: # Phần định nghĩa các dịch vụ (các container) trong ứng dụng của bạn
  service1: # Tên của dịch vụ đầu tiên
    # Cấu hình cho service1
    ...

  service2: # Tên của dịch vụ thứ hai
    # Cấu hình cho service2
    ...

# Các định nghĩa khác như Volumes, Networks (tuỳ chọn)
volumes:
  volume_name:

networks:
  network_name:
    driver: bridge

Hãy đi sâu vào các phần chính.

version

Phần này chỉ định phiên bản của cú pháp Compose file mà bạn đang sử dụng. Các phiên bản khác nhau có thể hỗ trợ các tính năng khác nhau. Phiên bản 3.x thường được sử dụng và tương thích với các tính năng Docker mới nhất.

version: '3.8'

Giải thích: Khai báo này cho Docker Compose biết nên phân tích tệp tin dựa trên quy tắc và tính năng của phiên bản 3.8. Luôn nên sử dụng phiên bản mới nhất hoặc phiên bản được khuyến nghị cho môi trường của bạn.

services

Đây là phần quan trọng nhất. Dưới mục services, bạn liệt kê từng thành phần của ứng dụng như một dịch vụ riêng biệt. Mỗi dịch vụ sẽ tương ứng với một container (hoặc nhiều container cùng loại nếu cấu hình mở rộng).

Cấu hình chi tiết cho một service

Bên trong mỗi định nghĩa dịch vụ, có rất nhiều tùy chọn cấu hình. Dưới đây là một số tùy chọn phổ biến và cực kỳ hữu ích cho phát triển web:

  1. image: Chỉ định image Docker để sử dụng cho dịch vụ này. Compose sẽ tự động tải image nếu nó chưa tồn tại trên máy của bạn.

    services:
      nginx:
        image: nginx:latest # Sử dụng image Nginx phiên bản latest
    

    Giải thích: Khai báo này nói với Docker Compose rằng dịch vụ có tên nginx sẽ được tạo từ image nginx:latest từ Docker Hub.

  2. build: Thay vì sử dụng một image có sẵn, bạn có thể yêu cầu Compose tự build image từ một Dockerfile. Điều này rất phổ biến cho các ứng dụng backend hoặc frontend cần môi trường tùy chỉnh.

    services:
      backend:
        build: ./backend # Build image từ Dockerfile trong thư mục ./backend
    

    Giải thích: Dịch vụ backend sẽ được tạo từ một image được xây dựng dựa trên nội dung và Dockerfile trong thư mục ./backend trên máy host của bạn.

  3. ports: Rất quan trọng cho các ứng dụng web! Ánh xạ (mapping) cổng giữa máy host của bạn và container. Điều này cho phép bạn truy cập vào ứng dụng đang chạy trong container từ trình duyệt trên máy tính của mình.

    services:
      frontend:
        image: node:lts
        ports:
          - "3000:3000" # Ánh xạ cổng 3000 của host tới cổng 3000 của container
          - "8080:80"   # Ánh xạ cổng 8080 của host tới cổng 80 của container
    

    Giải thích: Dòng - "3000:3000" nghĩa là bất kỳ traffic nào đến cổng 3000 trên máy host sẽ được chuyển hướng đến cổng 3000 bên trong container frontend. Dòng - "8080:80" làm tương tự với cổng 8080 trên host và cổng 80 trong container.

  4. volumes: Cấu hình volumes cho container. Volumes là cách ưa thích để lưu trữ dữ liệu bền vững (persistent data) hoặc để mount code từ máy host vào container trong môi trường phát triển.

    services:
      database:
        image: postgres:latest
        volumes:
          - db_data:/var/lib/postgresql/data # Ánh xạ named volume 'db_data' tới đường dẫn dữ liệu của Postgres trong container
    
      backend:
        build: ./backend
        volumes:
          - ./backend:/app # Mount thư mục code backend từ host vào /app trong container
          - /app/node_modules # Bỏ qua (ignore) thư mục node_modules trên host khi mount
    

    Giải thích:

    • Dòng - db_data:/var/lib/postgresql/data sử dụng một named volume (db_data, được định nghĩa ở phần volumes ở ngoài cùng) để lưu trữ dữ liệu của PostgreSQL. Dữ liệu này sẽ không bị mất khi container database bị xoá và tạo lại.
    • Dòng - ./backend:/app là một bind mount. Nó ánh xạ thư mục ./backend trên máy host (nơi chứa code nguồn của bạn) tới thư mục /app bên trong container backend. Điều này cực kỳ hữu ích cho phát triển vì bất kỳ thay đổi nào bạn thực hiện với code trên máy host sẽ ngay lập tức hiển thị trong container (thường kết hợp với các công cụ hot-reloading).
    • Dòng - /app/node_modules là một kỹ thuật nâng cao trong bind mount. Khi bạn mount thư mục ./backend chứa node_modules của host vào /app, nó sẽ ghi đè lên thư mục node_modules mà Dockerfile có thể đã tạo bên trong container. Dòng này tạo một volume ẩn tại /app/node_modules bên trong container, đảm bảo rằng container sử dụng node_modules của chính nó (được cài đặt trong quá trình build hoặc khi chạy lệnh npm install bên trong container) thay vì thư mục node_modules từ máy host (mà có thể có kiến trúc khác).
  5. environment: Thiết lập các biến môi trường (environment variables) trong container. Rất hữu ích để cấu hình ứng dụng (ví dụ: thông tin kết nối database, API keys...).

    services:
      backend:
        build: ./backend
        environment:
          - DATABASE_URL=postgresql://myuser:mypassword@database:5432/mydb # Thiết lập biến môi trường
          - NODE_ENV=development
          - API_KEY=your_secret_key # Lưu ý: Không nên lưu trữ key nhạy cảm trực tiếp trong file này ở production
    

    Giải thích: Các biến này sẽ có sẵn trong môi trường chạy của container backend. Ứng dụng backend của bạn có thể đọc các biến này để biết cách kết nối tới database (sử dụng tên dịch vụ database làm hostname nhờ mạng nội bộ của Docker Compose) hoặc cấu hình các setting khác.

  6. depends_on: Chỉ định sự phụ thuộc giữa các dịch vụ. Docker Compose sẽ đảm bảo các dịch vụ trong danh sách này được khởi tạo trước dịch vụ hiện tại.

    services:
      backend:
        build: ./backend
        depends_on:
          - database # Đảm bảo dịch vụ 'database' khởi động trước 'backend'
          - cache    # Đảm bảo dịch vụ 'cache' khởi động trước 'backend'
    

    Giải thích: Compose sẽ chờ cho container của dịch vụ databasecache bắt đầu chạy (ở trạng thái started, không nhất thiết là healthy trừ khi bạn cấu hình healthcheck) trước khi khởi động container backend. Điều này giúp tránh lỗi kết nối khi dịch vụ backend cố gắng truy cập database hoặc cache khi chúng chưa sẵn sàng.

  7. networks: Gán dịch vụ vào một hoặc nhiều mạng đã được định nghĩa. Mặc định, Compose tạo một mạng bridge cho tất cả các dịch vụ, cho phép chúng giao tiếp với nhau bằng tên dịch vụ. Tuy nhiên, việc định nghĩa mạng tùy chỉnh giúp kiểm soát tốt hơn luồng traffic và cô lập các dịch vụ.

    services:
      frontend:
        build: ./frontend
        networks:
          - app-network # Tham gia mạng 'app-network'
    
      backend:
        build: ./backend
        networks:
          - app-network # Tham gia mạng 'app-network'
    
      database:
        image: postgres:latest
        networks:
          - app-network # Tham gia mạng 'app-network'
    
    networks:
      app-network: # Định nghĩa mạng tên 'app-network'
        driver: bridge
    

    Giải thích: Tất cả ba dịch vụ (frontend, backend, database) đều được kết nối vào cùng một mạng tùy chỉnh app-network. Điều này cho phép chúng "nhìn thấy" nhau bằng tên dịch vụ (ví dụ, backend có thể kết nối tới database bằng hostname database).

volumes (Top-level)

Phần này định nghĩa các named volumes mà bạn muốn sử dụng. Named volumes là cách được khuyến khích để lưu trữ dữ liệu bền vững trong Docker vì chúng được quản lý bởi Docker và độc lập với chu kỳ sống của container.

version: '3.8'
services:
  # ... các dịch vụ sử dụng volume ...
  database:
    image: postgres:latest
    volumes:
      - db_data:/var/lib/postgresql/data # Sử dụng named volume 'db_data'

volumes: # Định nghĩa named volumes
  db_data: # Tên của volume
    driver: local # Có thể tuỳ chọn driver (mặc định là local)

Giải thích: Khối volumes ở cấp cao nhất định nghĩa một named volume có tên là db_data. Volume này sau đó được tham chiếu trong định nghĩa dịch vụ database để lưu trữ dữ liệu.

networks (Top-level)

Tương tự như volumes, bạn có thể định nghĩa các mạng tùy chỉnh ở đây. Mạng tùy chỉnh giúp các container trong ứng dụng của bạn giao tiếp với nhau một cách an toàn và dễ dàng bằng tên dịch vụ, đồng thời cô lập chúng khỏi các container không liên quan khác trên cùng một máy host.

version: '3.8'
services:
  # ... các dịch vụ sử dụng mạng ...
  frontend:
    networks:
      - app-network

  backend:
    networks:
      - app-network

networks: # Định nghĩa mạng tùy chỉnh
  app-network: # Tên mạng
    driver: bridge # Loại mạng (bridge là phổ biến nhất)

Giải thích: Khối networks ở cấp cao nhất định nghĩa một mạng bridge tùy chỉnh có tên app-network. Các dịch vụ frontendbackend được kết nối vào mạng này, cho phép chúng giao tiếp nội bộ.

Ví dụ tổng hợp: Ứng dụng Web Full-stack đơn giản

Hãy ghép nối các phần lại để tạo một tệp docker-compose.yml cho một ứng dụng web đơn giản gồm 3 dịch vụ:

  1. Frontend: Ứng dụng React/Vue/Angular được build tĩnh hoặc chạy bởi Node.js dev server. (Giả sử dùng Nginx phục vụ tĩnh cho đơn giản).
  2. Backend: API server (ví dụ: Node.js/Express).
  3. Database: PostgreSQL.

Cấu trúc thư mục giả định:

.
├── docker-compose.yml
├── frontend/
│   ├── Dockerfile
│   └── index.html (hoặc code frontend)
└── backend/
    ├── Dockerfile
    └── index.js (hoặc code backend)

./frontend/Dockerfile (Ví dụ đơn giản phục vụ HTML tĩnh bằng Nginx):

FROM nginx:alpine
COPY . /usr/share/nginx/html

./backend/Dockerfile (Ví dụ đơn giản Node.js):

FROM node:lts-alpine
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
CMD ["node", "index.js"]

Và đây là tệp docker-compose.yml:

version: '3.8'

services:
  frontend:
    build: ./frontend
    ports:
      - "80:80" # Ánh xạ cổng 80 của host tới cổng 80 của container (Nginx mặc định)
    networks:
      - app-network # Tham gia mạng nội bộ

  backend:
    build: ./backend
    ports:
      - "5000:5000" # Ánh xạ cổng 5000 của host tới cổng 5000 của container (nơi backend lắng nghe)
    environment:
      - DATABASE_URL=postgresql://myuser:mypassword@database:5432/mydb # Kết nối tới database
    depends_on:
      - database # Đảm bảo database khởi động trước
    volumes:
      - ./backend:/app # Mount code backend cho phát triển (nếu muốn hot-reloading)
      - /app/node_modules # Volume rỗng cho node_modules (xem giải thích ở trên)
    networks:
      - app-network # Tham gia mạng nội bộ

  database:
    image: postgres:latest # Sử dụng image Postgres
    ports:
      - "5432:5432" # Ánh xạ cổng 5432 của host tới cổng 5432 của container (tùy chọn, chỉ dùng khi cần truy cập DB từ host)
    environment:
      - POSTGRES_DB=mydb
      - POSTGRES_USER=myuser
      - POSTGRES_PASSWORD=mypassword
    volumes:
      - postgres_data:/var/lib/postgresql/data # Lưu trữ dữ liệu DB bền vững
    networks:
      - app-network # Tham gia mạng nội bộ

volumes: # Định nghĩa named volume cho dữ liệu database
  postgres_data:

networks: # Định nghĩa mạng nội bộ
  app-network:
    driver: bridge

Giải thích tổng thể ví dụ:

  • Tệp này định nghĩa 3 dịch vụ (frontend, backend, database).
  • frontendbackend được build từ Dockerfile trong các thư mục tương ứng. database sử dụng image có sẵn.
  • Cổng 80 của host được nối tới cổng 80 của container frontend, cho phép truy cập ứng dụng qua http://localhost.
  • Cổng 5000 của host được nối tới cổng 5000 của container backend (cho phép truy cập API trực tiếp nếu cần, hoặc chỉ để frontend truy cập nội bộ qua mạng Docker).
  • Dịch vụ backend phụ thuộc vào database, đảm bảo database sẵn sàng trước.
  • Dịch vụ backend được truyền thông tin kết nối database qua biến môi trường DATABASE_URL. Lưu ý hostname database được sử dụng, đây là tên dịch vụ trong mạng nội bộ của Docker Compose.
  • Dữ liệu của PostgreSQL được lưu vào named volume postgres_data để không bị mất khi container database dừng/khởi động lại.
  • Tất cả các dịch vụ đều nằm trong mạng nội bộ app-network, cho phép chúng giao tiếp với nhau một cách an toàn bằng tên dịch vụ (ví dụ backend dùng hostname database để nối tới database).
  • Trong dịch vụ backend, có sử dụng bind mount ./backend:/app và volume rỗng /app/node_modules. Cấu hình này rất phổ biến cho môi trường phát triển Node.js: code từ host được mount vào /app, còn thư mục node_modules sẽ sử dụng các gói được cài đặt bên trong container, tương thích với môi trường container.

Các lệnh Docker Compose cơ bản

Sau khi có tệp docker-compose.yml, bạn sử dụng lệnh docker-compose để tương tác với ứng dụng của mình.

  1. docker-compose up: Khởi tạo và chạy toàn bộ ứng dụng (tất cả các dịch vụ được định nghĩa trong tệp). Nếu các image chưa tồn tại, Compose sẽ build (nếu có build) hoặc pull (nếu có image).

    docker-compose up # Chạy ở chế độ foreground (hiển thị logs)
    docker-compose up -d # Chạy ở chế độ detached (nền)
    docker-compose up --build # Buộc build lại image trước khi chạy
    

    Giải thích: Lệnh này là lệnh phổ biến nhất. Nó đọc tệp docker-compose.yml, tạo mạng, volumes (nếu chưa có) và khởi tạo các container theo đúng thứ tự phụ thuộc.

  2. docker-compose down: Dừng và xóa các container, mạng và volumes (trừ các named volumes) được tạo bởi up.

    docker-compose down # Dừng và xóa container, mạng
    docker-compose down --volumes # Dừng, xóa container, mạng VÀ named volumes
    

    Giải thích: Lệnh này giúp "dọn dẹp" môi trường ứng dụng của bạn. Sử dụng --volumes khi bạn muốn xóa cả dữ liệu bền vững (cẩn thận khi dùng trong môi trường có dữ liệu quan trọng!).

  3. docker-compose ps: Liệt kê trạng thái của các dịch vụ (container) trong ứng dụng.

    docker-compose ps
    

    Giải thích: Cho biết container nào đang chạy, cổng nào đang được ánh xạ, và trạng thái hiện tại.

  4. docker-compose logs [service_name]: Xem log đầu ra của một hoặc tất cả các dịch vụ.

    docker-compose logs frontend # Xem log của dịch vụ frontend
    docker-compose logs # Xem log của tất cả các dịch vụ
    

    Giải thích: Hữu ích để gỡ lỗi và kiểm tra hoạt động của ứng dụng.

  5. docker-compose exec [service_name] [command]: Thực thi một lệnh bên trong một container đang chạy của một dịch vụ cụ thể.

    docker-compose exec backend bash # Mở shell trong container backend
    docker-compose exec database psql -U myuser mydb # Chạy lệnh psql để kết nối DB trong container database
    

    Giải thích: Cho phép bạn tương tác trực tiếp với môi trường bên trong container để kiểm tra, gỡ lỗi hoặc chạy các tác vụ thủ công.

  6. docker-compose build [service_name]: Build lại image cho một hoặc tất cả các dịch vụ có sử dụng build.

    docker-compose build backend # Chỉ build lại image cho dịch vụ backend
    docker-compose build # Build lại image cho tất cả dịch vụ có 'build'
    

    Giải thích: Sử dụng khi bạn đã thay đổi Dockerfile hoặc nội dung cần build lại image.

Tại sao Docker Compose đặc biệt hữu ích cho Lập trình Web?

  • Thiết lập môi trường phát triển nhất quán: Toàn bộ team có thể chạy môi trường phát triển giống hệt nhau chỉ với docker-compose up. Không còn "Nó chạy được trên máy tôi!".
  • Quản lý dịch vụ dễ dàng: Khởi động, dừng, khởi động lại toàn bộ ứng dụng đa dịch vụ chỉ với một vài lệnh.
  • Tách biệt môi trường: Dễ dàng định nghĩa các cấu hình khác nhau cho môi trường phát triển, staging, production bằng cách sử dụng nhiều tệp Compose và tính năng extends hoặc profile.
  • Tích hợp CI/CD: Docker Compose là nền tảng tuyệt vời để chạy các bài kiểm thử tích hợp trong môi trường giống với production.
  • Hot-reloading hiệu quả: Với volumes dạng bind mount, thay đổi code trên host sẽ được cập nhật trong container, giúp tăng tốc độ phát triển với các framework hỗ trợ hot-reloading.
  • Cấu hình tập trung: Tất cả cấu hình cho ứng dụng của bạn nằm gọn trong một tệp, dễ dàng quản lý và versioning.

Sử dụng Docker Compose không chỉ giúp bạn quản lý các ứng dụng phức tạp dễ dàng hơn mà còn là một kỹ năng quan trọng trong thế giới DevOps hiện đại. Nắm vững cách định cấu hình tệp docker-compose.yml sẽ mở ra rất nhiều khả năng trong việc xây dựng, thử nghiệm và triển khai các ứng dụng web. Hãy thử áp dụng nó vào dự án tiếp theo của bạn!

Comments

There are no comments at the moment.