Vòng lặp sửa mã rồi xem kết quả là thứ bạn lặp lại vài trăm lần mỗi ngày. Tôi đo bốn cách làm nó, tính từ lúc lưu tệp tới lúc ứng dụng trả về giá trị mới.
| Cách | Thời gian |
|---|---|
| Gắn cả thư mục + bộ nạp lại | 1,02 / 1,11 / 1,11 s |
Gắn thư mục + compose restart |
1,41 s |
Build lại + up -d --build |
1,92 / 1,97 / 1,92 s |
| Gắn một tệp + bộ nạp lại | không bao giờ nạp lại |
Dòng cuối là dòng đáng viết cả bài này.
Khi gắn một tệp, bộ nạp lại mù hẳn
volumes:
- ./app.py:/ma/app.py # gan DUNG mot tep
Tôi sửa tệp, chờ, ứng dụng vẫn trả về giá trị cũ. Kiểm tra bên trong container thì thấy tệp đã có nội dung mới:
noi dung app.py TRONG container: PHIEN_BAN = "vb1"
ung dung dang tra ve: v1
Nội dung đúng, hành vi sai, và không có dòng log nào. Nguyên nhân nằm ở siêu dữ liệu:
| Trên máy | Trong container | |
|---|---|---|
| inode | 126311407 → 126314800 | 51807 → 51807 |
| mtime | 1787822011 → 1787822161 | 1787822010 → 1787822010 |
| Nội dung | PHIEN_BAN = vX |
PHIEN_BAN = vX |
Trình soạn thảo (và sed -i) không sửa tệp tại chỗ — chúng ghi ra một tệp mới rồi đổi tên đè lên. Trên máy, inode đổi và mtime nhảy. Qua bind mount một tệp, container vẫn bám vào mục cũ: nội dung đọc ra đúng, nhưng dấu thời gian đứng yên.
Mọi bộ theo dõi tệp — của Flask, nodemon, webpack --watch, air, cargo watch — đều hỏi mtime. Dấu thời gian không đổi nghĩa là với chúng, tệp chưa hề bị sửa.
Cách sửa là gắn thư mục:
volumes:
- ./:/ma # gan CA THU MUC
Đo lại: mtime trong container khớp chính xác với trên máy (1787822185 cả hai bên), và bộ nạp lại chạy trong 1,02 giây.
Quy tắc: bind mount một tệp chỉ dùng cho thứ không bao giờ đổi trong lúc chạy — tệp cấu hình đọc một lần lúc khởi động, chứng chỉ. Mã nguồn thì luôn gắn cả thư mục.
Ba vòng lặp còn lại
Bộ nạp lại (1,0 s) nhanh nhất vì nó không dừng container, không dựng lại image, chỉ nạp lại mã trong tiến trình đang chạy. Đổi lại nó chỉ bắt được thay đổi mã; đổi dependency thì phải build lại.
compose restart (1,41 s) dùng khi ứng dụng không có bộ nạp lại. Container vẫn giữ nguyên, chỉ tiến trình bên trong khởi động lại. Không có bộ nạp lại thì tệp sửa xong hoàn toàn không có tác dụng cho tới khi restart — tôi chờ 4 giây và ứng dụng vẫn trả về v1 trong khi tệp trên đĩa đã là vc9.
up -d --build (1,9 s) là cách chậm nhất nhưng đúng nhất: nó dựng lại image nên bắt được cả thay đổi Dockerfile và dependency. Đây là cách duy nhất phản ánh đúng cái sẽ chạy trên máy chủ.
Chênh lệch 1,0 so với 1,9 giây nghe nhỏ, nhưng nhân với hai trăm lần mỗi ngày là ba phút — và quan trọng hơn là cảm giác: dưới một giây thì bạn còn giữ được mạch suy nghĩ, quá hai giây thì bạn bắt đầu chuyển sang cửa sổ khác.
Tệp riêng cho môi trường phát triển
Đừng nhét cấu hình phát triển vào tệp chính. Tách ra, rồi chọn tệp khi chạy:
# compose.dev.yaml
services:
web:
build: .
ports: ["127.0.0.1:18600:8000"]
volumes:
- ./:/ma
command: ["flask", "--app", "app", "run", "--host", "0.0.0.0", "--port", "8000", "--reload"]
docker compose -f compose.dev.yaml up -d
Ba thứ chỉ nên có ở đây, không bao giờ ở tệp chính:
- Bind mount mã nguồn. Trên máy chủ thật, mã phải nằm trong image.
- Cờ gỡ lỗi và bộ nạp lại. Chúng làm chậm và mở rộng bề mặt tấn công.
- Cổng gỡ lỗi từ xa. Và luôn bind vào
127.0.0.1, vì phần 35 đã đo được rằng0.0.0.0là mở ra Internet.
Với thư mục phụ thuộc (node_modules, .venv, target), nhớ đè volume lên như phần 42 đã đo — trên macOS đó là khác biệt giữa 3.027 và 290 mili giây.
Một cái bẫy nhỏ tôi vấp phải
Tôi đặt tên project là devA và mọi lệnh im lặng không làm gì. Bỏ phần nuốt lỗi ra mới thấy:
invalid project name "devA": must consist only of lowercase alphanumeric characters...
Tên project phải viết thường. Compose báo lỗi rõ ràng — nhưng tôi đã viết >/dev/null 2>&1 sau mỗi lệnh cho gọn, và thế là mất luôn thông báo. Đây là lần thứ n trong sê-ri này việc giấu đầu ra làm hỏng một phép đo.
Một cái nữa: tôi định đo "gắn thư mục nhưng không có bộ nạp lại" và thấy ứng dụng vẫn tự cập nhật. Hoá ra FLASK_DEBUG=1 tự bật bộ nạp lại, nên bỏ cờ --reload chẳng thay đổi gì. Phải gỡ cả biến môi trường đó thì ca đối chứng mới thật sự là đối chứng.
Thử ba mươi giây
Kiểm tra bộ theo dõi tệp của bạn có thấy thay đổi không:
docker compose exec web stat -c 'mtime=%Y' /duong/dan/tep
# sua tep tren may, roi chay lai:
docker compose exec web stat -c 'mtime=%Y' /duong/dan/tep
Hai con số giống nhau mà nội dung đã đổi thì bộ nạp lại của bạn sẽ không bao giờ chạy — và gần như chắc chắn bạn đang gắn một tệp thay vì cả thư mục.
Phần sau chuyển sang đầu kia: Compose trên máy chủ thật, và chuyện gì xảy ra khi máy khởi động lại.