Chapter sebelumnya menutup dengan satu janji: seluruh sulap section ini — myapp yang context-nya menyusut dari 148.21MB menjadi 1.87MB dengan npm ci 9.6 detik yang kini dilayani CACHED, dan katalog yang mengecil dari 851MB menjadi 12.6MB — berjalan di atas satu engine yang bekerja diam-diam di balik docker build, dan engine itu pantas diperkenalkan secara resmi. Engine tersebut bernama BuildKit, dan CLI yang mengendalikannya adalah plugin docker buildx. Chapter ini membuka kap keduanya: bagaimana BuildKit bekerja, apa itu builder dan driver, cara membuat builder multi-platform, serta frontend Dockerfile modern yang membuka fitur seperti heredoc — bekal yang kita butuhkan sebelum melangkah ke trik cache pada chapter berikutnya.

BuildKit, Mesin di Balik Setiap Build #

BuildKit adalah engine build generasi baru Docker dan telah menjadi engine bawaan sejak Docker Engine 23.0 — pada Engine 29.9.0 yang kita pakai, tidak ada sakelar yang perlu dinyalakan. Perintah docker build yang kita ketik sepanjang course ini sebenarnya diteruskan ke plugin CLI docker buildx versi 0.28.0, dan plugin itulah yang berbicara kepada BuildKit; docker build dengan demikian adalah alias dari docker buildx build, dua pintu menuju mesin yang sama.

Lima kemampuan membedakan BuildKit dari builder generasi lama yang memproses instruksi satu per satu secara berurutan:

  1. Eksekusi paralel. BuildKit mengevaluasi Dockerfile sebagai graf ketergantungan (DAG) lalu menjalankan node yang siap secara bersamaan — dua metadata basis dan transfer context pada output build myapp berjalan nyaris serentak, dan stage builder serta stage final katalog dikerjakan tumpang tindih.
  2. Cache granular per instruksi. Setiap instruksi memiliki cache key sendiri yang dihitung dari teks dan checksum berkas sumber — mekanisme CACHED yang kita bedah tuntas pada Chapter Build Cache, termasuk alat ukurnya docker buildx du.
  3. RUN --mount. Instruksi RUN dapat memasang cache, secret, bahkan kunci SSH hanya selama instruksi itu berjalan — kemampuan yang tidak pernah dikenal builder lama dan menjadi materi Chapter Cache Mount serta Secret dan SSH Mount.
  4. Heredoc. Skrip multi-baris dapat ditulis langsung di dalam Dockerfile tanpa rantai && yang panjang — kita peragakan di bagian akhir chapter ini.
  5. Frontend yang dapat diperbarui. Cara BuildKit membaca Dockerfile diatur komponen frontend yang bisa diperbarui tanpa menunggu versi daemon baru — kita bedah bersama heredoc.

Melihat Para Builder dengan docker buildx ls #

docker buildx membawa dua konsep yang perlu dipisahkan sejak awal. Builder adalah instance BuildKit yang benar-benar menjalankan pekerjaan build, sedangkan driver menentukan bagaimana instance itu hidup — tertanam di daemon, berjalan sebagai container khusus, atau jauh di mesin lain. Daftar seluruh builder di mesin kita terbaca dengan satu perintah:

bash
$ docker buildx ls

Output yang Dihasilkan:

text
NAME/NODE       DRIVER/ENDPOINT                  STATUS    BUILDKIT   PLATFORMS
default*        docker
 \_ default     \_ default                       running   v0.29.0    linux/amd64

Mari kita telaah lima kolom pada tabel di atas:

  1. NAME/NODE — nama builder beserta node anggotanya; satu builder boleh beranggotakan banyak node, dan baris \_ default yang menjorok adalah node di dalamnya. Tanda * pada default* menandai builder yang sedang aktif dipakai oleh docker build.
  2. DRIVER/ENDPOINT — driver builder dan alamat node-nya. Driver docker berarti BuildKit tertanam di dalam daemon Docker itu sendiri; endpointnya ditulis default karena tidak ada alamat terpisah untuk disebut.
  3. STATUS — keadaan node; running berarti siap menerima pekerjaan build.
  4. BUILDKIT — versi BuildKit yang berjalan pada node tersebut, v0.29.0 pada Engine 29.9.0 kita.
  5. PLATFORMS — arsitektur yang bisa dibangun secara lokal oleh node ini. Laptop Budi menjawab linux/amd64 saja — daftar ini akan membesar segera setelah kita memasang emulator.

Builder default inilah yang selama ini menangani seluruh build kita tanpa satu pun pengaturan.

