myapp:1.4.2 nghe như một định danh. Nó không phải. Tag chỉ là một cái tên trỏ tới một nội dung, và cái tên đó có thể được trỏ sang chỗ khác bất cứ lúc nào — kể cả sau khi bạn đã kiểm thử xong và đang chuẩn bị triển khai.

Bài này dựng một registry thật (registry:2 chạy trên máy) rồi làm đúng chuyện đó.

Đẩy đè lên chính một tag

Hai image chỉ khác nội dung một tệp, đẩy lần lượt lên cùng một tag:

day lan 1 | digest: sha256:4994d3d5...f692eb | noi dung: PHIEN BAN MOT
day lan 2 | digest: sha256:8dc1f84b...9b702c | noi dung: PHIEN BAN HAI

Tag giữ nguyên tên, digest đổi hẳn. Registry chấp nhận không một lời cảnh báo — vì đó là hành vi đúng theo thiết kế: tag là con trỏ có thể ghi được.

Hỏi thẳng registry xem tag đang trỏ đâu:

$ curl -I -H "Accept: application/vnd.oci.image.index.v1+json" \
     http://localhost:5000/v2/ung-dung/manifests/moi-nhat
Docker-Content-Digest: sha256:8dc1f84b...9b702c

Máy đã có cache thì chạy bản nào

Đây là phần nguy hiểm, và cũng là lý do bài này tồn tại.

Tôi đặt một máy vào đúng tình huống thật: nó đã pull tag đó trước khi bản mới được đẩy lên.

docker run (khong pull)   : PHIEN BAN MOT      <-- cu
registry dang chua        : PHIEN BAN HAI
sau docker pull           : PHIEN BAN HAI

docker run không hỏi registry nếu tag đã có trong máy. Nó chạy thứ nó đang có, im lặng, và không có cách nào nhìn vào lệnh docker run mà biết được bạn đang chạy bản nào.

Đặt vào thực tế thì hình ảnh khá rõ: bạn đẩy bản vá lên myapp:1.4.2, ba máy chủ trong cụm đã có tag đó từ tuần trước, hai máy mới thì chưa. Sau khi triển khai, cụm của bạn chạy hai phiên bản khác nhau dưới cùng một cái tên — và mọi lệnh docker ps đều hiển thị giống hệt nhau.

Digest thì không đổi được

keo theo digest cu -> duoc | noi dung: PHIEN BAN MOT

Digest cũ vẫn kéo về được, và vẫn cho ra đúng nội dung cũ. Nó là băm của nội dung, nên không ai đẩy đè lên được: đổi một byte là ra một digest khác, không phải ghi đè lên digest cũ.

Đó là lý do digest — chứ không phải tag — mới là thứ dùng được khi bạn cần chắc chắn:

FROM localhost:5000/ung-dung@sha256:8dc1f84b...9b702c

Tôi dựng thử một image với FROM ...@digest và nó chạy đúng như mong đợi. Cú pháp này dùng được ở mọi chỗ nhận tên image: FROM, docker run, image: trong Compose, manifest Kubernetes.

Còn :latest thì sao

:latest không có nghĩa gì đặc biệt. Nó là một tag như mọi tag khác, chỉ khác ở chỗ Docker tự thêm nó vào khi bạn không ghi tag.

Phép thử: đẩy bản MỘT lên :latest, rồi đẩy bản HAI lên :1.0. Sau đó pull không ghi tag:

docker pull localhost:5000/thu
noi dung nhan duoc: PHIEN BAN MOT

Nhận được bản cũ hơn, vì :latest vẫn đang trỏ vào bản MỘT. "latest" không có nghĩa là "mới nhất" — nó chỉ có nghĩa là "cái tag tên là latest".

--pull=always đắt bao nhiêu

Cách chữa hiển nhiên cho chuyện cache cũ là luôn kiểm tra registry. Tôi đo cái giá của nó, với registry chạy ngay trên máy — tức là gần như không có độ trễ mạng:

Thời gian
docker run bình thường 239,3 ms
docker run --pull=always 1 904,8 ms

Thêm 1 665 ms cho một registry ở localhost, khi không có gì thay đổi. Với registry thật ở xa, con số sẽ lớn hơn.

Nên --pull=always là công cụ đúng cho một số chỗ (máy CI, lệnh chạy một lần) nhưng là lựa chọn tệ nếu bạn khởi động container thường xuyên. Và nó vẫn không giải quyết được vấn đề gốc: bạn vẫn không biết mình đang chạy cái gì, chỉ biết là mình đang chạy cái mới nhất mà tag đó trỏ tới tại thời điểm đó.

Cách làm đúng

Ba mức, chọn theo mức độ nghiêm túc:

Mức tối thiểu — đừng bao giờ đẩy đè một tag phiên bản. 1.4.2 đã đẩy là bất biến; sửa gì thì ra 1.4.3. Tag di động như latest, stable, main thì được phép đổi, nhưng đừng dùng chúng để triển khai.

Mức thực dụng — ghi lại digest lúc dựng, triển khai bằng digest. Đường ống CI của bạn đẩy image, đọc digest trả về, rồi truyền digest đó xuống môi trường chạy:

digest=$(docker buildx imagetools inspect --format '{{.Manifest.Digest}}' myapp:1.4.2)
echo "myapp@$digest"

Mức chặt — bật tính bất biến ở registry. Phần lớn registry thương mại có tuỳ chọn "immutable tags"; bật lên thì lần đẩy thứ hai vào cùng tag bị từ chối thẳng.

Và với image nền trong Dockerfile, ghim digest là cách duy nhất để build hôm nay và build sáu tháng sau cho ra cùng một thứ — FROM node:20-alpine hôm nay không phải FROM node:20-alpine của tháng trước.

Một điều đáng nhớ về docker images

Sau tất cả những phép đo trên, có một chỗ dễ nhầm: docker images hiển thị tag, không hiển thị digest. Muốn biết một image thật sự là gì:

docker inspect <ten>:<tag> --format '{{index .RepoDigests 0}}'

Nếu kết quả rỗng, image đó chưa từng được đẩy hoặc kéo từ registry nào — nó chỉ tồn tại trên máy bạn, và không có gì để đối chiếu.

Thử ba mươi giây

Xem các image đang chạy trên máy bạn thật sự là bản nào:

docker ps --format '{{.Names}}\t{{.Image}}' | while read ten anh; do
  printf "%-28s %s\n" "$ten" "$(docker inspect "$anh" --format '{{index .RepoDigests 0}}' 2>/dev/null || echo 'khong co digest')"
done

Hai container cùng hiện myapp:1.4.2 mà digest khác nhau nghĩa là chúng không chạy cùng một thứ — và đó chính là tình huống bài này vừa dựng lại.

Phần sau mổ xẻ Dockerfile: mỗi lệnh sinh ra gì, và vì sao thứ tự viết quyết định thời gian build.