Máy lập trình viên là Mac dùng chip Apple (arm64). Máy chủ sản xuất là amd64. Image dựng trên máy này không chạy được ở máy kia — trừ khi bạn dựng cho cả hai.
buildx làm được, và có hai cách làm với chênh lệch rất lớn.
Hai cách dựng cho kiến trúc khác
Cách 1 — mô phỏng. Toàn bộ tầng build chạy trong môi trường amd64 giả lập bằng QEMU:
FROM golang:1.22-alpine AS xay
WORKDIR /src
COPY go.mod main.go ./
RUN CGO_ENABLED=0 go build -o /ct .
Cách 2 — biên dịch chéo. Tầng build chạy ở kiến trúc của máy, chỉ bảo trình biên dịch sinh mã cho kiến trúc đích:
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS xay
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
COPY go.mod main.go ./
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /ct .
Hai biến TARGETOS và TARGETARCH là biến dựng sẵn của BuildKit — phần 13 đã đo chúng, và nhớ rằng phải khai ARG trong tầng thì mới dùng được.
Đo trên một chương trình Go 1 824 dòng, máy arm64:
| Thời gian | |
|---|---|
| Biên dịch chéo → arm64 (kiến trúc của máy) | 3,8 s |
| Biên dịch chéo → amd64 | 3,4 s |
| Mô phỏng QEMU → amd64 | 42,7 s |
Chậm hơn 12,6 lần. Và chú ý hai dòng đầu: biên dịch chéo sang kiến trúc khác không tốn thêm gì so với dựng cho chính máy mình — vì trình biên dịch vẫn chạy nguyên tốc độ, chỉ xuất ra mã khác.
Con số đầu tiên của tôi là 4,3 lần, và nó sai
Lần đo đầu tiên cho ra "mô phỏng chậm hơn 4,3 lần". Nghe hợp lý nên suýt viết luôn.
Nhưng vòng lặp đo của tôi có một dòng thay thế chuỗi làm hỏng tệp main.go:
1787633450.631695
// moc 1787633453.8477068
Mã nguồn không biên dịch được, go build thoát với lỗi, và tôi đang bấm giờ cho một build hỏng — mà thất bại thì luôn nhanh. Vòng lặp đo không kiểm mã thoát nên không có gì báo.
Sau khi sinh lại mã nguồn sạch và thêm kiểm tra returncode, con số thật là 12,6 lần.
Đây là lần thứ hai trong sê-ri tôi dính đúng cái bẫy này — lần trước là Redis báo 72 triệu lệnh mỗi giây khi 959 trên 1 000 lô đã hỏng. Luật rút ra thì luôn giống nhau: mọi vòng lặp đo phải kiểm mã thoát, và một con số bất ngờ theo hướng dễ chịu là dấu hiệu để nghi ngờ chứ không phải để mừng.
Khi nào buộc phải mô phỏng
Biên dịch chéo chỉ làm được nếu công cụ của bạn hỗ trợ. Bảng thực tế:
| Biên dịch chéo | |
|---|---|
| Go | ✅ có sẵn, chỉ cần GOOS/GOARCH |
| Rust | ✅ với --target, cần thêm linker |
| Java, .NET | ✅ bytecode vốn không phụ thuộc kiến trúc |
| Node, Python thuần | ✅ không có gì để biên dịch |
| Node/Python có gói native | ❌ phải mô phỏng, hoặc dùng wheel/prebuild sẵn |
| C/C++ với thư viện hệ thống | ❌ rất khó, thường phải mô phỏng |
Với những trường hợp phải mô phỏng, con số 12,6 lần là cái giá bạn trả. Cách giảm đau thực dụng là chỉ mô phỏng phần bắt buộc: cài gói native trong tầng mô phỏng, còn lại biên dịch chéo.
Ảnh đa kiến trúc trông như thế nào
docker buildx build --platform linux/amd64,linux/arm64 --tag registry/da:v1 --push .
MediaType: application/vnd.oci.image.index.v1+json
Manifests:
Platform: linux/amd64
Platform: linux/arm64
Platform: unknown/unknown
Cái được đẩy lên không phải một image mà là một index trỏ tới nhiều image. Khi ai đó docker pull registry/da:v1, daemon đọc index, chọn manifest khớp kiến trúc máy, và chỉ tải phần đó.
Mục unknown/unknown là manifest chứa thông tin nguồn gốc (provenance) mà BuildKit tự thêm — chủ đề của phần 22.
Cả hai biến thể đều chạy và tự báo đúng kiến trúc của mình:
--platform linux/arm64 : chay tren linux/arm64
--platform linux/amd64 : chay tren linux/amd64
Dòng thứ hai chạy được trên máy arm64 của tôi là nhờ QEMU — cùng cơ chế làm build chậm 12,6 lần ở trên, nhưng ở đây chỉ để chạy thử một lệnh nên không sao.
Giới hạn --load đã biến mất
Lời khuyên phổ biến: --load chỉ nạp được một kiến trúc; muốn nhiều thì phải --push lên registry. Tôi thử:
$ docker buildx build --platform linux/amd64,linux/arm64 --tag da:hai --load .
$ echo $?
0
IMAGE ID DISK USAGE CONTENT SIZE
da:hai d55077ee9543 37.6MB 12.1MB
├─ linux/amd64 3a3bbc37a775 18.1MB 5.89MB
└─ linux/arm64 5d70709e66e7 19.4MB 6.23MB
Chạy được, và cả hai biến thể đều dùng được. Lý do là Docker Desktop này dùng kho image containerd:
driver-type: io.containerd.snapshotter.v1
Kho image cũ của Docker không lưu được nhiều kiến trúc dưới một tag, kho containerd thì được. Nếu máy bạn chưa bật, --load đa kiến trúc vẫn sẽ báo lỗi — kiểm bằng docker info | grep -i snapshotter.
Dựng cả hai không tốn gấp đôi
chi arm64 : 4,1 s
chi amd64 : 3,6 s
ca hai cung luc: 1,0 s
Hàng cuối nhanh vì hai lần build trước đã điền cache. Nhưng ngay cả khi cache rỗng, BuildKit dựng các kiến trúc song song — đúng cơ chế đã đo ở phần 17 với ba tầng độc lập (11,0 s xuống 3,6 s). Tổng thời gian là của kiến trúc chậm nhất, không phải tổng của tất cả.
Về dung lượng thì có nhân lên: mỗi kiến trúc là một bộ layer riêng — 18,1 MB cho amd64 và 19,4 MB cho arm64 trong ví dụ trên. Registry lưu cả hai, nhưng mỗi máy chỉ tải phần của mình.
Thử ba mươi giây
Xem builder của bạn dựng được cho kiến trúc nào:
docker buildx inspect --bootstrap | grep -i platforms
Và kiểm tra Dockerfile của bạn có đang mô phỏng không:
grep -n 'FROM' Dockerfile
Dòng FROM của tầng build không có --platform=$BUILDPLATFORM nghĩa là tầng đó sẽ chạy dưới mô phỏng khi bạn dựng cho kiến trúc khác — và nếu công cụ của bạn biên dịch chéo được, đó là 12,6 lần thời gian bạn đang trả không cần thiết.
Phần sau bàn về bí mật lúc build: --mount=type=secret so với --build-arg, và cách kiểm chứng bí mật không lọt vào image.