Docker Buildx Bake: Build Image Multi-Platform Deklaratif

Foto oleh John Fielding from Norwich, UK via Wikimedia Commons (CC BY 2.0)
docker buildx bake membangun image container dari file HCL, JSON, atau Compose yang deklaratif alih-alih perintah docker build yang panjang. Anda mendefinisikan target bernama, grup, dan variabel sekali, lalu membangun semuanya secara paralel dengan satu perintah. File yang sama berjalan identik di mesin lokal dan di CI, sehingga logika build tidak melenceng di antara keduanya.
Atribut matrix adalah map dari nama parameter ke daftar nilai, dan bake membangun setiap kombinasi sebagai target terpisah. Field name yang wajib mengatur bagaimana tiap target hasil dinamai lewat interpolasi variabel seperti versi atau nama service. Dua sumbu berisi dua nilai menghasilkan empat target, semuanya dibangun paralel dari satu definisi.
Atur cache-from dan cache-to pada target Anda memakai backend GitHub Actions, type=gha, karena runner hosted bersifat sementara. Beri setiap varian matrix scope cache sendiri agar varian tidak saling menimpa. Pakai mode=max untuk meng-cache layer perantara, tetapi perhatikan batas cache 10 GB per-repositori yang mengusir entri paling lama tak dipakai.
Untuk build murni Anda bisa melewatinya. Sejak v6 docker/bake-action membangun dari konteks Git secara default, jadi BuildKit meng-checkout repositori sendiri. Anda tetap butuh actions/checkout jika ada langkah sebelum build yang mengubah file, karena build berbasis konteks Git mengabaikan perubahan file lokal yang dibuat lebih awal di job.
Untuk satu image, satu platform, satu tag, docker build biasa lebih sederhana dan lebih sedikit yang perlu dipelajari. Bake berbuah saat beberapa image berbagi konfigurasi dasar, saat Anda membangun image sama di banyak platform atau versi, atau saat build lokal dan CI harus tetap identik byte-per-byte. Logika kondisional yang sangat kompleks bisa lebih jelas dalam bahasa skrip sungguhan.

