Bạn chạy docker compose up, database và API cùng khởi động, nhưng API báo không kết nối được. Chạy lại một lúc sau thì mọi thứ bình thường. Đây thường là khác biệt giữa tiến trình container đã được tạo và dịch vụ bên trong đã sẵn sàng nhận công việc.
Bài viết dành cho môi trường phát triển với Docker Compose plugin v2. Ví dụ chọn PostgreSQL 17 và Python 3.12 làm các image minh họa, không khẳng định đó là phiên bản mới nhất. Tài liệu được đối chiếu ngày 22/10/2026; cấu hình cần được chạy kiểm tra trong môi trường của bạn trước khi dùng cho hệ thống thật.
Ba điều kiện phục vụ ba mục đích
depends_on dạng danh sách thể hiện quan hệ khởi động, nhưng không tự chờ database sẵn sàng. Dạng đầy đủ cho phép chọn điều kiện tương ứng:
service_started: dịch vụ phụ thuộc đã được khởi động.service_healthy: healthcheck của dịch vụ phụ thuộc đã đạt.service_completed_successfully: một tác vụ như migration đã kết thúc thành công.
Hãy định nghĩa thứ ứng dụng thực sự cần. Nếu ứng dụng cần schema đã được tạo, kiểm tra cổng database mở vẫn chưa đủ. Nếu chỉ muốn chứng minh chuỗi khởi động, có thể dùng một ứng dụng demo nhỏ rồi thay nó bằng image của dự án.
Một ví dụ cục bộ với bước tạo schema
Lưu nội dung sau thành compose.yaml. HTTP server chỉ minh họa bước cuối sau migration; nó không phải API có truy vấn PostgreSQL.
services:
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: demo
POSTGRES_DB: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}
volumes:
- demo_pg:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -h 127.0.0.1 -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"']
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
migrate:
image: postgres:17-alpine
environment:
PGPASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}
depends_on:
db:
condition: service_healthy
command:
- psql
- -h
- db
- -U
- demo
- -d
- app
- -v
- ON_ERROR_STOP=1
- -c
- CREATE TABLE IF NOT EXISTS startup_probe (id integer PRIMARY KEY);
web:
image: python:3.12-alpine
working_dir: /tmp
command: ['python', '-m', 'http.server', '8000']
ports:
- '127.0.0.1:8080:8000'
depends_on:
migrate:
condition: service_completed_successfully
volumes:
demo_pg:
pg_isready kiểm tra trạng thái nhận kết nối của PostgreSQL. Nó không chứng minh tài khoản ứng dụng đã có mọi quyền hoặc schema nghiệp vụ đã đúng. Bước migrate chạy một câu SQL thật với ON_ERROR_STOP=1, vì vậy lỗi SQL phải làm tác vụ thất bại thay vì cho bước sau tiếp tục như thể đã xong.
Dấu $$ giữ việc mở rộng biến cho shell bên trong container. Nếu dùng một dấu $ không đúng chỗ, Compose có thể thay giá trị từ môi trường máy chủ trước khi healthcheck chạy. Hai môi trường đó không nhất thiết chứa cùng giá trị.
Chạy và quan sát đúng từng lớp
Đặt POSTGRES_PASSWORD bằng một giá trị riêng cho thử nghiệm cục bộ, không commit vào repository và không dùng lại mật khẩu production. Kiểm tra phiên bản plugin và cú pháp trước khi khởi động:
docker compose version
docker compose config --quiet
docker compose up -d
docker compose ps -a
docker compose logs migrate
docker compose exec db psql -U demo -d app -c "SELECT to_regclass('public.startup_probe');"
curl http://127.0.0.1:8080
ps -a giúp bạn thấy cả tác vụ đã thoát như migration. Đừng xem trạng thái exited của tác vụ một lần là lỗi nếu exit code đúng bằng 0. Ngược lại, database healthy nhưng migration thất bại là dấu hiệu phải đọc log migration, không nên tăng thời gian chờ database theo cảm tính.
Các giá trị 5 giây, 3 giây và 10 lần trong ví dụ là tham số minh họa. Chúng chưa được đo trên workload của bạn. Điều chỉnh dựa trên thời gian khởi động thực tế, loại ổ đĩa và thao tác kiểm tra, thay vì sao chép rồi xem đó là một cam kết thời gian sẵn sàng.
Healthcheck không thay thế phục hồi kết nối
Một database có thể healthy lúc API khởi động rồi mất kết nối sau đó. Ứng dụng vẫn cần timeout, quản lý connection pool và chính sách retry phù hợp. Đừng để một tác vụ ghi nghiệp vụ bị retry vô hạn; nếu có khả năng tạo trùng, cần thiết kế hợp đồng idempotency riêng.
Cũng không nên suy từ depends_on rằng mọi lần container phụ thuộc bị lỗi đều tự động khởi động lại toàn bộ chuỗi. Điều kiện khởi động và hành vi phục hồi trong thời gian chạy là hai việc cần cấu hình, kiểm chứng riêng. Healthcheck phản ánh kết quả phép thử bạn viết, nên một lệnh chỉ kiểm tra “tiến trình tồn tại” có thể bỏ sót lỗi ứng dụng thật.
Lỗi dễ gặp khi sửa theo thói quen
Chèn sleep 30 có thể làm lần thử hiện tại qua được nhưng không gắn với điều kiện sẵn sàng. Tăng timeout kết nối cũng không sửa được sai hostname hoặc mật khẩu. Trong mạng Compose, hostname thường là tên service như db, không phải localhost của container API.
Khi thử lại database có named volume, nhớ rằng dữ liệu và cấu hình khởi tạo ban đầu có thể vẫn còn. Đổi biến khởi tạo không có nghĩa database cũ tự thay mọi tài khoản. Kiểm tra dữ liệu hiện có trước khi quyết định tạo môi trường mới; không xóa volume chỉ để che đi nguyên nhân lỗi.
Kết luận
Hãy dùng một điều kiện có thể quan sát cho mỗi chặng: database nhận kết nối, migration thành công, rồi ứng dụng khởi động. Sau khi chuỗi đó rõ ràng, tiếp tục kiểm chứng khả năng phục hồi khi một dịch vụ mất kết nối trong lúc hệ thống đang chạy.

