Hình dung một quản lý sân khấu cầm kịch bản. Gõ docker run từng cái một là bạn đứng hô từng động tác cho diễn viên mỗi suất diễn. Compose thì như đưa cả kịch bản mô tả sân khấu phải trông thế nào: trước mỗi suất, người quản lý soi cảnh hiện tại, so với kịch bản, và chỉ dời đúng thứ đang lệch. Diễn lại y hệt kịch bản mà sân khấu đã đúng thì chẳng ai phải nhấc món đạo cụ nào; đổi một cảnh thì chỉ cảnh đó được dựng lại. Bốn mươi tám phần vừa rồi đều gõ docker run với một dãy cờ dài — Compose gom dãy đó vào một tệp, và làm thêm vài việc mà docker run không làm.
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: bimat
volumes:
- dulieu:/var/lib/postgresql/data
web:
build: .
ports:
- "127.0.0.1:18500:8000"
depends_on:
- db
volumes:
dulieu:
Một lệnh, bốn thứ được tạo
docker compose -p thulab up -d --build # 1,5 giay
| Loại | Tên |
|---|---|
| Container | thulab-db-1, thulab-web-1 |
| Mạng | thulab_default (bridge riêng) |
| Volume | thulab_dulieu |
| Nhãn | com.docker.compose.* trên mọi container |
Cái tên thulab là tên project, mặc định lấy từ tên thư mục. Mọi thứ Compose tạo ra đều mang tiền tố đó, và đó cũng là cách nó tìm lại đồ của mình ở lần chạy sau.
Mạng riêng cho mỗi project là thứ quan trọng nhất mà bạn được miễn phí: web gọi db theo tên, đúng như phần 34 đã đo, và hai project khác nhau không thấy nhau (phần 38).
Chú ý cả ports: "127.0.0.1:18500:8000" — bind vào loopback thay vì 0.0.0.0, vì phần 35 đã đo được rằng tường lửa không cứu bạn ở chỗ này.
Nhãn config-hash quyết định lần up sau
Trong đống nhãn Compose gắn vào, một cái đáng để ý:
com.docker.compose.config-hash = 38bfeebc26a6f8b55...
com.docker.compose.project = thulab
com.docker.compose.depends_on = db:service_started:false
com.docker.compose.container-number = 1
Compose băm cấu hình của từng dịch vụ rồi lưu vào container. Lần up sau, nó băm lại và so. Đo được:
| Lệnh | Thời gian | Compose làm gì |
|---|---|---|
up -d khi không đổi gì |
0,17 s | cả hai báo Running, không đụng gì |
up -d sau khi đổi 1 biến của db |
0,5 s | chỉ db được tạo lại, web vẫn Running |
Đây là điểm khác biệt lớn nhất so với gõ docker run bằng tay: Compose so sánh trạng thái mong muốn với trạng thái đang có, và chỉ đụng vào phần lệch. Chạy up -d nhiều lần liên tiếp là an toàn và gần như miễn phí.
Dòng depends_on = db:service_started:false cũng đáng nhớ: nó ghi rõ điều kiện mặc định là service_started, và phần sau sẽ đo xem điều kiện đó bảo đảm được những gì (rất ít).
down và down -v khác nhau ở đúng một chỗ, và chỗ đó là dữ liệu
Tôi tạo một bảng trong CSDL rồi thử cả hai:
| Lệnh | Thời gian | Container | Mạng | Volume |
|---|---|---|---|---|
down |
1,7 s | xoá | xoá | giữ |
down -v |
1,7 s | xoá | xoá | xoá |
Sau down rồi up lại, bảng cũ vẫn còn — đúng như thiết kế. Sau down -v, volume biến mất và Compose không hỏi lại một câu nào.
Đây là lệnh nguy hiểm nhất trong toàn bộ Compose. Nó nằm cách một phím so với lệnh an toàn, và trên máy phát triển bạn gõ nó hằng ngày để làm sạch — thói quen đó theo bạn sang máy chủ thật.
Nếu muốn chắc, hãy xem trước cái gì sẽ mất:
docker volume ls --filter name=<ten-project>
Lệnh up trả về không có nghĩa là dịch vụ đã chạy
Đo từ bên ngoài, tính từ lúc bắt đầu gõ lệnh:
| Mốc | Thời điểm |
|---|---|
Lệnh up -d trả về |
0,50 s |
| Web trả lời HTTP 200 | 1,10 s |
| PostgreSQL thật sự sẵn sàng | 1,17 s |
Lệnh trả về sớm hơn dịch vụ sẵn sàng khoảng 0,6 giây trong ví dụ nhỏ này. Với hệ thống thật, khoảng đó tính bằng chục giây — và ứng dụng của tôi ghi lại rằng nó đã thử kết nối tới CSDL 4 lần thất bại trước khi được.
Con số đó là chủ đề của phần sau. Ở đây chỉ cần nhớ: up -d xong không phải là "chạy được rồi". Mọi script CI kiểu docker compose up -d && ./chay-test.sh đều có một cuộc đua ẩn ở dấu &&.
Vài quy ước nên theo từ đầu
Đặt tên project tường minh. Mặc định lấy theo tên thư mục, nên đổi tên thư mục là Compose mất dấu toàn bộ container và volume cũ:
docker compose -p ten-ro-rang up -d
# hoac dat trong tep: name: ten-ro-rang
Tên tệp là compose.yaml. Đây là tên chuẩn hiện tại; docker-compose.yml vẫn chạy nhưng là tên cũ. Và dòng version: ở đầu tệp đã bị bỏ — Compose hiện tại còn cảnh báo nếu bạn để nó lại.
Volume phải có tên. Khai trong mục volumes: như ví dụ trên. Nếu bạn viết - /var/lib/postgresql/data không kèm tên, bạn được một volume ẩn danh với cái tên băm dài, và phần 41 đã chỉ ra nó sẽ thành rác không ai dám xoá.
Muốn thấy một hệ Compose đang chạy thật sự gồm những gì, liệt kê container, mạng, volume và nhãn:
docker compose ps
docker network ls --filter name=$(basename "$PWD")
docker volume ls --filter name=$(basename "$PWD")
docker inspect $(docker compose ps -q | head -1) \
--format '{{range $k,$v := .Config.Labels}}{{$k}}={{$v}}{{println}}{{end}}' | grep compose
Nếu docker volume ls in ra những cái tên bạn không nhận ra, đó là volume ẩn danh — và chúng đang giữ dữ liệu mà down -v sẽ xoá sạch trong 1,7 giây.
Mẫu số chung
Bài học đầu tiên, đọc thẳng từ "up lại mất 0,17 giây và không đụng gì": mô tả trạng thái đích rồi để công cụ tự tính phần chênh (khai báo) mạnh hơn hẳn ra lệnh từng bước (mệnh lệnh) cho mọi thứ chạy lặp lại — bạn được tính lũy đẳng và chỉ-sửa-phần-lệch miễn phí. config-hash chính là cái so- sánh đó: Compose băm cấu hình mong muốn, đối chiếu với cái đang chạy, và chỉ tạo lại dịch vụ nào khác. Cùng bước nhảy "khai báo thắng mệnh lệnh" ở khắp nơi: Terraform/IaC mô tả hạ tầng đích, Kubernetes hội tụ về trạng thái mong muốn, React so cây ảo rồi vá đúng chỗ đổi, make so đích với nguồn, migration lũy đẳng chạy lại được. Nguyên tắc: hãy mô tả cái mình muốn có, đừng liệt kê các bước để tới đó — vì một mô tả trạng thái chạy lại bao nhiêu lần cũng an toàn, còn một chuỗi lệnh thì lần thứ hai hoặc nổ hoặc làm thừa.
Điều thứ hai, đọc từ down so với down -v: một thao tác không thể hoàn tác nằm cách một thao tác an toàn đúng một phím, và chẳng hỏi lại câu nào — mà thói quen hình thành trên máy dev thì đi theo bạn sang máy chủ thật. down -v xoá sạch volume trong 1,7 giây không một lời xác nhận; bạn gõ nó hằng ngày để dọn máy, rồi một hôm gõ nó ở nhầm chỗ. Cùng cái bẫy "huỷ diệt sát ngay cạnh an toàn" ở khắp nơi: rm -rf cạnh rm, DROP TABLE cạnh DELETE ... WHERE, push --force, một nút "reset tất cả" không xác nhận. Nguyên tắc: rào cái không-hoàn-tác lại cho khó chạm nhầm, và luôn xem trước cái gì sẽ mất trước khi bấm (docker volume ls trước down -v) — vì trí nhớ cơ bắp không phân biệt dev với prod, và cái giá của một phím gõ theo quán tính ở prod là dữ liệu thật.
Phần sau đo depends_on: nó bảo đảm điều gì, và cái gì thì không.