Hình dung hai cách giữ cho bản tài liệu của bạn và của đồng nghiệp luôn khớp nhau. Cách một: cả hai cùng sửa một bản sống chung — nhanh, luôn mới, nhưng bạn phải gánh mọi tật của cái bản chung đó (ai là bản "thật", đồng hồ của ai tính). Cách hai: mỗi người giữ bản của mình, và mỗi lần bạn lưu, một người đưa thư photo đúng trang vừa đổi rồi đặt lên bàn đồng nghiệp. Người đưa thư canh bàn của bạn, nên hệ thống hồ sơ bên kia ra sao không còn quan trọng — cùng một kết quả dù họ ngồi phòng bên (Linux) hay ở nước khác (macOS/Windows). Phần 56 kết thúc bằng một cái bẫy của cách một: 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 chọn cách hai — 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.
Muốn tự thử, thêm khối develop.watch vào tệp Compose hiện có rồi chạy docker compose watch:
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: ./package.json
Rồi kiểm 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.
Mẫu số chung
Bài học đầu tiên, chính là chỗ khác nhau giữa hai cơ chế: "chia sẻ" và "sao chép" là hai mô hình đồng bộ khác gốc, và mỗi cái có cái giá riêng — chia sẻ thì không tốn bản sao và luôn mới, nhưng cột chặt bạn vào các tật của tầng bên dưới (inode, mtime, ngữ nghĩa hệ tệp, nền tảng); sao chép thì đồng nhất và dễ đoán, nhưng một chiều và tốn phí mỗi lần đổi. watch chép nên chạy giống nhau trên mọi HĐH và né sạch chuyện mtime, đổi lại tệp do container sinh ra không quay về máy bạn. Cùng lựa chọn "chia sẻ vs sao chép" ở khắp nơi: bộ nhớ chung so với truyền thông điệp, mmap so với read/write, mount so với rsync, một con trỏ sống so với một bản chụp bất biến. Nguyên tắc: chia sẻ nhanh nhưng ghép cứng và thừa hưởng mọi tật của cái chung; sao chép tách rời và ổn định nhưng trả bằng chiều-một-hướng và chi phí mỗi lần — chọn mô hình theo cái bạn cần tách hay cần chung, đừng mặc định lấy cái nhanh.
Điều thứ hai, về độ hạt của kích hoạt: hãy khớp cái giá của phản ứng với tần suất của tác nhân kích hoạt — phản ứng đắt chỉ nên gắn với tác nhân hiếm. watch có sync (1,1 giây, chạy suốt) và rebuild (6,5 giây, đắt gấp sáu); cả nghệ thuật nằm ở việc gắn sync với thứ đổi liên tục (mã nguồn) và rebuild với thứ đổi hiếm (requirements.txt, package.json). Đặt rebuild cho cả thư mục mã nguồn là mỗi lần lưu tốn 6,5 giây, xoá sạch lợi ích. Cùng nguyên lý ở khắp nơi: phạm vi làm mất hiệu lực cache, biên dịch tăng dần so với dựng lại toàn bộ, thay đổi nào phá layer nào, debounce/gộp sự kiện. Nguyên tắc: việc càng đắt thì tác nhân kích hoạt nó càng phải hiếm và hẹp — mỗi lần buộc một phản ứng nặng vào một sự kiện thường xuyên, bạn đang trả cái giá đắt nhất ở đúng tần suất cao nhất.
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ì.