Chạy PostgreSQL bằng Docker là một dòng lệnh:

docker run -d --name pg -e POSTGRES_PASSWORD=x \
  -v pgdata:/var/lib/postgresql/data postgres:16-alpine

Nhưng lần khởi động đầu tiên làm nhiều việc hơn bạn tưởng, và một trong số đó khiến healthcheck phổ biến nhất có một khoảng báo sai.

Đường thời gian lần khởi động đầu

Lần đầu có hai máy chủ, không phải một

Tôi lấy mốc thời gian từ chính log của container thay vì đo bằng vòng lặp bên ngoài — đây là lý do ở mục cuối bài.

51.174  creating subdirectories ... ok
51.276  running bootstrap script ... ok
51.511  performing post-bootstrap initialization ... ok
51.551  syncing data to disk ... ok
51.572  [PID 53] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
51.577  [PID 53] LOG: database system is ready to accept connections
51.672  entrypoint: ignoring /docker-entrypoint-initdb.d/*
51.785  [PID  1] LOG: listening on IPv4 address "0.0.0.0", port 5432

Để ý PID. Máy chủ nói "ready to accept connections" ở giây .577PID 53 — một máy chủ tạm mà entrypoint dựng lên để chạy initdb, tạo người dùng, và chạy các script trong /docker-entrypoint-initdb.d/. Nó chỉ nghe Unix socket, cố ý không mở TCP.

Xong việc, entrypoint tắt nó đi:

LOG: aborting any active transactions
LOG: shutting down
LOG: checkpoint starting: shutdown immediate

Rồi mới khởi động máy chủ thật — PID 1 — và đó mới là cái nghe trên 0.0.0.0:5432.

Khoảng 208 mili giây báo sai

Giữa .577.785 có một cửa sổ 208 ms trong đó:

  • pg_isready qua Unix socket trả về OK
  • Cổng TCP 5432 chưa mở
  • Và máy chủ đang trả lời đó sắp bị tắt

Healthcheck mà mọi người hay dán vào Compose:

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres"]

Không có -h, pg_isready dùng Unix socket. Trong cửa sổ đó nó báo khoẻ cho một máy chủ sắp biến mất. Dịch vụ nào chờ condition: service_healthy rồi kết nối ngay có thể vấp đúng khoảnh khắc này.

Sửa bằng một tham số:

test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres"]

Qua TCP thì máy chủ tạm không trả lời được, nên healthcheck chỉ xanh khi máy chủ thật đã lên.

Hai trăm mili giây nghe không đáng kể — trên máy tôi thì đúng vậy. Nhưng cửa sổ này dài bằng thời gian initdb chạy: trên đĩa chậm, hoặc khi bạn có script khởi tạo nạp dữ liệu mẫu trong /docker-entrypoint-initdb.d/, nó thành hàng chục giây. Và trong suốt khoảng đó, healthcheck của bạn nói mọi thứ ổn.

initdb để lại những gì

dung luong: 38,3 MB
so tep    : 969
Thư mục Dung lượng
base 22,1 MB
pg_wal 16,4 MB
global 544 KB

Và ba cơ sở dữ liệu:

postgres  (7519 kB)
template0 (7361 kB)
template1 (7361 kB)

template1 là khuôn: mọi CREATE DATABASE đều sao chép từ nó. Cài extension vào template1 là mọi CSDL tạo sau đều có sẵn. template0 là bản dự phòng không được sửa, dùng khi bạn cần một CSDL sạch hoàn toàn.

pg_wal chiếm 16,4 MB ngay từ đầu vì PostgreSQL cấp phát trước các tệp WAL — chúng được tái sử dụng chứ không tạo mới liên tục. Phần 46 của sê-ri sẽ đo kỹ chuyện này.

Lần thứ hai nhanh hơn gần bốn lần

Lần khởi động Thời gian tới khi nhận kết nối TCP
Volume rỗng, phải chạy initdb 1,09 s
Đã có dữ liệu, lần trước dừng tử tế 0,28 s

Và log lần hai xác nhận nó bỏ qua toàn bộ phần khởi tạo:

PostgreSQL Database directory appears to contain a database; Skipping initialization
LOG: database system was shut down at 2026-08-27 10:17:14 UTC
LOG: database system is ready to accept connections

Dòng database system was shut down at ... là dấu hiệu tốt: PostgreSQL biết lần trước nó dừng sạch nên không cần phục hồi.

Image postgres dừng đúng cách

Đây là chỗ image chính thức làm tốt và đáng ghi nhận:

STOPSIGNAL cua image: SIGINT
docker stop mat: 0,12 s
LOG: shutting down
LOG: checkpoint starting: shutdown immediate
LOG: database system is shut down

PostgreSQL hiểu SIGTERM là "dừng thông minh" — chờ mọi client tự ngắt, có thể chờ mãi. Image khai STOPSIGNAL SIGINT để đổi sang "dừng nhanh": ngắt client, checkpoint, thoát. Nhờ vậy docker stop xong trong 0,12 giây với một checkpoint sạch, thay vì bị SIGKILL sau 10 giây và phải phục hồi ở lần sau.

Nếu bạn tự dựng image PostgreSQL, đừng quên dòng đó.

Cấu hình tối thiểu nên có

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD:?can dat mat khau}
      POSTGRES_DB: ung_dung
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s
    restart: unless-stopped
volumes:
  pgdata:

Ba điểm đáng nói:

Volume phải có tên. Không khai thì bạn được một volume ẩn danh, và dữ liệu CSDL nằm sau một cái tên băm mà docker volume prune sẽ xoá.

POSTGRES_PASSWORD chỉ dùng ở lần đầu. Sau khi initdb chạy xong, đổi biến đó không có tác dụng gì — mật khẩu đã nằm trong volume. Muốn đổi thì ALTER USER.

Đừng publish cổng 5432 ra 0.0.0.0. Nếu ứng dụng chạy cùng Compose, nó gọi db:5432 theo tên dịch vụ và bạn không cần ports: gì cả.

Vì sao tôi đọc log thay vì bấm giờ từ ngoài

Lần đầu tôi đo cửa sổ 208 ms bằng một vòng lặp gọi pg_isready liên tục qua docker exec. Kết quả ra âm: TCP báo sẵn sàng trước socket.

Nguyên nhân: mỗi lần docker exec tốn khoảng 50–100 ms, và tôi kiểm socket trước rồi mới kiểm TCP trong cùng một vòng lặp. Công cụ đo tốn nhiều thời gian hơn chính khoảng cần đo.

Log của PostgreSQL có mốc tới mili giây và do chính máy chủ ghi ra, nên nó không có vấn đề đó. Bài học: khi khoảng cần đo nhỏ hơn chi phí của phép đo, hãy tìm một nguồn thời gian nằm bên trong thứ đang đo.

Thử ba mươi giây

Xem lần khởi động đầu của bạn có hai máy chủ không:

docker rm -f pgthu; docker volume rm pgthu
docker run -d --name pgthu -e POSTGRES_PASSWORD=x \
  -v pgthu:/var/lib/postgresql/data postgres:16-alpine
sleep 5
docker logs pgthu 2>&1 | grep -E 'listening on|ready to accept|shutting down'

Bạn sẽ thấy hai lần "ready to accept connections" với hai PID khác nhau, và một lần "shutting down" ở giữa. Nếu healthcheck của bạn không có -h 127.0.0.1, nó có thể bắt được cái đầu tiên.

Phần sau bắt đầu vào kiểu dữ liệu: chọn đúng ngay từ đầu tốn bao nhiêu và tiết kiệm bao nhiêu.