Phần 56 kết thúc bằng một cái bẫy: gắn một tệp vào container thì nội dung vào được nhưng mtime đứng yên, nên bộ nạp lại không bao giờ chạy. docker compose watch giải quyết chuyện đó bằng cách bỏ hẳn bind mount.
Nó không gắn gì cả
so muc trong .Mounts: 0
/ma khong phai mount rieng — no la lop cua image
Container chạy với watch không có mount nào. Thư mục mã nguồn bên trong nó là một lớp bình thường của image. Khi bạn sửa tệp, Compose chép tệp đó vào container.
Đây là khác biệt cốt lõi: bộ theo dõi tệp chạy trên máy bạn, không phải trong container. Nên mọi thứ ở phần 56 — inode đổi, mtime không truyền qua, virtiofs — đều không còn liên quan. Tôi sửa tệp bằng sed -i (thứ ghi tệp mới rồi đổi tên đè, đúng cách làm hỏng bind mount một tệp), và watch bắt được ngay.
Nó cũng có nghĩa là cách này chạy giống nhau trên macOS, Windows và Linux, thay vì phụ thuộc vào lớp chia sẻ tệp của từng nền tảng.
Ba action, ba mức giá
services:
web:
build: .
develop:
watch:
- action: sync
path: ./app.py
target: /ma/app.py
- action: rebuild
path: ./requirements.txt
| Action | Thời gian | Nó làm gì |
|---|---|---|
sync |
1,41 / 1,09 / 1,09 s | chỉ chép tệp, không đụng tiến trình |
sync+restart |
1,99 / 1,96 / 1,96 s | chép tệp rồi khởi động lại tiến trình |
rebuild |
6,50 s | dựng lại image và tạo lại container |
Đặt cạnh số liệu của phần 56:
| Cách | Thời gian |
|---|---|
| bind mount + bộ nạp lại | 1,02 s |
watch sync + bộ nạp lại |
1,09 s |
compose restart |
1,41 s |
watch sync+restart |
1,96 s |
up -d --build |
1,92 s |
sync nhanh ngang bind mount (1,09 so với 1,02 — chênh lệch nằm trong nhiễu), nhưng đáng tin cậy hơn vì không phụ thuộc mtime.
sync+restart chậm hơn compose restart khoảng nửa giây, đổi lại nó tự động — bạn không phải gõ gì.
rebuild là cái đắt nhất và bạn muốn nó hiếm khi chạy. Đó là lý do khai đường dẫn riêng cho requirements.txt (hoặc package.json, go.mod, pom.xml) thay vì để cả thư mục kích hoạt rebuild.
Chọn action nào
| Ứng dụng của bạn | Dùng |
|---|---|
Có bộ nạp lại sẵn (Flask --reload, nodemon, air, Vite) |
sync |
| Không có bộ nạp lại, khởi động nhanh | sync+restart |
| Tệp khai dependency | rebuild |
Tệp biên dịch ra (dist/, target/) |
không khai — để công cụ build lo |
Sai lầm hay gặp là đặt action: rebuild cho cả thư mục mã nguồn. Khi đó mỗi lần lưu tệp là 6,5 giây, và bạn mất luôn lợi ích của watch.
Nó tôn trọng cả hai loại quy tắc bỏ qua
Tôi tạo .dockerignore chứa một tệp, và khai thêm mục ignore: trong Compose:
- action: sync
path: ./
target: /ma
ignore:
- "khong-dong-bo/"
Rồi sửa cả ba loại tệp trong lúc watch đang chạy:
| Tệp | Trong container |
|---|---|
bi-bo-qua.txt (trong .dockerignore) |
không có |
khong-dong-bo/f.txt (trong ignore:) |
không có |
binh-thuong.txt |
có, nội dung đúng |
Điều này quan trọng hơn vẻ ngoài của nó: không có nó, watch sẽ đồng bộ cả node_modules, .git, .venv và mọi tạo phẩm build vào container mỗi lần chúng đổi. Với node_modules thì đó là hàng chục nghìn tệp, và phần 42 đã đo được cái giá của việc chép nhiều tệp nhỏ.
Luôn kiểm tra .dockerignore của bạn trước khi bật watch trên cả thư mục.
Một phép đo tôi làm hỏng
Để đo rebuild, tôi thêm một gói vào requirements.txt rồi đếm thời gian tới khi import được nó trong container. Kết quả: 0,14 giây — nhanh một cách vô lý, và log của watch chỉ ghi Syncing chứ không hề có Rebuilding.
Gói tôi chọn là itsdangerous, mà nó vốn đã là phụ thuộc của Flask nên có sẵn từ đầu. Phép thử của tôi thành công ngay lập tức vì nó chẳng kiểm tra gì cả.
Đổi sang tabulate và thêm một bước kiểm tra trước:
kiem tra truoc: goi 'tabulate' co san khong?
ModuleNotFoundError: No module named 'tabulate'
Giờ phép đo mới có nghĩa, và con số thật là 6,50 giây. Bài học lặp lại: một phép thử phải thất bại được trước khi kết quả thành công của nó có giá trị.
Hạn chế nên biết
Watch chạy ở tiền cảnh. docker compose watch chiếm terminal và dừng khi bạn đóng nó. Đó là công cụ cho lúc ngồi làm việc, không phải cho máy chủ.
Nó chỉ chép một chiều. Tệp do container tạo ra không quay về máy bạn. Nếu công cụ của bạn sinh mã (migration, tệp lock, mã từ codegen), bạn vẫn cần bind mount hoặc docker compose cp để lấy về.
Chưa thay thế được bind mount cho mọi trường hợp, nhưng cho vòng lặp sửa mã hằng ngày thì nó đơn giản hơn và ít bất ngờ hơn.
Thử ba mươi giây
Thêm vào tệp Compose hiện có của bạn:
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: ./package.json
docker compose watch
Rồi kiểm tra nó thật sự không gắn gì:
docker inspect $(docker compose ps -q web) --format '{{len .Mounts}}'
Ra 0 nghĩa là mã nguồn đang được chép vào chứ không phải chia sẻ — và mọi cái bẫy của bind mount ở phần 56 không còn áp dụng.
Phần sau đo bảo mật chuỗi cung ứng: quét lỗ hổng bắt được gì, và ghim digest ngăn được gì.