Câu hỏi hay gặp lúc nửa đêm: container đang chạy trên máy chủ này được dựng từ commit nào?

Nếu image của bạn không có nhãn, câu trả lời là một chuỗi phỏng đoán — xem tag, đoán ngày, hỏi người khác, mở lịch sử CI. Nếu có nhãn, nó là một lệnh.

Bộ nhãn chuẩn

Đặc tả OCI định nghĩa một tập nhãn với tiền tố org.opencontainers.image.:

ARG SHA
ARG NGAY
LABEL org.opencontainers.image.title="Ung dung vi du" \
      org.opencontainers.image.description="Vi du cho bai viet ve nhan OCI" \
      org.opencontainers.image.version="1.4.2" \
      org.opencontainers.image.revision="${SHA}" \
      org.opencontainers.image.created="${NGAY}" \
      org.opencontainers.image.source="https://github.com/vi-du/ung-dung" \
      org.opencontainers.image.licenses="MIT" \
      org.opencontainers.image.vendor="Ca Phe va Code"

Trong đó hai nhãn quan trọng nhất cho việc vận hành là revision (commit SHA) và source (địa chỉ kho mã).

Chi phí: 1 337 byte

Dung lượng Layer
Không nhãn 4 094 764 byte 2
8 nhãn OCI 4 096 101 byte 2

Thêm 1 337 bytekhông thêm layer nào. Nhãn nằm trong cấu hình image, không phải trong filesystem — đúng như phần 2 đã đo: LABEL hiện 0B trong docker history.

Với giá đó thì không có lý do nào để không gắn nhãn.

Truy vết ngược: ba lệnh

$ docker inspect nh-chay --format '{{.Image}}'
sha256:dec27236cb994fb046a98c42e8df2c386bfdbd372...

$ docker inspect nh-chay --format '{{index .Config.Labels "org.opencontainers.image.revision"}}'
062422ac69077f502ae3319fc4827b70db6fd3ad

$ docker inspect nh-chay --format '{{index .Config.Labels "org.opencontainers.image.source"}}'
https://github.com/vi-du/ung-dung

Từ container đang chạy ra thẳng commit, không cần hỏi ai, không cần tin vào tag. Điều này quan trọng vì phần 4 đã cho thấy tag có thể bị đẩy đè: hai container cùng hiện myapp:1.4.2 có thể đang chạy hai nội dung khác nhau. Nhãn revision thì đi cùng nội dung, không đi cùng tên.

Nhãn cũng phá cache — đặt cuối tệp

revision đổi mỗi lần commit, nên LABEL chứa nó là một điểm mất cache, giống hệt ARGphần 13:

Sau khi đổi SHA
LABEL ở đầu Dockerfile 2,6 s — 1 bước CACHED
LABEL ở cuối Dockerfile 0,3 s — 2 bước CACHED

Vì nhãn không phụ thuộc vào bất cứ thứ gì trong image, luôn đặt khối LABEL ở cuối — sau mọi RUNCOPY. Không mất gì, và giữ được toàn bộ cache phía trên.

Trong multi-stage, chỉ nhãn tầng cuối có tác dụng

FROM alpine:3.20 AS xay
LABEL nhan.o-tang-build="co"
...
FROM alpine:3.20 AS chay
LABEL nhan.o-tang-cuoi="co"
{ "nhan.o-tang-cuoi": "co" }

Nhãn gắn ở tầng build biến mất cùng tầng đó. Đây là lỗi dễ mắc khi chuyển từ Dockerfile một tầng sang multi-stage — khối LABEL nằm nguyên chỗ cũ và không còn tác dụng.

Image của bạn đang mang tên người khác

Nhãn kế thừa từ image nền. Một Dockerfile không khai LABEL nào cả:

FROM nginx:alpine
RUN echo x > /x
ke thua 1 nhan tu anh nen
maintainer = NGINX Docker Maintainers <docker-maint@nginx.com>

Image của bạn tự giới thiệu là do đội NGINX bảo trì. Với maintainer thì chỉ hơi buồn cười, nhưng các image nền khác mang theo cả org.opencontainers.image.source trỏ về kho mã của họ — và khi ai đó truy vết sự cố, họ sẽ đi vào đúng kho mã sai.

Cách chữa là ghi đè tường minh những nhãn quan trọng, kể cả khi bạn không có gì để điền:

LABEL org.opencontainers.image.source="https://github.com/cua-ban/du-an" \
      org.opencontainers.image.vendor="Ten cong ty cua ban" \
      maintainer=""

Lọc theo nhãn

Nhãn không chỉ để đọc, chúng còn tìm được:

$ docker images --filter "label=org.opencontainers.image.vendor=Ca Phe va Code"
nh:nhan

Và có một loại nhãn thứ hai, gắn lúc chạy, độc lập với nhãn của image:

$ docker run -d --label moi-truong=san-xuat --label doi=ha-tang alpine sleep 60
$ docker ps --filter "label=moi-truong=san-xuat"
nh-rt | alpine:3.20

Nhãn lúc chạy hữu ích cho việc gom nhóm container theo môi trường, theo đội sở hữu, theo dịch vụ — và là cách các công cụ giám sát nhận biết container nào thuộc về ai. Docker Compose tự gắn một bộ nhãn kiểu này cho mọi thứ nó tạo ra, đó là cách docker compose down biết phải xoá cái gì.

Bộ nhãn tối thiểu nên có

# ... moi thu khac o tren ...
ARG SHA=unknown
ARG NGAY=unknown
ARG PHIEN_BAN=dev
LABEL org.opencontainers.image.title="ten-dich-vu" \
      org.opencontainers.image.source="https://github.com/to-chuc/kho-ma" \
      org.opencontainers.image.revision="${SHA}" \
      org.opencontainers.image.version="${PHIEN_BAN}" \
      org.opencontainers.image.created="${NGAY}"

Và trong đường ống CI:

docker build \
  --build-arg SHA="$(git rev-parse HEAD)" \
  --build-arg NGAY="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  --build-arg PHIEN_BAN="$(git describe --tags --always)" \
  -t myapp:1.4.2 .

Ba giá trị đó là toàn bộ thứ bạn cần để, sáu tháng sau, đi từ một container lạ trên một máy chủ lạ về đúng dòng mã đã sinh ra nó.

BuildKit còn có thể ghi thông tin nguồn gốc chi tiết hơn nhiều dưới dạng provenance — ai dựng, dựng bằng gì, từ nguồn nào — và đó là chủ đề của phần 22.

Thử ba mươi giây

Xem image trên máy bạn có truy vết được không:

docker images -q | head -20 | while read i; do
  printf "%-16s %s\n" "$i" \
    "$(docker inspect $i --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' 2>/dev/null)"
done

Cột thứ hai trống nghĩa là image đó không nói được nó từ đâu ra. Và nếu bạn thấy maintainer của một tổ chức khác trong image của mình:

docker inspect <anh> --format '{{json .Config.Labels}}'

thì đó là nhãn kế thừa mà bạn chưa ghi đè.

Phần sau xếp hạng từng thủ thuật cắt dung lượng image theo số megabyte thật sự tiết kiệm được.