Phần 44 đo cách sao lưu và khôi phục volume. Bài này đi theo hướng ngược lại: dọn những thứ không ai cần tới nữa. docker system df trên máy đang viết bài này báo image chiếm 2,209GB (75% thu hồi được), volume 942,5MB (100% thu hồi được), build cache 1,753GB (chỉ 11% thu hồi được ở lần đo đầu). Ba con số đó trông như ba việc dọn dẹp giống nhau, nhưng khi chạy prune thật cho từng loại, ba lớp hoá ra hoạt động theo ba luật rất khác nhau — và một trong ba lệnh đã xoá mất dữ liệu thật ngay trong lúc chuẩn bị bài, không hỏi lại câu nào. Toàn bộ chạy trên Docker 29.7.2 (Docker Desktop, macOS), driver overlayfs với containerd.snapshotter.v1.
Lớp 1: image — dangling gần như không còn tồn tại
Kịch bản kinh điển để tạo image <none> (dangling): build một image gắn tag demo:v1, sửa Dockerfile rồi build lại với cùng tag — bản cũ mất tag, thành mồ côi. Dựng đúng kịch bản đó rồi chạy:
docker image prune -f
# Total reclaimed space: 0B
0 byte. Không phải vì không có gì để dọn — mà vì trên Docker 29 với driver-type: io.containerd.snapshotter.v1 (xem bằng docker info | grep snapshotter), một image mất tag cuối cùng bị xoá thẳng ngay lúc đó, không còn ở lại dưới dạng <none>:<none> như kiểu graph driver overlay2 cũ. Kiểm bằng docker images -a: chỉ còn đúng image đang có tag, không có dòng dangling nào. Đây là điều đi ngược thói quen nhiều năm nay — lời khuyên "chạy docker image prune định kỳ để dọn dangling" gần như không còn việc gì để làm trên bản Docker mới, vì container runtime đã tự dọn ngay khi untag.
Rác thật nằm ở lớp khác: image có tag nhưng không container nào dùng tới. image prune mặc định không đụng vào loại này — phải thêm -a:
docker image prune -a -f
# Untagged: prunelab/demo:v1
# Total reclaimed space: 10.51MB
10,51MB được thu hồi thật, vì -a coi mọi image không được container nào tham chiếu là ứng viên, bất kể có tag hay không. Cái bẫy nằm ở đây: -a không phân biệt "image cũ không ai cần" với "image bản trước, giữ lại để rollback nếu bản mới lỗi". Chạy -a trên máy CI/CD mà không kiểm trước là xoá luôn cả bản rollback đó.
Lớp 2: volume — cái bẫy đã xảy ra thật khi chuẩn bị bài này
Dựng kịch bản: tạo volume prunelab-data, ghi 50MB dữ liệu, chạy container bằng --rm (container biến mất, volume ở lại — đúng thiết kế, volume sống ngoài vòng đời container). Ý định ban đầu chỉ là gọi docker volume prune với filter theo nhãn để xoá đúng một volume thử nghiệm. Lệnh đó chạy xong báo Total reclaimed space: 0B — filter không khớp như kỳ vọng, volume thử nghiệm vẫn còn nguyên. Bước tiếp theo, để kiểm tra hành vi mặc định, là chạy docker volume prune -f không kèm filter — nghĩ đơn giản đây chỉ là bước xem thử.
Đó là bước sai. docker volume prune -f xoá ngay lập tức, không hỏi lại, mọi volume không có container nào (kể cả đã dừng) tham chiếu tới. Kết quả thật:
Deleted Volumes:
0bef001f065575007cec9fae2be6f0ccfb84a8bb93a9a9fe01f7a047437f452f
0e1092ea32b6757576bdab722ccaa65c9a21b02787daab953b1d376ecfb8edce
cf415cdfe7833d0de75052ff70e7a7a24f826b92bfc4b1f215eb1149da505c98
def419d0c47c11722f9fe1f14bfc3ee1fce0d2f3b7f6213040389132005d1909
Total reclaimed space: 942.5MB
Bốn volume ẩn danh còn lại từ những lần chạy thử nghiệm ngày trước — không phải volume dựng riêng cho bài này — bị xoá sạch, 942,5MB, trong đúng một câu lệnh. Đây chính là "cái bẫy prune" mà chủ đề bài này nhắc tới, và nó xảy ra thật chứ không phải dựng kịch bản: -f bỏ qua lời nhắc xác nhận (bình thường Docker sẽ hỏi "WARNING! This will remove all local volumes not used by at least one container."), prune không có chế độ dry-run, và nó không quan tâm dữ liệu trong volume quan trọng hay không — chỉ quan tâm đúng một điều: có container nào đang trỏ tới nó hay không.
Cách xem trước an toàn, không xoá gì cả:
docker volume ls --filter dangling=true
Lệnh này liệt kê chính xác những volume mà prune sẽ xoá, không đụng tới byte nào. Luôn chạy lệnh này trước, đọc kỹ danh sách, rồi mới quyết định có prune hay không — đừng bao giờ gõ thẳng -f khi chưa biết mình sắp xoá cái gì.
Vế còn lại của cái bẫy nằm ở hướng ngược lại: nhiều người chạy docker system prune -a và nghĩ nó đã dọn luôn cả volume. Đo trên chính máy này: docker system prune (không kèm cờ --volumes) không đụng tới volume dù chỉ một byte — 942,5MB kể trên chỉ mất khi gọi thẳng docker volume prune hoặc thêm --volumes vào system prune. Tưởng đã dọn sạch mà thật ra volume chết vẫn còn nguyên, hoặc ngược lại tưởng an toàn mà --volumes xoá sạch — cùng một cờ, hai kỳ vọng trái ngược, tuỳ người dùng có đọc kỹ hay không.
Lớp 3: build cache — dọn cache đang "còn sống"
docker system df cho biết build cache khác image và volume ở một điểm: phần lớn dung lượng không thu hồi được bằng lệnh mặc định, vì cache đó vẫn đang nuôi layer của image hiện có.
docker builder prune -f
# Total reclaimed space: 236.9MB
docker system df | grep "Build Cache"
# Build Cache 8 0 1.558GB 0B
Prune mặc định chỉ thu được 236,9MB trên tổng 1,795GB — phần còn lại (1,558GB) báo 0% thu hồi được, vì nó vẫn được image blog-drawio-tools:local (1,55GB) tham chiếu. Thêm -a để ép xoá cả phần đó:
docker builder prune -a -f
# Total reclaimed space: 1.558GB
Sau lệnh này, docker run blog-drawio-tools:local ... vẫn chạy bình thường — image không hề bị đụng tới, vì build cache và image là hai kho lưu trữ tách biệt dù có thể trỏ tới cùng nội dung. Cái mất đi là khả năng tái sử dụng layer cho lần build sau: lần docker build kế tiếp trên image này sẽ phải dựng lại từ đầu thay vì cache-hit. Đo trên một Dockerfile nhỏ có bước chậm giả lập (RUN sleep 3): build lần đầu (cache đã bị xoá sạch) mất 3,35 giây, build lần hai y hệt (cache hit) chỉ mất 0,20 giây — nhanh hơn khoảng 16,75 lần. Với Dockerfile thật có npm install hay apt-get, khoảng cách này còn lớn hơn nhiều lần về số phút chứ không chỉ giây; con số 16,75 lần ở đây chỉ minh hoạ tỷ lệ, không phải mức tiết kiệm tuyệt đối cho mọi dự án.
Bảng tổng hợp
| Lớp | Lệnh mặc định | Đo thật | Lệnh mở rộng | Đo thật | Rủi ro |
|---|---|---|---|---|---|
| Image | image prune |
0B (containerd tự dọn dangling) | image prune -a |
10,51MB | xoá luôn bản rollback |
| Volume | volume prune |
942,5MB, xoá ngay, không hỏi | system prune --volumes |
tương tự | mất dữ liệu không phân biệt quan trọng |
| Build cache | builder prune |
236,9MB / 1,795GB | builder prune -a |
+1,558GB | build sau mất cache, không mất image |
Vậy nên dọn theo thứ tự nào
- Image: đừng kỳ vọng
image prunethu về nhiều trên Docker bản mới — dangling gần như tự biến mất. Muốn dọn thật thì-a, nhưng gắn tag rõ ràng (app:rollback) cho bản cần giữ trước khi chạy, vì-akhông phân biệt "không dùng tạm thời" với "không dùng vĩnh viễn". - Volume: luôn
docker volume ls --filter dangling=truetrước, đọc tên và kiểm tra từng cái nếu nghi ngờ, không bao giờ gõ-fkhi chưa xem danh sách. Đây là lớp duy nhất trong ba lớp mà một lệnh sai có thể làm mất dữ liệu không khôi phục được. - Build cache: an toàn nhất trong ba lớp — image không hỏng khi xoá cache, cái mất chỉ là thời gian build lần sau. Có thể xoá
-athoải mái trên máy CI đã cấu hình cache riêng (registry cache,--cache-from); trên máy dev đang build image nặng thì cân nhắc, vì lần build tiếp theo sẽ chậm hẳn.
Thử ba mươi giây
Xem trước chính xác những gì docker volume prune sẽ xoá, không đụng byte nào:
docker volume ls --filter dangling=true
docker system df -v | grep -A 5 "Local Volumes"
Nếu danh sách trống, docker volume prune không có gì để xoá — an toàn. Nếu có tên lạ, kiểm tra docker volume inspect <tên> trước khi quyết định.
Bài viết liên quan
- Nén chỉ giảm 3% dung lượng nhưng chậm gấp 6 lần: đo ba cách sao lưu và khôi phục volume Docker
- UID 1000 ghi được, UID 2000 đọc bị từ chối ngay lập tức: đo lệch quyền tệp giữa container và host, vá bằng ACL và group thay vì chown
- Bind mount ghi ngẫu nhiên 4K nhanh gấp 4 lần volume, nhưng thua khi ghi tuần tự: đo I/O theo bốn kiểu truy cập