Buổi 14 – Production-minded Compose
Tuần 7 · Giai đoạn 2 – Containerization · Slide: Slide buổi 14
Tóm tắt: Nâng compose.yaml từ mức chạy được lên mức đáng tin cậy: healthcheck, thứ tự khởi động, restart, giới hạn tài nguyên, secrets, logging, backup và restore.
Mục tiêu học tập
- Giải thích vì sao
depends_onthường không đủ vàcondition: service_healthygiải quyết điều gì. - Viết được healthcheck cho backend và frontend, chọn đúng
interval,timeout,retries,start_period. - Chọn được restart policy phù hợp và kiểm chứng bằng cách làm một container thoát bất thường.
- Cấu hình được resource limits và log rotation, rồi kiểm tra bằng
docker statsvàdocker inspect. - Chẩn đoán được lỗi "API chạy nhưng database chưa sẵn sàng" từ
docker compose ps,logsvà endpoint/ready. - Thực hiện được backup bằng
pg_dumpvà restore vào một database mới, rồi chứng minh dữ liệu còn nguyên.
Điều kiện cần
- Đã hoàn thành buổi 13: chạy được
starter-projectbằngdocker compose up -d --build. - Docker Desktop (WSL2 backend) đang chạy; làm việc trong filesystem của WSL (ví dụ
~/devops-labs/lab-14), không dùng/mnt/c. - Cổng 8080 (hoặc
WEB_PORTbạn đặt) và 3000 còn trống.
Kiểm tra nhanh:
docker --version
docker compose version
docker psKết quả mong đợi (rút gọn): Docker 27 trở lên, Compose v2 (Docker Compose version v2.x.x), docker ps chạy không báo lỗi.
Chuẩn bị thư mục làm việc:
mkdir -p ~/devops-labs/lab-14 && cd ~/devops-labs/lab-14
cp -r ~/devops-course/starter-project source
cd source
cp .env.example .envNếu bạn clone giáo trình ở đường dẫn khác, sửa lại ~/devops-course (trong WSL). File .env không được commit. Lưu ý: lab-13/source và lab-14/source đều có tên thư mục source nên cùng tên project Compose (source). Hãy chạy docker compose down trong lab-13/source trước khi bắt đầu để hai lab không dẫm lên container và volume của nhau.
Vấn đề thực tế
Một buổi sinh hoạt CLB, bạn demo hệ thống quản lý thành viên trên laptop. Bạn chạy docker compose up -d, mở trình duyệt ngay lập tức và thấy lỗi 500 vì Postgres còn đang khởi tạo. Chạy lại lần hai thì được. Đến tối máy khởi động lại, container API chết và không tự dậy. Vài tuần sau, thư mục log của Docker chiếm hàng GB. Không có lỗi nào trong số này nằm ở code ứng dụng; chúng nằm ở cách vận hành.
Production-minded Compose không có nghĩa là đưa Compose lên production lớn. Nó có nghĩa là tập thói quen: hệ thống tự cho biết khi nào nó sẵn sàng, tự hồi phục khi lỗi vặt, không ăn hết tài nguyên máy, không để lộ mật khẩu và có đường quay lại khi mất dữ liệu. Những thói quen này chuyển nguyên sang Kubernetes ở Tuần 11 (probe, resource requests/limits, Secret, PVC).
Khái niệm cốt lõi
Container "started" khác với service "ready"
Docker chỉ biết tiến trình chính (PID 1) của container có đang chạy hay không. Postgres có thể đã chạy nhưng chưa nhận kết nối vì đang khởi tạo cluster và chạy init.sql. Healthcheck là một lệnh Docker chạy định kỳ bên trong container; exit code 0 là healthy, 1 là unhealthy.
| Trạng thái | Ý nghĩa |
|---|---|
starting | Đang trong start_period hoặc chưa có lần check thành công đầu tiên |
healthy | Lệnh check gần nhất thành công |
unhealthy | Lệnh check thất bại liên tiếp đủ retries lần |
Các tham số của healthcheck:
| Khóa | Ý nghĩa | Gợi ý cho lab |
|---|---|---|
test | Lệnh kiểm tra; dạng ["CMD", ...] hoặc ["CMD-SHELL", "..."] | xem bên dưới |
interval | Khoảng cách giữa hai lần check | 5s đến 10s |
timeout | Quá thời gian này thì tính là một lần thất bại | 3s |
retries | Số lần thất bại liên tiếp trước khi unhealthy | 3 đến 5 |
start_period | Thời gian khởi động: lỗi trong khoảng này chưa bị tính vào retries | 10s đến 30s |
depends_on với condition
| Cú pháp | Compose chờ điều gì |
|---|---|
depends_on: [postgres] | Container postgres đã được tạo/khởi động; không chờ ready |
condition: service_started | Giống dạng ngắn |
condition: service_healthy | Healthcheck của dependency đã healthy |
condition: service_completed_successfully | Dependency (job một lần) đã thoát với exit code 0 |
Lưu ý: depends_on chỉ tác động lúc docker compose up. Nếu Postgres khởi động lại giữa chừng, Compose không khởi động lại API. Vì vậy ứng dụng vẫn phải chịu được việc mất kết nối tạm thời (retry, endpoint /ready).
Restart policy
| Policy | Hành vi |
|---|---|
no | Không tự khởi động lại (mặc định) |
on-failure | Chỉ khởi động lại khi exit code khác 0 (có thể giới hạn số lần: on-failure:3) |
always | Luôn khởi động lại, kể cả sau khi daemon khởi động lại; vẫn chạy lại nếu bạn đã docker stop rồi daemon restart |
unless-stopped | Như always, nhưng nếu bạn chủ động dừng thì không tự dậy lại |
Restart policy không thay thế healthcheck: container unhealthy vẫn chạy, Docker Engine không tự restart nó chỉ vì unhealthy. Restart chỉ xảy ra khi tiến trình thoát.
Resource limits
Không đặt giới hạn thì một container rò bộ nhớ có thể kéo sập cả WSL2. Trong Compose v2, deploy.resources.limits được áp dụng cho docker compose up thông thường:
deploy:
resources:
limits:
cpus: "0.50"
memory: 256MVượt giới hạn CPU thì tiến trình bị làm chậm (throttle); vượt giới hạn memory thì kernel kết thúc tiến trình (OOM kill, exit code 137).
Secrets và cấu hình nhạy cảm
| Cách | Ưu điểm | Nhược điểm |
|---|---|---|
environment ghi cứng trong YAML | Đơn giản | Mật khẩu nằm trong Git; không chấp nhận được |
Biến lấy từ .env | Đủ cho lab, không commit .env | Hiện trong docker inspect và docker compose config |
Compose secrets (file) | Mật khẩu mount thành file /run/secrets/ten_secret, không nằm trong biến môi trường | Ứng dụng phải biết đọc file |
Image postgres hỗ trợ biến POSTGRES_PASSWORD_FILE. Backend của starter-project hiện đọc DB_PASSWORD từ biến môi trường, nên trong buổi này bạn áp dụng secrets cho Postgres và thảo luận cách mở rộng cho backend ở thử thách L2.
Logging
Mặc định driver json-file ghi log không giới hạn. Hãy bật rotation:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"Mỗi container tối đa khoảng 30 MB log. Log ứng dụng nên ghi ra stdout/stderr để docker compose logs đọc được.
Backup và restore
Volume không phải là backup: xóa nhầm volume hoặc hỏng đĩa thì mất hết. Với PostgreSQL, pg_dump tạo bản dump logic (file SQL) nhất quán trong khi database đang chạy; psql nạp lại bản dump đó.
Kiến trúc và cách hoạt động
Chuỗi khởi động sau khi áp dụng bài lab:
Giải thích từng bước:
- Compose tạo network và volume, khởi động
postgrestrước vì cả hai service kia (trực tiếp hoặc gián tiếp) phụ thuộc vào nó. - Docker chạy
pg_isreadymỗi 5 giây (trongcompose.yamlgốc làretries: 5, chưa cóstart_period; bạn có thể thêmstart_periodcho postgres nếu muốn). Lưu ý:pg_isreadycó thể báo sẵn sàng sớm khi server tạm thời của bước khởi tạo đã nhận kết nối qua socket, nên saudown -vhãy chờ vài giây trước khi truy vấn. - Khi
healthy, Compose khởi độngbackend. Backend có healthcheck riêng gọi/health. - Khi backend
healthy, Compose mới khởi độngfrontend, nên người dùng không bao giờ thấy trang web trỏ vào API chưa chạy. - Nếu một container thoát bất thường, restart policy khởi động lại nó; Compose không chạy lại các service phụ thuộc.
Demo của mentor
Mục tiêu: cho lớp thấy sự khác nhau giữa "started" và "ready". Dùng starter-project nguyên bản (chưa sửa).
Mở hai terminal WSL. Ở terminal B, theo dõi trạng thái; ở terminal A, dọn môi trường và chạy lại từ đầu:
bash# Terminal B cd ~/devops-labs/lab-14/source watch -n 1 docker compose psbash# Terminal A cd ~/devops-labs/lab-14/source docker compose down -v docker compose up -d --buildKết quả mong đợi (rút gọn): ở terminal B,
postgreshiệnUp ... (health: starting)trong vài giây đầu,backendvàfrontendchưa xuất hiện (hoặcCreated) vì backend chờ postgres healthy. Lệnhup -dở terminal A cũng chỉ trả về sau khi postgres healthy và backend đã được start. Nếu chạydocker compose psngay sauup -dtrong cùng một terminal, bạn sẽ thấy mọi thứ đãUp, nên cần terminal thứ hai để quan sát khoảng chờ.Câu hỏi cho lớp: "Vì sao backend chưa chạy dù ta không viết sleep ở đâu cả?"
Khi
up -dđã xong, xem lại ở terminal A (dừngwatchbằngCtrl+C):bashdocker compose psKết quả mong đợi:
postgres(healthy),backendvàfrontendUp.Xem cấu hình healthcheck thật sự Docker đang dùng:
bashdocker inspect --format '{{json .State.Health}}' $(docker compose ps -q postgres)Kết quả mong đợi: JSON có
"Status":"healthy"và danh sáchLogcác lần chạypg_isready.Tạo tình huống DB mất giữa chừng, rồi hỏi lớp dự đoán trước khi chạy:
bashdocker compose stop postgres curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/health curl -s -w "\n%{http_code}\n" http://localhost:3000/ready docker compose start postgresKết quả mong đợi:
/healthtrả200(tiến trình sống),/readytrả503với{"status":"not-ready"}. Sau khistart postgresvà chờ healthy,/readytrả200.Điểm chốt: "sống" (liveness) khác "sẵn sàng phục vụ" (readiness). Đây chính là hai loại probe của Kubernetes.
Lưu ý cho mentor:
db.jschưa lắng nghe sự kiệnerrorcủapg.Pool. Nếu lúc postgres dừng, pool đang giữ một kết nối nhàn rỗi, tiến trình Node có thể crash và đượcrestart: unless-stoppeddựng lại (trong thời gian đó/healthcó thể tạm lỗi). Đây là cơ hội để nói về vai trò của restart policy, và là gợi ý cải tiến cho capstone (pool.on("error", ...)).Xem restart policy bằng một lần thoát bất thường thật (không dùng
docker compose kill, vì Docker Engine bản mới coi lệnh kill do người dùng gõ là dừng thủ công và không restart; đã kiểm chứng với 29.x):bashcurl -s http://localhost:3000/api/members > /dev/null docker compose stop postgres sleep 5 docker compose ps -a backend docker compose start postgresKết quả mong đợi: backend
Upvới thời gian chạy vài giây (Node thoát vì pool nhận lỗi kết nối bị ngắt, rồiunless-stoppeddựng lại). Lưu ý:docker compose stop backendlà dừng chủ động nên không tự dậy lại.
Câu hỏi mentor nên hỏi: "Healthcheck của backend nên gọi /health hay /ready? Nếu gọi /ready thì khi DB chết, backend thành unhealthy, frontend có bị ảnh hưởng không?"
Hands-on lab
Thời lượng tham khảo: 60 phút (Phần 1 đến 3 khoảng 30 phút cho L1; Phần 4 đến 6 cùng thử thách L2 khoảng 30 phút).
Chuẩn bị
cd ~/devops-labs/lab-14/source
grep -n "DB_PASSWORD" .env
git status --short 2>/dev/null | head -3
mkdir -p ../evidence
docker compose down -vKết quả mong đợi: .env có dòng DB_PASSWORD=.... Từ đây, mọi lệnh quan trọng hãy ghi vào ../evidence/command-output.txt, ví dụ docker compose ps | tee -a ../evidence/command-output.txt.
Phần 1 – Quan sát healthcheck của postgres (L1)
Mở terminal thứ hai, chạy
watch -n 1 docker compose pstrong~/devops-labs/lab-14/source. Ở terminal chính, chạy hệ thống:bashdocker compose up -d --buildNhấn
Ctrl+Cở terminal thứ hai khipostgresthànhhealthyvàbackendđãUp. Vì sao: bạn thấy trực tiếp khoảng chênh giữa container chạy và service sẵn sàng. (up -dtự chờ postgres healthy rồi mới start backend, nên nếu chỉ chạywatchsau khiup -dkết thúc thì bạn sẽ bỏ lỡ khoảng đó.)Đọc lịch sử check:
bashdocker inspect --format '{{range .State.Health.Log}}{{.ExitCode}} {{.Output}}{{end}}' $(docker compose ps -q postgres)Kết quả mong đợi: các dòng dạng
0 /var/run/postgresql:5432 - accepting connections.Kiểm tra API đọc được dữ liệu:
bashcurl -s http://localhost:3000/api/membersKết quả mong đợi: JSON có Alice và Bob.
Phần 2 – Thêm healthcheck cho backend và frontend (L1)
Image backend (node:22-alpine) và frontend (nginx:alpine) đều có wget của BusyBox, nên không cần cài thêm gì. Dùng 127.0.0.1 thay vì localhost để tránh trường hợp localhost phân giải sang IPv6 trong khi ứng dụng chỉ lắng nghe IPv4.
Sửa compose.yaml, thêm vào service backend:
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:3000/health || exit 1"]
interval: 10s
timeout: 3s
retries: 3
start_period: 10sThêm vào service frontend, đồng thời đổi depends_on của nó:
depends_on:
backend:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:80/ || exit 1"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5sÁp dụng và kiểm tra:
docker compose config --quiet && echo "YAML OK"
docker compose up -d
docker compose psKết quả mong đợi: YAML OK; sau khoảng 15 đến 25 giây cả ba service (healthy). Vì sao config --quiet: bắt lỗi thụt lề và khóa sai trước khi chạy.
Phần 3 – Kiểm chứng restart policy (L1)
curl -s http://localhost:3000/api/members > /dev/null
docker compose stop postgres
sleep 5
docker compose ps -a backend
docker inspect --format '{{.RestartCount}} {{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q backend)
docker compose start postgresKết quả mong đợi: backend Up với thời gian chạy vài giây, đầu ra 1 unless-stopped (hoặc số lần restart tương ứng). Lệnh curl đầu tiên tạo một kết nối nhàn rỗi trong pool; khi postgres dừng, kết nối đó bị ngắt, backend chưa bắt sự kiện error của pool nên Node thoát với mã khác 0 và policy dựng lại container. Lưu ý: Docker Engine bản mới (đã kiểm chứng với 29.x) coi docker compose kill là dừng thủ công, container nằm ở Exited (137) và RestartCount vẫn là 0, nên bài này không dùng kill. Sau đó thử docker compose stop backend, xác nhận nó nằm yên ở Exited, rồi docker compose start backend.
Phần 4 – Resource limits và logging (L2 một phần)
Thêm vào service backend (và tương tự cho frontend, postgres với giá trị bạn tự chọn):
deploy:
resources:
limits:
cpus: "0.50"
memory: 256M
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"docker compose up -d
docker stats --no-stream
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.NanoCpus}} {{.HostConfig.LogConfig.Config}}' $(docker compose ps -q backend)Kết quả mong đợi: cột MEM USAGE / LIMIT của backend hiện giới hạn 256MiB; dòng inspect có dạng 268435456 500000000 map[max-file:3 max-size:10m]. Gợi ý giá trị: 256M cho backend và frontend, 512M cho postgres. Vì sao: giới hạn có hiệu lực thật chứ không chỉ nằm trong file YAML.
Phần 5 – Secrets cho Postgres (L2 một phần)
Tạo file secret (không commit) và sửa service postgres:
mkdir -p secrets
printf '%s' "$(grep '^DB_PASSWORD=' .env | cut -d= -f2- | tr -d '\r')" > secrets/db_password.txt
echo "secrets/" >> .gitignoreTrong service postgres, thay dòng POSTGRES_PASSWORD: ... bằng POSTGRES_PASSWORD_FILE (xóa hẳn POSTGRES_PASSWORD, vì image postgres từ chối khởi động khi đặt cả hai):
postgres:
environment:
POSTGRES_DB: clubdb
POSTGRES_USER: club
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_passwordỞ cuối file compose.yaml thêm khối cấp cao nhất:
secrets:
db_password:
file: ./secrets/db_password.txtChú ý: mật khẩu chỉ được áp dụng khi volume còn trống. Nếu volume đã có dữ liệu, Postgres giữ mật khẩu cũ. Vì mật khẩu trong file giống hệt DB_PASSWORD, backend vẫn kết nối được; hãy kiểm chứng:
docker compose up -d
docker compose exec postgres sh -c 'ls -l /run/secrets && echo "POSTGRES_PASSWORD=[${POSTGRES_PASSWORD}]"'
curl -s http://localhost:3000/api/members | head -c 120Kết quả mong đợi: file db_password tồn tại, POSTGRES_PASSWORD=[] (biến rỗng, mật khẩu không nằm trong env), API vẫn trả dữ liệu.
Phần 6 – Backup và restore (L2 một phần)
Thêm một thành viên để có dữ liệu phân biệt, rồi backup:
bashdocker compose exec -T postgres psql -U club -d clubdb -c "INSERT INTO members (name, email) VALUES ('Carol', 'carol@example.com');" mkdir -p ../backups docker compose exec -T postgres pg_dump -U club -d clubdb > ../backups/clubdb-$(date +%Y%m%d).sql ls -l ../backups head -n 5 ../backups/clubdb-*.sqlKết quả mong đợi: file
.sqllớn hơn 0 byte, dòng đầu có-- PostgreSQL database dump. Dùng-Tđể Compose không cấp TTY, tránh làm hỏng đầu ra khi chuyển hướng.Giả lập mất dữ liệu:
bashdocker compose down -v docker compose up -d sleep 10 curl -s http://localhost:3000/api/membersKết quả mong đợi: chỉ còn Alice và Bob (Carol đã mất vì volume bị xóa,
init.sqlnạp lại dữ liệu mẫu).Restore thử vào một database mới để kiểm chứng bản backup dùng được (cách an toàn, không đụng database đang chạy):
bashBACKUP=$(ls -1t ../backups/clubdb-*.sql | head -n 1) docker compose exec -T postgres psql -U club -d postgres -c "CREATE DATABASE clubdb_restore;" docker compose exec -T postgres psql -U club -d clubdb_restore < "$BACKUP" docker compose exec -T postgres psql -U club -d clubdb_restore -c "SELECT id, name FROM members ORDER BY id;"Kết quả mong đợi: bảng trong
clubdb_restorecó 3 dòng gồm Carol. Dùng biếnBACKUPvì chuyển hướng<với ký tự đại diện cho nhiều file sẽ báoambiguous redirect.Khôi phục dữ liệu cho database đang phục vụ API:
bashdocker compose exec -T postgres psql -U club -d clubdb -c "DROP TABLE members;" docker compose exec -T postgres psql -U club -d clubdb < "$BACKUP" curl -s http://localhost:3000/api/members docker compose exec -T postgres psql -U club -d postgres -c "DROP DATABASE clubdb_restore;"Kết quả mong đợi: danh sách có Carol trở lại. Vì sao
DROP TABLEtrước: bản dump chứaCREATE TABLE, nếu bảng đã tồn tại thì restore báo lỗialready exists. Trong thực tế bạn sẽ restore vào database trống thay vì xóa bảng.
L1 – Foundation: checklist hoàn thành
-
docker compose pscho thấypostgres,backend,frontendđều(healthy). - Giải thích được (bằng lời của bạn) vì sao
backendchờpostgres. - Làm backend thoát bất thường (dừng postgres khi pool có kết nối nhàn rỗi) và thấy nó tự khởi động lại với
RestartCounttăng;docker compose stop backendthì không. -
/healthtrả 200 và/readytrả 503 khi bạn tạm dừngpostgres. - Đã lưu đầu ra các lệnh chính vào
evidence/command-output.txt.
L2 – Engineering: thử thách tự làm
Hoàn thành tối thiểu 3 trong 4 thử thách. Tiêu chí hoàn thành nằm trong từng mục.
- Healthcheck có chủ đích. Đổi healthcheck của backend sang
/readyvà so sánh hành vi khi DB dừng. Tiêu chí: giải thích bằng 3 đến 5 câu trongreport.mdnên chọn/healthhay/readycho healthcheck của Compose và vì sao. - OOM có kiểm soát. Đặt
memory: 16Mcho backend, quan sát điều gì xảy ra, rồi trả về giá trị hợp lý. Tiêu chí: có ảnh chụp/logdocker inspectchoOOMKilledhoặc exit code 137, và giá trị cuối cùng có lý giải dựa trêndocker stats. - Backup tự động. Viết script
backup.shtạo fileclubdb-YYYYmmdd-HHMMSS.sql, giữ lại 5 bản gần nhất. Tiêu chí: chạy 7 lần còn đúng 5 file; script không chứa mật khẩu. - Secret cho backend. Làm cho backend đọc mật khẩu từ file (ví dụ nếu có
DB_PASSWORD_FILEthì đọc file) và bỏDB_PASSWORDkhỏi môi trường của backend. Tiêu chí:docker compose exec backend envkhông còn mật khẩu và/readytrả 200.
Gợi ý
- Thử thách 1: với
/ready, khi DB dừng backend chuyểnunhealthysauretrieslần. Vìfrontendchỉ chờ lúc khởi động nên nó không bị dừng, nhưng nếu chạy lạiupthì thứ tự sẽ bị chặn. - Thử thách 2:
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' ID. Node idle dùng khoảng 14 MiB nên 16M có thể vẫn chạy được (đã kiểm chứng); nếu không thấyOOMKilled, hạ xuống 8M, lúc đó backend bị OOM kill (exit 137) vàRestartCounttăng liên tục. - Thử thách 3: trong thư mục chứa backup,
ls -1t clubdb-*.sql | tail -n +6 | xargs -r rm --xóa các bản cũ hơn 5 bản gần nhất. - Thử thách 4: trong
db.js, đọc bằngfs.readFileSync(process.env.DB_PASSWORD_FILE, "utf8").trim()khi biến này có mặt; thêmsecrets:cho service backend.
Lời giải tham khảo (thử thách 3)
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
mkdir -p backups
out="backups/clubdb-$(date +%Y%m%d-%H%M%S).sql"
docker compose exec -T postgres pg_dump -U club -d clubdb > "$out"
test -s "$out" || { echo "backup rong"; rm -f "$out"; exit 1; }
ls -1t backups/clubdb-*.sql | tail -n +6 | xargs -r rm --
echo "OK: $out"Đặt backup.sh cạnh compose.yaml (trong source/) để docker compose tìm đúng project. Lệnh xác nhận: chạy bash backup.sh bảy lần (cách nhau hơn 1 giây) rồi ls backups | wc -l phải ra 5.
Ví dụ tham khảo hoàn chỉnh nằm trong examples/compose/healthcheck-restart/; hãy tự làm trước khi so sánh.
Phá lỗi và debug
Tình huống: healthcheck quá ngặt
Cách phá: sửa healthcheck của backend thành interval: 2s, timeout: 1s, retries: 1, bỏ start_period, và trỏ vào đường dẫn sai http://127.0.0.1:3000/healthz.
docker compose up -d
docker compose psTriệu chứng: backend ở (health: starting) rồi (unhealthy); frontend không bao giờ được tạo (vì chờ service_healthy), lệnh up báo dependency failed to start: container ... is unhealthy.
Hướng điều tra:
docker compose psđể xác định service nào unhealthy.Xem log của healthcheck:
bashdocker inspect --format '{{range .State.Health.Log}}{{.ExitCode}} {{.Output}}{{end}}' $(docker compose ps -q backend)Output có dạng
wget: server returned error: HTTP/1.1 404 Not Found(nội dung chính xác phụ thuộc phiên bản BusyBox).Chạy tay lệnh check trong container để tái hiện:
docker compose exec backend wget -S -O /dev/null http://127.0.0.1:3000/health.Đối chiếu đường dẫn trong
app.js(/health,/ready).
Nguyên nhân gốc: lệnh check sai đường dẫn, cộng với thông số quá ngặt (retries: 1, không start_period) khiến một lần chậm cũng bị coi là chết.
Phòng tránh: chạy tay lệnh check trước khi đưa vào YAML; để start_period bao trùm thời gian khởi động thật; timeout lớn hơn thời gian phản hồi bình thường vài lần.
Tình huống liên quan
Hãy làm thêm tình huống "API không kết nối được DB" trong examples/troubleshooting/ (scenario-03) và tình huống "database mất dữ liệu sau recreate" (scenario-04): chỉ đọc triệu chứng, tự tìm nguyên nhân gốc.
Lỗi thường gặp
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
Backend báo lỗi 500 ngay sau up | Dùng depends_on dạng ngắn, không chờ Postgres ready | Thêm healthcheck cho postgres và condition: service_healthy |
Healthcheck luôn unhealthy dù ứng dụng chạy | Thiếu wget/curl trong image, hoặc check vào localhost bị phân giải sang IPv6 | Chạy tay lệnh check bằng docker compose exec; dùng 127.0.0.1; chọn công cụ có sẵn trong image |
dependency failed to start khi up | Dependency chuyển unhealthy | docker compose ps, đọc .State.Health.Log, sửa lệnh hoặc tăng start_period |
POSTGRES_PASSWORD_FILE đổi mà vẫn đăng nhập bằng mật khẩu cũ | Volume đã được khởi tạo, Postgres chỉ đặt mật khẩu lần đầu | Đổi mật khẩu bằng ALTER USER, hoặc down -v nếu chấp nhận mất dữ liệu |
| Container thoát với exit code 137 | Vượt giới hạn memory (OOM kill) hoặc bị kill | docker inspect xem OOMKilled; tăng limit hoặc sửa rò rỉ bộ nhớ |
| File backup rỗng hoặc có ký tự lạ | Thiếu -T khi dùng docker compose exec với chuyển hướng | Dùng exec -T; kiểm tra test -s |
Restore báo relation already exists | Nạp dump vào database đã có bảng | Restore vào database trống hoặc DROP TABLE trước |
| Cổng database lộ ra mạng | Publish 5432:5432 không cần thiết | Bỏ ports của postgres; backend truy cập qua network nội bộ |
Thay đổi deploy.resources không thấy hiệu lực | Chưa recreate container | Chạy lại docker compose up -d rồi kiểm tra bằng docker inspect |
Tự kiểm tra
depends_on: [postgres]đảm bảo điều gì và không đảm bảo điều gì?- Khác biệt giữa
restart: alwaysvàrestart: unless-stopped? - Vì sao container
unhealthykhông tự được Docker restart, và bạn có thể xử lý thế nào? - Vì sao lưu mật khẩu vào file secret an toàn hơn biến môi trường, và giới hạn của cách này là gì?
- Tại sao phải dùng
docker compose exec -Tkhi chạypg_dump > file? - Tình huống: sau
docker compose up -d, API trả 503 ở/readytrong 20 giây rồi tự ổn. Bạn kết luận gì và có cần sửa không? - Tình huống: dịch vụ đang chạy tốt thì Postgres bị restart. Frontend và backend có bị Compose khởi động lại không? Hệ thống phục hồi thế nào?
Đáp án
- Đảm bảo container dependency đã được khởi động trước; không đảm bảo ứng dụng bên trong đã sẵn sàng nhận kết nối.
- Cả hai tự khởi động lại khi thoát;
unless-stoppedkhông khởi động lại container mà bạn đã chủ động dừng khi daemon Docker khởi động lại, cònalwaysthì có. - Restart chỉ kích hoạt khi tiến trình thoát;
unhealthychỉ là trạng thái. Có thể để ứng dụng tự thoát khi lỗi nặng, dùng công cụ giám sát bên ngoài, hoặc dùng Kubernetes (liveness probe) ở các tuần sau. - Secret không hiện trong
docker inspecthay danh sách biến môi trường của tiến trình con, và không bị in nhầm vào log. Giới hạn: ứng dụng phải biết đọc file, và file trên host vẫn phải được bảo vệ, không commit. - Không có
-T, Compose cấp TTY, có thể chèn ký tự điều khiển/CRLF vào luồng và làm hỏng file dump hoặc làm lệnh báo lỗi stdin không phải TTY. - Đó là thời gian Postgres khởi tạo; hành vi bình thường. Không cần sửa code, nhưng nên có healthcheck và
depends_onđể người dùng không nhìn thấy khoảng đó. - Không;
depends_onchỉ tác động lúcup. Pool kết nối của backend sẽ lỗi trong lúc DB tắt (/ready503) rồi tạo kết nối mới khi DB lên, nên API tự phục hồi mà không cần restart.
Nộp bài
Nộp vào lab-14/ trong repository cá nhân (ví dụ devops-labs), theo templates/lab-template.md:
-
README.mdtrả lời đủ 7 câu hỏi của template (giải quyết vấn đề gì, kiến thức, các bước, kết quả mong đợi, lỗi đã gặp, cách khắc phục, cải thiện) và đánh dấu level đã làm (L1/L2). -
source/chứacompose.yamlđã sửa (healthcheck cho cả 3 service, restart, limits, logging, secrets),.env.example, vàbackup.shnếu làm L2. -
evidence/command-output.txtgồm đầu ra củadocker compose ps(healthy),docker inspectHealth Log,docker stats --no-stream, lệnh backup và restore kèm kết quảcurl /api/members. -
report.mdmô tả tình huống Break -> Debug (healthcheck quá ngặt): triệu chứng, cách điều tra, nguyên nhân gốc, cách phòng tránh. -
screenshots/(nếu có) chỉ bổ trợ; không thay cho log và source. - Đã kiểm tra: không commit
.env,secrets/, hay file backup chứa dữ liệu nhạy cảm (git statussạch các mục đó).
Recap
- Container chạy chưa có nghĩa là service sẵn sàng: dùng healthcheck và
condition: service_healthy. depends_onchỉ tác động lúc khởi động; ứng dụng vẫn phải chịu được mất kết nối tạm thời./health(còn sống) khác/ready(sẵn sàng phục vụ); đây là nền tảng của liveness và readiness probe trong Kubernetes.restart: unless-stoppedhồi phục khi tiến trình thoát, nhưng không xử lý trạng tháiunhealthy.- Luôn đặt giới hạn tài nguyên và log rotation để một container lỗi không làm sập cả máy.
- Không đưa mật khẩu vào Git; ưu tiên secret dạng file khi image hỗ trợ.
- Volume không phải backup: tập
pg_dumpvà thử restore thật trước khi cần đến nó.