Foto oleh John Fielding from Norwich, UK via Wikimedia Commons (CC BY 2.0)
Ringkasan Utama
docker buildx bake membaca file HCL deklaratif yang mendeskripsikan setiap image, platform, tag, dan setelan cache sebagai target bernama. Satu perintah membangun semuanya secara paralel, matrix target melipatgandakan satu definisi menjadi banyak varian, dan file yang sama menjalankan build lokal maupun CI tanpa perubahan. Ini menggantikan skrip docker build panjang yang gampang melenceng.
Cukup lama build container saya hidup di dalam skrip shell: tumpukan perintah docker buildx build, masing-masing dengan flag --platform, --tag, --cache-from, dan --build-arg sendiri yang disalin-tempel dengan perbedaan kecil. Setiap image baru berarti satu blok yang hampir identik lagi, dan YAML CI perlahan menyimpang dari yang saya jalankan di laptop. docker buildx bake memperbaiki ini dengan memindahkan seluruh matriks build ke file HCL deklaratif yang dibaca dengan cara sama oleh mesin saya maupun GitHub Actions.
Bake bukan tool terpisah yang perlu diinstal; ia ikut di dalam Buildx sebagai subperintah bake. Anda mengarahkannya ke sebuah file definisi, menyebut target yang diinginkan, dan BuildKit membangunnya secara bersamaan, berbagi cache dan layer sebisa mungkin. Saya menjalankan file yang sama di VPS ber-Docker dan di CI, dan satu-satunya perbedaan adalah beberapa override yang dilewatkan lewat command line.
File bake punya tiga blok penyusun: variabel yang memparameterisasi konfigurasi, target yang mendeskripsikan satu build masing-masing, dan grup yang membundel target agar satu perintah membangun beberapa sekaligus. Nama file default-nya adalah docker-bake.hcl, dan bake mengambilnya otomatis. Fitur paling berguna adalah inherits: definisikan setelan bersama sekali di target dasar, lalu perluas, sehingga image baru cukup beberapa baris alih-alih salinan penuh.
# docker-bake.hcl
variable "REGISTRY" {
default = "ghcr.io/matthewswongofficial"
}
variable "TAG" {
default = "latest"
}
# Shared base — every real target inherits from this.
target "_common" {
context = "."
dockerfile = "Dockerfile"
platforms = ["linux/amd64", "linux/arm64"]
labels = {
"org.opencontainers.image.source" = "https://github.com/MatthewsWongOfficial/app"
}
}
target "api" {
inherits = ["_common"]
target = "api"
tags = ["${REGISTRY}/api:${TAG}"]
}
target "worker" {
inherits = ["_common"]
target = "worker"
tags = ["${REGISTRY}/worker:${TAG}"]
}
# A group ties targets together so one command builds both.
group "default" {
targets = ["api", "worker"]
}Dengan file itu di tempatnya, docker buildx bake tanpa argumen membangun grup default, yang membangun api dan worker sekaligus untuk amd64 dan arm64 secara paralel. Menyebut satu target hanya membangun target itu. Karena REGISTRY dan TAG adalah variabel, saya tidak pernah mengedit file untuk merilis; saya menimpanya saat pemanggilan.
# Build everything in the default group
docker buildx bake
# Build just one target
docker buildx bake api
# Print the fully-resolved config as JSON without building
docker buildx bake --print
# Override a variable and push instead of loading locally
TAG=v1.4.0 docker buildx bake --set "*.output=type=registry"Jalankan docker buildx bake --print sebelum setiap build sungguhan. Ia mengurai semua variabel, pewarisan, dan ekspansi matrix menjadi JSON polos dan tidak membangun apa pun. Ini cara tercepat memastikan tag dan platform Anda benar-benar sesuai dugaan, dan persis struktur yang dikonsumsi CI saat menghasilkan matriks build.
Matrix target adalah tempat bake membuktikan nilainya. Atribut matrix adalah map dari nama parameter ke daftar nilai, dan bake membangun setiap kombinasi sebagai target terpisah. Field name yang wajib mengatur bagaimana tiap target hasil dinamai lewat interpolasi variabel. Beginilah saya membangun service yang sama terhadap tiga versi Node, atau memecah satu stage Dockerfile menjadi varian debug dan produksi, tanpa mengulang satu blok pun.
target "test" {
name = "test-node${nodeversion}"
matrix = {
nodeversion = ["18", "20", "22"]
}
args = {
NODE_VERSION = nodeversion
}
target = "test"
}
# Multi-axis: 2 x 2 = 4 targets
target "image" {
name = "${svc}-${flavor}"
matrix = {
svc = ["api", "worker"]
flavor = ["debug", "release"]
}
target = flavor
tags = ["ghcr.io/matthewswongofficial/${svc}:${flavor}"]
}Blok pertama mengembang menjadi test-node18, test-node20, dan test-node22. Blok kedua adalah matriks dua sumbu yang menghasilkan empat target mencakup setiap kombinasi service dan flavour. Satu kali jalan docker buildx bake membangun semuanya secara paralel, dan BuildKit menghapus duplikasi layer bersama di antaranya secara otomatis.
Cache diatur per target dengan cache-from dan cache-to, dan backend yang Anda pilih sangat menentukan di CI. Di VPS saya backend registry lokal sudah memadai, tetapi runner GitHub bersifat sementara, jadi saya memakai backend cache GitHub Actions (type=gha). Satu aturan yang paling menyelamatkan: beri setiap varian matrix scope cache sendiri. Jika dua varian berbagi scope, keduanya saling menimpa dan setiap build tampak seperti cache dingin.
# In the bake file — reference-type gha cache, scoped per target name.
target "_common" {
# ...
cache-from = ["type=gha,scope=build-${target}"]
cache-to = ["type=gha,scope=build-${target},mode=max"]
}
# Or override entirely from the CLI / CI:
docker buildx bake \
--set "*.cache-from=type=gha,scope=api" \
--set "*.cache-to=type=gha,scope=api,mode=max"mode=max mengekspor cache untuk setiap layer perantara, bukan hanya image akhir, sehingga rebuild jauh lebih cepat tetapi menyimpan jauh lebih banyak data. Cache GitHub Actions punya batas 10 GB per-repositori dan mengusir entri yang paling lama tak dipakai, jadi build matrix yang sibuk bisa diam-diam mengusir cache-nya sendiri antar-run. Beri scope sempit dan pangkas tag yang tak lagi Anda bangun.
Action resmi docker/bake-action membungkus perintah bake untuk CI. Per pertengahan 2026 ia berada di v7, dan sejak v6 ia membangun dari konteks Git secara default, artinya BuildKit meng-checkout repo sendiri dan Anda bisa melewati actions/checkout untuk build murni. Input push: true adalah singkatan untuk mengatur output setiap target ke registry. Berikut workflow minimal yang saya pakai untuk membangun dan mendorong image multi-platform pada setiap push.
# .github/workflows/build.yml
name: build
on:
push:
branches: [main]
jobs:
bake:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3 # arm64 emulation
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/bake-action@v7
with:
files: docker-bake.hcl
targets: default
push: true
env:
TAG: ${{ github.sha }}Untuk matriks besar ada pola yang lebih tajam: pakai docker buildx bake --print yang disalurkan lewat jq di satu job untuk memancarkan daftar nama target, umpankan itu ke strategi matrix GitHub Actions, dan jalankan tiap target di job paralelnya sendiri. Itu memberi tiap target runner terisolasi dan, yang penting, scope cache sendiri, sehingga build arm64 yang lambat tak pernah menghambat yang amd64. Saya tetap memakai setup-qemu-action di workflow karena layer arm64 di-cross-compile lewat emulasi pada runner amd64 kecuali Anda memasang builder arm64 native.
| Situasi | Pakai bake? |
|---|---|
| Satu image, satu platform, satu tag | Tidak — docker build biasa lebih sederhana |
| Beberapa image berbagi konfigurasi dasar | Ya — inherits menghapus duplikasi |
| Image sama di banyak platform atau versi | Ya — matrix target dibuat untuk ini |
| Build lokal dan CI harus tetap identik | Ya — satu file, dua lingkungan |
| Logika build kondisional kompleks dalam bahasa sungguhan | Sebagian — fungsi HCL membantu, tapi skrip bisa lebih cocok |
Patokan saya: begitu saya menyadari sedang menyalin-tempel perintah docker build dengan sedikit ubahan, saya memindahkannya ke sebuah target bake. File deklaratif menjadi satu sumber kebenaran, keluaran --print mendokumentasikan dirinya sendiri, dan CI berhenti menjadi tempat logika build diam-diam melenceng dari yang saya jalankan manual. Itu saja sudah menangkap lebih banyak rilis rusak ketimbang tes apa pun yang bisa saya tulis di sekitar skrip shell yang digantikannya.