Membuat Builder docker-container #

Driver docker nyaman untuk kerja harian, tetapi membawa dua batasan: satu build hanya menghasilkan image untuk satu platform, dan hasilnya selalu dituangkan ke image store lokal. Kedua batasan itu diangkat oleh driver kedua — docker-container, yaitu BuildKit yang hidup sebagai container khusus. Kita ciptakan builder baru bernama pembangun dan langsung mengaktifkannya:

bash
$ docker buildx create --name pembangun --use

Output yang Dihasilkan:

text
pembangun

Flag --use memindahkan tanda aktif ke builder baru. Verifikasinya tetap lewat docker buildx ls:

bash
$ docker buildx ls

Output yang Dihasilkan:

text
NAME/NODE        DRIVER/ENDPOINT                  STATUS    BUILDKIT   PLATFORMS
pembangun*       docker-container
 \_ pembangun0   \_ unix:///var/run/docker.sock   running   v0.29.0    linux/amd64
default          docker
 \_ default      \_ default                       running   v0.29.0    linux/amd64

Mari kita telaah perubahan pada tabel di atas:

  1. Tanda * berpindah ke pembangun* — mulai sekarang setiap docker build di terminal ini dikerjakan builder baru.
  2. DRIVER/ENDPOINT menunjuk unix:///var/run/docker.sock — builder ini berbicara ke daemon lewat socket yang sama, tetapi BuildKit-nya kini hidup di dalam container bernama buildx_buildkit_pembangun0; container itu adalah container biasa yang terlihat di docker ps dan boleh kita inspeksi seperti container lain.
  3. Perpindahan builder bersifat per-terminal dan dapat dibatalkan — docker buildx use default mengembalikan builder lama, dan docker buildx rm pembangun membongkar builder beserta containernya.

Driver docker versus docker-container #

Kapan memakai yang mana? Lima aspek berikut merangkum seluruh selisih praktisnya:

AspekDriver docker (bawaan)Driver docker-container
Lokasi buildtertanam di daemon Dockercontainer khusus berisi BuildKit
Hasil build di docker imageslangsung tersediatidak — perlu flag --load atau --push
Build multi-platformtidak, satu platform per buildya, beberapa platform sekaligus
Siap pakaiinstan, tanpa persiapanbutuh bootstrap beberapa detik saat pertama dipakai
Ekspor cache buildterbatas--cache-from dan --cache-to ke registry untuk mesin CI

Aturan praktisnya sederhana: biarkan default mengurus build sehari-hari, dan nyalakan builder docker-container hanya ketika kebutuhan lintas platform atau ekspor cache benar-benar muncul.

Build Multi-Platform dan Manifest List #

Kekuatan pertama builder baru kita: satu perintah menghasilkan image untuk dua arsitektur sekaligus. Bangunan semacam ini membutuhkan dua persiapan. Pertama, emulator QEMU agar mesin amd64 sanggup menjalankan biner arm64 selama build — Docker Desktop sudah membawanya, sedangkan Engine Linux di laptop Budi cukup satu perintah persiapan memakai image tonistiigi/binfmt: docker run --privileged --rm tonistiigi/binfmt --install all. Setelah handler terdaftar di kernel, kolom PLATFORMS milik pembangun membesar menjadi linux/amd64, linux/arm64, linux/arm/v6, .... Persiapan kedua ada di Dockerfile katalog — dua suntingan kecil agar kompilasi Go menyasar arsitektur target:

dockerfile
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
ARG TARGETARCH
WORKDIR /src
COPY *.go go.mod ./
RUN CGO_ENABLED=0 GOARCH=$TARGETARCH go build -o /app/server .

Mari kita telaah dua suntingan kuncinya: --platform=$BUILDPLATFORM memaksa stage builder berjalan di platform mesin build sehingga compiler yang dieksekusi adalah biner amd64 natif, sedangkan GOARCH=$TARGETARCH — variabel yang diisi BuildKit secara otomatis sesuai platform yang sedang dibangun — menentukan arsitektur binary hasilnya. Kompilasi silang Go berjalan natif tanpa penerjemahan emulator; bedah lengkap tiga jalur lintas platform menunggu Chapter Target Platform. Sekarang momen pembuktiannya:

bash
$ cd katalog
$ docker buildx build --platform linux/amd64,linux/arm64 -t budi/katalog:1.0 --push .

Output yang Dihasilkan (dipotong):

