Khi làm việc với các ứng dụng hiện đại, hiếm khi bạn chỉ cần chạy một container độc lập. Thông thường, chúng ta sẽ có một hệ thống gồm nhiều thành phần giao tiếp với nhau: một container chạy web server (như Nginx), một container chạy ứng dụng backend (Node.js, Python/Flask/Django, Java/Spring), một container database (PostgreSQL, MySQL, MongoDB), và có thể thêm container cho cache (Redis) hoặc message queue (RabbitMQ).
Việc quản lý các container này một cách riêng lẻ bằng lệnh docker run
có thể trở nên phức tạp và tốn thời gian. Bạn cần nhớ các port mapping, volume, network và các cờ cấu hình khác cho từng container. Khi số lượng service tăng lên, việc này trở nên khó khăn và dễ gây lỗi.
Đây chính là lúc Docker Compose tỏa sáng. Nó là một công cụ được thiết kế để định nghĩa và chạy các ứng dụng đa container bằng cách sử dụng một file cấu hình duy nhất. Thay vì gõ hàng loạt lệnh docker run
phức tạp, bạn chỉ cần viết cấu hình của toàn bộ ứng dụng vào một file YAML và sử dụng một lệnh duy nhất để khởi động hoặc dừng toàn bộ hệ thống. Nếu bạn chưa quen với khái niệm Container Là Gì, hãy tham khảo lại bài viết trước trong series.
Trong bài viết này, chúng ta sẽ đi sâu vào cách sử dụng Docker Compose để đơn giản hóa quy trình phát triển và kiểm thử ứng dụng đa container, một bước tiến quan trọng trong hành trình Roadmap Docker của bạn.
Mục lục
Tại Sao Cần Docker Compose?
Quản lý nhiều container thủ công bằng các lệnh docker run
dài dòng, phức tạp là một thách thức lớn, đặc biệt là trong môi trường phát triển và kiểm thử. Mỗi khi bạn hoặc đồng đội cần thiết lập môi trường, việc này đòi hỏi phải nhớ chính xác từng tham số, từng volume, từng network. Điều này dẫn đến sự không nhất quán giữa các môi trường làm việc.
Docker Compose giải quyết vấn đề này bằng cách:
- Định nghĩa tập trung: Toàn bộ cấu hình của ứng dụng đa container được viết trong một file
docker-compose.yml
duy nhất. File này có thể được đưa vào hệ thống kiểm soát phiên bản (VCS) như Git, giúp dễ dàng chia sẻ và theo dõi thay đổi. - Môi trường lặp lại (Reproducible Environments): Bất kỳ ai có file
docker-compose.yml
đều có thể dựng lên một môi trường làm việc y hệt chỉ với một lệnh duy nhất. Điều này loại bỏ tình trạng “nó chạy trên máy tôi”. - Đơn giản hóa quy trình: Khởi động, dừng, xây dựng lại hoặc xem log của toàn bộ ứng dụng chỉ cần vài lệnh đơn giản thay vì quản lý từng container riêng lẻ (như khi chỉ dùng chạy container với docker run).
- Networking tích hợp: Compose tự động tạo một mạng mặc định cho các service trong file, cho phép chúng giao tiếp với nhau chỉ bằng tên service mà không cần mapping port phức tạp giữa các container nội bộ.
- Quản lý vòng đời ứng dụng: Compose giúp quản lý toàn bộ vòng đời của ứng dụng đa container, từ xây dựng image, khởi động các service, scale up/down, đến dừng và xóa toàn bộ môi trường.
Đối với các dự án vừa và nhỏ hoặc trong giai đoạn phát triển, Docker Compose là một công cụ vô cùng mạnh mẽ và cần thiết.
Cấu Trúc File docker-compose.yml
Trái tim của Docker Compose là file cấu hình docker-compose.yml
(hoặc docker-compose.yaml
). File này sử dụng định dạng YAML, một định dạng dữ liệu dễ đọc, dễ viết, rất phù hợp cho các file cấu hình.
Một file docker-compose.yml
cơ bản thường có cấu trúc như sau:
version: '3.8' # Phiên bản cú pháp Compose
services:
<tên_service_1>:
# Cấu hình cho service 1 (container 1)
image: ubuntu:latest # Hoặc build: ./path/to/dockerfile
ports:
- "host_port:container_port"
volumes:
- "host_path:container_path"
environment:
- KEY=value
networks:
- my_network
depends_on:
- <tên_service_khác>
<tên_service_2>:
# Cấu hình cho service 2 (container 2)
image: mysql:latest
# ... các cấu hình khác
volumes: # Định nghĩa các named volumes (tùy chọn)
<tên_volume_1>:
<tên_volume_2>:
networks: # Định nghĩa các mạng tùy chỉnh (tùy chọn)
my_network:
driver: bridge # Hoặc driver khác
Hãy cùng phân tích các thành phần chính:
* version
: Chỉ định phiên bản cú pháp của file Compose. Phiên bản ‘3.x’ là phổ biến nhất cho các ứng dụng hiện đại và được sử dụng với Docker Engine tích hợp Compose (lệnh `docker compose`). Phiên bản ‘2.x’ và ‘1.x’ sử dụng với công cụ `docker-compose` độc lập cũ hơn. Chúng ta nên sử dụng cú pháp mới nhất được hỗ trợ.
* services
: Đây là nơi bạn định nghĩa tất cả các container (services) tạo nên ứng dụng của mình. Mỗi key dưới `services` là tên của một service (ví dụ: `web`, `app`, `db`, `redis`). Compose sẽ sử dụng tên này làm hostname cho các container tương ứng bên trong mạng nội bộ.
* volumes
: Định nghĩa các named volumes mà bạn muốn sử dụng để lưu trữ dữ liệu bền vững, độc lập với vòng đời của container.
* networks
: Định nghĩa các mạng tùy chỉnh mà các service sẽ tham gia. Mặc định, Compose sẽ tạo một mạng “bridge” cho toàn bộ ứng dụng, và các service có thể giao tiếp với nhau qua tên service. Tuy nhiên, việc định nghĩa mạng tùy chỉnh có thể hữu ích trong các kịch bản phức tạp hơn.
Định Nghĩa Services Chi Tiết
Bên trong mỗi service, bạn có thể cấu hình rất nhiều thứ, tương tự như các cờ của lệnh `docker run`:
* image
: Chỉ định image Docker sẽ sử dụng để tạo container. Ví dụ: `nginx:latest`, `postgres:13`, `python:3.9`.
* build
: Thay vì kéo một image sẵn có, bạn có thể chỉ định Compose xây dựng image từ một Dockerfile. Bạn cung cấp đường dẫn tới thư mục chứa Dockerfile.
services:
app:
build: ./my-app-source # Compose sẽ tìm Dockerfile trong thư mục ./my-app-source
Bạn cũng có thể chỉ định rõ tên Dockerfile hoặc truyền build arguments:
services:
app:
build:
context: ./my-app-source
dockerfile: Dockerfile.dev
args:
NODE_VERSION: "14"
* ports
: Map port từ host sang container. Cú pháp thông dụng nhất là `”HOST_PORT:CONTAINER_PORT”`.
services:
web:
image: nginx:latest
ports:
- "80:80" # Truy cập Nginx trên host qua port 80
* volumes
: Gắn volume vào container. Có thể là named volume hoặc bind mount. Cú pháp: `”SOURCE:DESTINATION”`.
services:
db:
image: postgres:13
volumes:
- db_data:/var/lib/postgresql/data # Gắn named volume 'db_data' vào thư mục dữ liệu của Postgres
- ./app/config:/etc/app/config # Gắn bind mount từ host vào container (ví dụ: file cấu hình)
Hãy nhớ lại bài viết về Volume Mounts và Bind Mounts để hiểu rõ hơn về sự khác biệt.
* environment
: Đặt các biến môi trường trong container. Có thể dùng dạng list hoặc dictionary.
services:
app:
# ...
environment:
- DATABASE_URL=postgres://user:password@db:5432/mydatabase # Dạng list
- API_KEY=YOUR_API_KEY
db:
# ...
environment:
POSTGRES_USER: user # Dạng dictionary
POSTGRES_PASSWORD: password
POSTGRES_DB: mydatabase
* networks
: Chỉ định container tham gia vào mạng nào đã được định nghĩa ở top-level `networks`. Nếu không chỉ định, container sẽ tham gia vào mạng mặc định của ứng dụng Compose.
* depends_on
: Chỉ định sự phụ thuộc giữa các service. Compose sẽ đảm bảo các service trong danh sách này được khởi động *trước* service hiện tại. Lưu ý rằng `depends_on` chỉ đảm bảo thứ tự khởi động, không đảm bảo service phụ thuộc đã *sẵn sàng* (ví dụ: database đã lắng nghe kết nối). Đối với sự sẵn sàng, nên kết hợp với Health checks.
services:
app:
# ...
depends_on:
- db # App sẽ khởi động sau db
* healthcheck
: Định nghĩa cách kiểm tra xem container có đang hoạt động và sẵn sàng xử lý yêu cầu hay không. Rất hữu ích khi kết hợp với `depends_on` trong các phiên bản Compose mới hơn.
services:
db:
image: postgres:13
# ...
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 3
Ví Dụ File docker-compose.yml Đầy Đủ
Đây là một ví dụ về file Compose cho một ứng dụng web đơn giản bao gồm một frontend tĩnh (Nginx), một backend API (ví dụ: Python/Flask), và một database (PostgreSQL).
version: '3.8'
services:
web:
image: nginx:latest
volumes:
- ./frontend:/usr/share/nginx/html:ro # Gắn source code frontend tĩnh vào Nginx, chỉ đọc
ports:
- "80:80" # Public port cho frontend
networks:
- app_network
depends_on:
- app # Frontend có thể cần chờ backend khởi động (tùy kiến trúc)
app:
build: ./backend # Xây dựng từ Dockerfile trong thư mục ./backend
ports:
- "5000:5000" # Expose port 5000 nội bộ container
volumes:
- ./backend:/app # Bind mount source code backend cho phát triển
environment:
DATABASE_URL: postgresql://user:password@db:5432/mydatabase
PYTHONUNBUFFERED: 1 # Tùy chọn cho Python để hiển thị log ngay lập tức
networks:
- app_network
depends_on:
- db # Backend phụ thuộc vào database
db:
image: postgres:13
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydatabase
volumes:
- db_data:/var/lib/postgresql/data # Named volume cho dữ liệu database
networks:
- app_network
healthcheck: # Kiểm tra sức khỏe database
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s # Thời gian chờ ban đầu trước khi bắt đầu kiểm tra
volumes:
db_data: # Khai báo named volume
networks:
app_network: # Khai báo mạng bridge tùy chỉnh
driver: bridge
Trong ví dụ này, chúng ta đã định nghĩa 3 services: `web`, `app`, và `db`.
* Chúng tham gia vào cùng một mạng `app_network`.
* `app` phụ thuộc vào `db`, và `web` phụ thuộc vào `app` (về mặt thứ tự khởi động).
* Dữ liệu của database được lưu trữ bền vững bằng `db_data` named volume.
* Source code frontend và backend được bind mount vào các container tương ứng, giúp dễ dàng chỉnh sửa code trên host và thấy thay đổi (cần cơ chế reload trong ứng dụng).
* Sử dụng biến môi trường để truyền thông tin kết nối database vào service `app`.
* Định nghĩa healthcheck cho service `db`.
Các Lệnh Docker Compose Phổ Biến
Sau khi có file `docker-compose.yml`, việc quản lý ứng dụng trở nên rất đơn giản với các lệnh `docker compose`. Lưu ý: các phiên bản Docker Desktop và Docker Engine gần đây đã tích hợp Compose, sử dụng lệnh `docker compose` (không có dấu gạch ngang). Các phiên bản cũ hơn sử dụng công cụ độc lập `docker-compose` (có dấu gạch ngang). Chúng ta sẽ sử dụng cú pháp mới `docker compose`.
Giả sử file `docker-compose.yml` của bạn nằm trong thư mục hiện tại:
* Khởi động toàn bộ ứng dụng:
docker compose up
Lệnh này sẽ tìm file `docker-compose.yml` trong thư mục hiện tại, xây dựng (nếu cần) và khởi động tất cả các service đã định nghĩa. Output của tất cả các container sẽ hiển thị trực tiếp trên terminal của bạn.
* Khởi động ứng dụng ở chế độ detached (chạy nền):
docker compose up -d
Thêm cờ `-d` (detached) để chạy các container dưới nền, trả lại quyền điều khiển terminal cho bạn.
* Khởi động lại và xây dựng lại image (nếu source code thay đổi):
docker compose up --build
Lệnh này buộc Compose phải xây dựng lại các image từ Dockerfile (nếu có service sử dụng `build`) trước khi khởi động hoặc cập nhật các service. Rất hữu ích khi bạn thay đổi source code ứng dụng.
* Dừng và xóa toàn bộ ứng dụng:
docker compose down
Lệnh này sẽ dừng và xóa tất cả các container, network mặc định đã tạo bởi `docker compose up`. Mặc định, các named volume sẽ *không* bị xóa để bảo vệ dữ liệu.
* Dừng, xóa và cả volume:
docker compose down --volumes
Sử dụng cờ `–volumes` nếu bạn muốn xóa cả các named volume. Hãy cẩn thận với lệnh này vì nó sẽ xóa vĩnh viễn dữ liệu trong volume!
* Xem trạng thái các service:
docker compose ps
Liệt kê các service đang chạy, trạng thái của chúng, port mapping, v.v.
* Xem log của tất cả hoặc từng service:
docker compose logs
Xem log của tất cả các service.
docker compose logs app
Xem log riêng của service `app`.
docker compose logs -f app
Xem log của service `app` theo thời gian thực (follow).
* Chạy một lệnh trong container của service:
docker compose exec app bash
Mở một phiên bash trong container của service `app`. Rất hữu ích để gỡ lỗi hoặc kiểm tra môi trường bên trong container.
* Chỉ xây dựng image mà không chạy container:
docker compose build
Lệnh này chỉ thực hiện bước xây dựng image cho các service có `build` directive trong file Compose, mà không khởi động container nào.
So Sánh: docker run vs. docker compose
Để làm rõ hơn vai trò của Docker Compose, hãy so sánh nó với việc quản lý container bằng lệnh `docker run` đơn lẻ:
Tính năng | docker run (cho từng container) | docker compose (cho ứng dụng đa container) |
---|---|---|
Phạm vi | Quản lý một container độc lập | Quản lý một ứng dụng gồm nhiều container liên kết |
Cấu hình | Sử dụng các cờ (flags) trên dòng lệnh (dài và phức tạp khi nhiều tùy chọn) | Sử dụng file YAML docker-compose.yml (khai báo, dễ đọc, dễ quản lý) |
Networking | Mặc định cách ly hoặc cần cấu hình liên kết (--link – deprecated, hoặc mạng do người dùng tạo) |
Tự động tạo mạng mặc định, các service giao tiếp qua tên service, dễ dàng cấu hình mạng tùy chỉnh |
Volumes | Cấu hình từng bind mount/volume riêng lẻ bằng cờ -v |
Khai báo tập trung trong file cấu hình, dễ dàng gắn vào nhiều service |
Dependencies | Không có cơ chế tích hợp sẵn để đảm bảo thứ tự khởi động hoặc sự sẵn sàng của các service | Sử dụng depends_on (cho thứ tự khởi động) và Health checks (cho sự sẵn sàng) |
Khởi động/Dừng | Chạy từng lệnh docker run , docker stop , docker rm riêng lẻ cho mỗi container |
Một lệnh duy nhất (docker compose up , docker compose down ) để quản lý toàn bộ ứng dụng như một đơn vị |
Độ lặp lại | Khó đảm bảo cấu hình chính xác và lặp lại trên các môi trường khác nhau do phụ thuộc vào việc gõ lệnh thủ công | Cấu hình được định nghĩa rõ ràng trong file, đảm bảo môi trường nhất quán và lặp lại trên mọi máy tính có Docker và Compose |
Rõ ràng, đối với các ứng dụng có nhiều hơn một service, Docker Compose mang lại hiệu quả và tính lặp lại vượt trội. Nó biến việc thiết lập môi trường phức tạp thành một tác vụ đơn giản chỉ với một file cấu hình và một vài lệnh cơ bản.
Các Khái Niệm Nâng Cao & Thực Tiễn Tốt Nhất
Khi đã quen với các khái niệm cơ bản, bạn có thể khám phá thêm một số tính năng và thực tiễn tốt nhất:
Sử Dụng Nhiều File Compose
Đôi khi, bạn muốn có các cấu hình khác nhau cho các môi trường khác nhau (ví dụ: phát triển, kiểm thử, staging). Docker Compose cho phép bạn tách cấu hình thành nhiều file và kế thừa chúng.
Ví dụ, bạn có file `docker-compose.yml` chứa cấu hình cơ bản và một file `docker-compose.override.yml` chứa các tùy chỉnh cho môi trường phát triển (ví dụ: bind mount source code, mở thêm port debug).
File `docker-compose.override.yml` (Compose sẽ tự động load file này nếu nó tồn tại trong cùng thư mục):
version: '3.8'
services:
app:
ports:
- "9229:9229" # Mở port debug
volumes:
- ./backend:/app # Ghi đè volume bind mount cho dev
environment: # Thêm biến môi trường cho dev
DEBUG: "true"
command: ["nodemon", "index.js"] # Ghi đè lệnh chạy (ví dụ: dùng nodemon cho auto-reload)
Khi bạn chạy `docker compose up`, Compose sẽ tự động hợp nhất (merge) cấu hình từ `docker-compose.yml` và `docker-compose.override.yml`. Các giá trị trong file override sẽ ghi đè lên các giá trị trong file gốc.
Bạn cũng có thể chỉ định rõ các file cần sử dụng bằng cờ `-f`:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Lệnh này sẽ sử dụng `docker-compose.yml` làm nền và áp dụng các ghi đè từ `docker-compose.prod.yml`.
Quản Lý Biến Môi Trường
Bạn nên tránh hardcode các giá trị nhạy cảm (mật khẩu database, API keys) hoặc các giá trị thay đổi giữa các môi trường vào file `docker-compose.yml`. Thay vào đó, sử dụng biến môi trường.
Compose có thể đọc biến môi trường từ shell của bạn hoặc từ một file `.env` nằm cùng cấp với file `docker-compose.yml`.
File `.env`:
DATABASE_USER=myuser
DATABASE_PASSWORD=mypassword
DATABASE_NAME=myapp_db
APP_PORT=5000
File `docker-compose.yml`:
version: '3.8'
services:
app:
build: ./backend
ports:
- "${APP_PORT}:${APP_PORT}" # Sử dụng biến môi trường APP_PORT
environment:
DATABASE_URL: postgresql://${DATABASE_USER}:${DATABASE_PASSWORD}@db:5432/${DATABASE_NAME} # Sử dụng biến môi trường
Khi chạy `docker compose up`, Compose sẽ tự động đọc các biến từ file `.env` và thay thế các giá trị `${VARIABLE_NAME}` trong file Compose.
Health Checks
Như đã đề cập, `depends_on` chỉ đảm bảo thứ tự khởi động. Health checks (`healthcheck` directive) giúp Compose hiểu khi nào một service thực sự sẵn sàng. Compose *có thể* sử dụng thông tin healthcheck để đợi service phụ thuộc sẵn sàng trước khi khởi động service chính (cần cấu hình thêm hoặc sử dụng cú pháp `depends_on` mới với condition). Tuy nhiên, giá trị chính của healthcheck là cung cấp thông tin trạng thái rõ ràng khi bạn dùng `docker compose ps`.
Resource Limits
Trong file Compose, bạn có thể đặt giới hạn về CPU và bộ nhớ cho từng service bằng cách sử dụng `deploy.resources.limits`. Điều này giúp ngăn chặn một container sử dụng quá nhiều tài nguyên trên máy host.
services:
app:
# ...
deploy: # Directive cho Swarm/Kubernetes, nhưng cũng áp dụng cho resource limits trong Compose V3+
resources:
limits:
cpus: '0.5' # Tối đa 0.5 core CPU
memory: 512M # Tối đa 512MB RAM
Logging
Bạn có thể cấu hình driver ghi log và các tùy chọn cho từng service hoặc toàn bộ ứng dụng bằng directive `logging`.
services:
app:
# ...
logging:
driver: "json-file" # driver mặc định
options:
max-size: "10m"
max-file: "3"
Hoặc cấu hình logging ở cấp ứng dụng (áp dụng cho tất cả services trừ khi bị ghi đè):
version: '3.8'
services:
# ... services here ...
x-logging: &default-logging # Sử dụng Anchor/Alias trong YAML để tái sử dụng cấu hình
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
services:
web:
# ...
logging: *default-logging
app:
# ...
logging: *default-logging
db:
# ...
logging:
driver: "none" # Không ghi log cho DB (ví dụ)
Gỡ Lỗi Với Docker Compose
Khi làm việc với Compose, bạn có thể gặp lỗi. Dưới đây là một số mẹo gỡ lỗi hữu ích:
* Kiểm tra log: Lệnh `docker compose logs` là công cụ số 1. Xem log của tất cả service hoặc tập trung vào service gây lỗi.
* Xem trạng thái: Lệnh `docker compose ps` cho biết container nào đang chạy, container nào bị lỗi (exit code), port mapping, v.v.
* Shell vào container: Sử dụng `docker compose exec
* Xây dựng lại: Nếu bạn thay đổi Dockerfile hoặc source code, đảm bảo chạy `docker compose up –build` hoặc `docker compose build` trước khi chạy `up`.
* Kiểm tra cấu hình: Sử dụng `docker compose config` để kiểm tra file cấu hình Compose cuối cùng sau khi áp dụng các file override và biến môi trường. Lệnh này giúp phát hiện lỗi cú pháp YAML hoặc vấn đề trong việc phân giải biến.
* Kiểm tra dependencies và health checks: Nếu một service không khởi động, hãy kiểm tra service mà nó phụ thuộc (`depends_on`) xem service đó đã chạy và pass health check chưa (nếu có health check).
Kết Luận
Docker Compose là một công cụ không thể thiếu trong bộ công cụ của bất kỳ DevOps Engineer nào, đặc biệt là trong môi trường phát triển và kiểm thử. Nó đơn giản hóa đáng kể việc định nghĩa, cấu hình và quản lý các ứng dụng gồm nhiều container tương tác.
Bằng cách sử dụng một file YAML khai báo duy nhất, Compose giúp tạo ra môi trường làm việc nhất quán, dễ dàng chia sẻ và lặp lại trên các máy tính khác nhau. Nó loại bỏ sự phức tạp của việc gõ lệnh `docker run` dài dòng và tập trung vào định nghĩa kiến trúc ứng dụng của bạn.
Mặc dù Compose rất mạnh mẽ cho môi trường đơn máy (local machine, server đơn lẻ), nó không phải là một giải pháp orchestrator đầy đủ cho môi trường production ở quy mô lớn. Đối với production, bạn sẽ cần tìm hiểu về các nền tảng orchestration như Docker Swarm hoặc Kubernetes, những công cụ này được xây dựng dựa trên các nguyên tắc tương tự nhưng cung cấp khả năng quản lý cluster, scaling tự động, rolling updates và nhiều tính năng nâng cao khác. Nhưng trước khi đến với những công cụ phức tạp đó, nắm vững Docker Compose là một bước đệm cực kỳ vững chắc.
Tiếp theo trên Roadmap Docker, chúng ta sẽ khám phá cách sử dụng Dockerfile hiệu quả hơn và các kỹ thuật tối ưu hóa image. Hãy tiếp tục theo dõi!