Docker có hai bộ máy build. Bộ cũ là thứ tồn tại từ 2013; BuildKit là mặc định từ Docker 23 và làm việc theo cách khác hẳn. Builder cũ vẫn còn dùng được, và điều đó cho phép đo A/B thật:

DOCKER_BUILDKIT=0 docker build ...   # builder cu
docker build ...                     # BuildKit

Docker 29.3.1 nhắc ngay khi bạn dùng bộ cũ:

DEPRECATED: The legacy builder is deprecated and will be removed in a future release.

Nhưng trước khi nó biến mất, bốn phép đo dưới đây giải thích vì sao BuildKit thay nó.

Build context: 30,05 MB so với 2 byte

Một dự án có thư mục rác 30 MB, và một Dockerfile chỉ cần đúng một tệp nhỏ:

FROM alpine:3.20
COPY nho.txt /
RUN echo x > /x
builder CU: Sending build context to Docker daemon  30.05MB
BuildKit  : transferring context: 2B

Builder cũ đóng gói toàn bộ thư mục thành tar rồi gửi cho daemon trước khi đọc Dockerfile — nó không biết mình sẽ cần gì. BuildKit đọc Dockerfile trước, dựng đồ thị phụ thuộc, rồi chỉ lấy đúng thứ cần: hai byte, đúng bằng nội dung nho.txt.

Đây chính là lý do phần 7 kết luận rằng lời khuyên "thêm .dockerignore để khỏi gửi hàng trăm megabyte" được viết cho thời builder cũ.

Ba tầng độc lập: 11,0 giây so với 3,6 giây

FROM alpine:3.20 AS a
RUN sleep 3 && echo a > /a

FROM alpine:3.20 AS b
RUN sleep 3 && echo b > /b

FROM alpine:3.20 AS c
RUN sleep 3 && echo c > /c

FROM alpine:3.20
COPY --from=a /a /
COPY --from=b /b /
COPY --from=c /c /
Thời gian
Builder cũ 11,0 s
BuildKit 3,6 s

Ba tầng không phụ thuộc nhau. Builder cũ chạy tuần tự — 3 + 3 + 3 giây. BuildKit thấy chúng độc lập và chạy song song, nên tổng bằng tầng chậm nhất cộng chút phần dôi.

Với một Dockerfile thật — tầng cài phụ thuộc, tầng biên dịch tài sản tĩnh, tầng chạy kiểm thử — đây thường là khác biệt lớn nhất trong cả bài.

Tầng thừa: 6,2 giây so với 0,5 giây

FROM alpine:3.20 AS dung
RUN echo dung > /d

FROM alpine:3.20 AS khong-dung
RUN sleep 5 && echo khong > /k     # khong ai COPY --from tang nay

FROM alpine:3.20
COPY --from=dung /d /
Thời gian
Builder cũ 6,2 s
BuildKit 0,5 s

Builder cũ chạy mọi tầng theo thứ tự xuất hiện, kể cả tầng chẳng ai dùng tới. BuildKit đi ngược từ tầng cuối lên và bỏ qua hoàn toàn nhánh không dẫn tới nó.

Phần 9 đã nêu hệ quả thực tế của hành vi này: thêm một tầng chạy npm test vào Dockerfile không làm test chạy, vì không ai tham chiếu tới nó.

Bí mật lúc build: builder cũ không có

RUN --mount=type=secret,id=mk cat /run/secrets/mk > /dev/null && echo "da doc bi mat" > /kq
BuildKit  : dung duoc | bi mat trong docker history: tim thay 0
builder CU: Usage: docker build [OPTIONS] PATH | URL | -

Builder cũ không nhận cờ --secret — nó in ra hướng dẫn sử dụng rồi thoát. Với bộ cũ, cách duy nhất để đưa bí mật vào lúc build là --build-arg, mà phần 13 đã đo được rằng giá trị đó nằm nguyên văn trong docker history.

BuildKit gắn bí mật vào một tmpfs chỉ tồn tại trong đúng lệnh RUN đó, và nó không để lại dấu vết nào trong image. Tôi tìm chuỗi bí mật trong toàn bộ lịch sử image: 0 kết quả. Đây là chủ đề đầy đủ của phần 21.

Build đơn giản thì chênh không nhiều

Để công bằng, với một Dockerfile hai dòng thì khác biệt nhỏ:

lan 1: builder CU 0,80 s | BuildKit 0,68 s
lan 2: builder CU 0,72 s | BuildKit 0,54 s
lan 3: builder CU 0,72 s | BuildKit 0,50 s

Nhanh hơn khoảng 30%, không phải mấy lần. Lợi thế của BuildKit chỉ bung ra khi Dockerfile có cấu trúc để nó khai thác: nhiều tầng, context lớn, hoặc những thứ chỉ nó mới làm được.

BuildKit còn có gì mà bộ cũ không có

Ngoài hai thứ đã đo ở trên:

--mount=type=cache giữ cache của npm, Maven, apt giữa các lần build — phần 18
--mount=type=bind đọc tệp từ context mà không chép vào layer
--platform và buildx dựng ảnh cho nhiều kiến trúc — phần 20
Cache đẩy lên registry chia cache giữa các máy CI — phần 19
SBOM và provenance ghi nguồn gốc chi tiết — phần 22
--progress=plain log đầy đủ, đọc được trong CI

Và một khác biệt về vận hành đáng nhớ: cache của BuildKit nằm ở chỗ khác với image. Phần 2 đo được rằng dựng 10 image tạo thêm 163,8 MB image cộng 109,1 MB cache build — và docker images không hề nhắc tới khoản thứ hai. Dọn nó bằng docker builder prune, không phải docker image prune.

Nếu bạn còn đang dùng bộ cũ

Ba trường hợp còn gặp trong thực tế:

  • DOCKER_BUILDKIT=0 nằm trong script CI cũ, thường được thêm vào từ lâu để chữa một lỗi đã hết. Bỏ dòng đó ra là được ngay ba phép đo ở trên.
  • Một số bản Docker rất cũ trên máy chủ lâu năm. Ở đó thì nâng cấp là việc đáng làm.
  • Cần định dạng log của bộ cũ. --progress=plain cho BuildKit ra log dạng văn bản đọc được trong CI, gần với thứ bạn quen.

Thử ba mươi giây

Xem bạn đang dùng bộ nào và Dockerfile của bạn có tận dụng được BuildKit không:

docker build --progress=plain -t thu . 2>&1 | head -3

Thấy Sending build context to Docker daemon là bộ cũ. Thấy [internal] load build context là BuildKit.

Và đếm tầng độc lập trong Dockerfile của bạn:

grep -c '^FROM' Dockerfile

Nhiều hơn hai nghĩa là BuildKit đang chạy song song cho bạn — hoặc sẽ chạy, nếu các tầng đó thật sự không phụ thuộc nhau.

Phần sau đo --mount=type=cache: giữ cache của npm, Maven và apt qua các lần build tiết kiệm được bao nhiêu.