text
[+] Building 54.6s (26/26) FINISHED
 => [internal] load build definition from Dockerfile                    0.1s
 => [linux/amd64 1/5] FROM docker.io/library/golang:1.24-alpine         0.0s
 => [linux/arm64 1/5] FROM docker.io/library/golang:1.24-alpine         4.2s
 ...
 => [linux/amd64 4/5] RUN CGO_ENABLED=0 GOARCH=$TARGETARCH go build   10.8s
 => [linux/arm64 4/5] RUN CGO_ENABLED=0 GOARCH=$TARGETARCH go build   11.9s
 ...
 => exporting manifest list sha256:7d41c9a2e8f5b063d1c8a4e7f2b9c5d8e1a4f7b0c3d6e9f2a5b8c1d4e7f0a3b6c9  0.2s
 => => naming to docker.io/budi/katalog:1.0                            0.0s
 => => pushing manifest for docker.io/budi/katalog:1.0                  6.4s

Mari kita telaah baris-baris kuncinya: tiap langkah Dockerfile dieksekusi dua kali — sekali untuk linux/amd64 dan sekali untuk linux/arm64, keduanya berjalan berdampingan berkat eksekusi paralel BuildKit — lalu kedua hasilnya digabungkan menjadi satu manifest list: satu tag budi/katalog:1.0 yang menunjuk daftar image untuk beberapa platform sekaligus. Mesin amd64 yang menarik tag itu menerima varian amd64; mesin arm64 menerima varian arm64 — masing-masing tanpa satu baris konfigurasi tambahan. Flag --push memang wajib pada pola ini: manifest list hidup di registry, bukan di image store lokal — perilaku yang sudah kita tandai pada tabel driver di atas.

Frontend Dockerfile 1.x dan Heredoc #

Seluruh sintaks Dockerfile yang kita tulis diterjemahkan oleh komponen bernama frontend — bagian BuildKit yang membaca berkas Dockerfile lalu menyusun graf langkah build. Parser directive # syntax=docker/dockerfile:1 yang kita biasakan sejak Chapter Struktur Dockerfile mengaktifkan frontend Dockerfile 1.x: frontend ditarik sebagai image dari registry sesuai versi yang diminta, sehingga fitur baru tersedia tanpa menunggu daemon diperbarui. Dua bentuk penulisannya:

Bentuk directivePerilaku
# syntax=docker/dockerfile:1selalu memakai rilis 1.x terbaru dari registry
# syntax=docker/dockerfile:1.7mengunci ke versi minor tertentu demi build yang sangat deterministik

Fitur frontend 1.x yang paling terasa sehari-hari adalah heredoc — skrip multi-baris yang ditulis langsung di dalam instruksi. Tanpa heredoc, RUN panjang ditulis sebagai rantai && agar tetap satu layer:

dockerfile
RUN apt-get update && apt-get install -y --no-install-recommends curl jq && rm -rf /var/lib/apt/lists/*

Dengan heredoc, ketiganya menjadi satu blok skrip yang terbaca seperti di terminal:

dockerfile
RUN <<EOF
apt-get update
apt-get install -y --no-install-recommends curl jq
rm -rf /var/lib/apt/lists/*
EOF

Mari kita telaah bentuk keduanya: penanda <<EOF membuka blok, seluruh baris hingga EOF dieksekusi oleh satu shell dalam satu instruksi RUN — apt-get update dan apt-get install kini seumur hidup dalam satu layer, memenuhi aturan emas Chapter Build Cache tanpa satu pun tanda &&, dan baris yang terlewat saat menyunting tidak lagi memutus seluruh rantai. Heredoc juga bekerja untuk COPY — menulis berkas kecil langsung dari isi Dockerfile:

dockerfile
COPY <<EOF /app/ucapan.txt
Halo dari BuildKit
EOF

Blok di atas melahirkan berkas /app/ucapan.txt berisi satu baris teks tanpa perlu berkas sumber di build context — pas untuk berkas kecil seperti .gitignore atau catatan versi di dalam image.

Kita kini mengenal mesin yang menggerakkan seluruh section ini: BuildKit dengan eksekusi paralel dan cache granular, dikemudikan docker buildx lewat konsep builder dan driver, mampu melahirkan manifest list multi-platform, dan membaca Dockerfile lewat frontend yang dapat diperbarui. Namun ada satu kemampuan BuildKit yang paling mengubah pengalaman build harian dan sengaja kita simpan untuk momen ini: flag --mount pada instruksi RUN — pintu menuju cache mount yang menyimpan unduhan dependensi di luar layer image, sehingga npm ci tidak perlu membayar 9.6 detik setiap kali manifest berubah. Chapter berikutnya mengupasnya tuntas.

Referensi #