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.

Hai cơ chế và ba action của compose watch

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ì.