Section Dockerfile kita tutup dengan bekal lengkap: tujuh belas instruksi sudah dikenal satu per satu, dan Budi berhasil merakit image myapp yang bisa dijalankan di mesin mana pun. Namun sepanjang perjalanan itu, perintah yang menggerakkan semuanya — docker build — tetap kita perlakukan seperti mantra: diketik, ditunggu, lalu image muncul. Ceritanya pun kini berubah: myapp tumbuh menjadi aplikasi sungguhan dengan dependensi runtime dan alat pengembangan, folder proyeknya memuat node_modules sebesar 146MB hasil npm install, dan build yang tadinya selesai dalam hitungan detik kini memakan waktu hampir 24 detik. Section Image Build dan Optimisasi dibuka dengan membongkar kap mesin docker build itu sendiri: apa yang sebenarnya terjadi dari saat kita menekan Enter sampai image lahir, flag-flag apa yang mengendalikannya, dan output BuildKit mana yang perlu kita baca — fondasi bagi dua tujuan section ini, build yang cepat dan image yang ramping.

Anatomi Perintah docker build #

Mari kita mulai dari perintah yang paling sering kita ketik sepanjang course ini, kini dibedah kata demi kata:

bash
$ docker build -t myapp:1.0 .

Mari kita telaah tiga bagian perintah di atas:

  1. docker build — subperintah untuk membangun image dari Dockerfile dan build context. Sejak Docker Engine 23.0, perintah ini berjalan di atas BuildKit sebagai engine build bawaan; Docker Engine 29.9.0 yang kita pakai melanjutkan tradisi yang sama.
  2. -t myapp:1.0 — flag --tag dalam bentuk pendek; memberi nama repositori dan tag pada image hasil build, dengan pola repositori:tag yang sudah kita kenal pada Chapter Memahami Image Tag. Tanpa flag ini, image tetap dibangun tetapi hanya bisa dirujuk lewat ID — tag myapp:1.0 adalah alamat yang nyaman untuk docker run dan docker push.
  3. . (satu titik) — argumen PATH, yaitu build context: folder yang isinya dikirim ke builder sebagai bahan mentah build. Titik berarti “folder saat ini”; seluruh isi folder inilah yang nanti terlihat sebagai 148.21MB pada output build pertama kita.

Sebelum membangun, mari kita lihat dulu Dockerfile yang akan dieksekusi. myapp versi baru memuat package-lock.json, sehingga pemasangan dependensi kini menjadi bagian dari build:

dockerfile
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci
EXPOSE 8000
CMD ["node", "server.js"]

Mari kita telaah keenam instruksi di atas:

  1. FROM node:22-alpine — basis yang sama dengan build pertama Budi pada Chapter Membangun Image dengan Dockerfile; seluruh layer Node.js 22 menjadi fondasi tumpukan.
  2. WORKDIR /app — direktori kerja untuk instruksi-instruksi setelahnya; path relatif dari COPY dan RUN mengacu ke sini.
  3. COPY . . — menyalin seluruh isi build context ke /app. Perhatikan bahwa bentuk ini sengaja belum dioptimasi: node_modules sebesar 146MB ikut tersalin, padahal baris berikutnya memasang dependensi sendiri. Pola seperti ini sangat umum di Dockerfile pemula, dan justru karena itu kita mempertahankannya — inilah bahan percobaan yang akan kita perbaiki bertahap pada chapter-chapter berikutnya.
  4. RUN npm ci — memasang dependensi persis seperti yang tercatat di package-lock.json, tanpa negosiasi versi; berbeda dari npm install, npm ci menghapus folder node_modules yang ada lebih dulu lalu memasang ulang seluruhnya dengan angka versi yang terkunci.
  5. EXPOSE 8000 — catatan metadata bahwa aplikasi mendengarkan port 8000, persis dokumentasi port yang kita bahas pada Chapter EXPOSE dan Port.
  6. CMD ["node", "server.js"] — perintah bawaan container saat start, dalam exec form yang kita bahas pada Chapter CMD dan ENTRYPOINT.

Build Pertama: Membaca Output BuildKit #

Budi berdiri di dalam folder myapp lalu menjalankan build pertama untuk versi baru ini. Output yang muncul di terminal terlihat panjang, tetapi setiap barisnya bermakna:

bash
$ docker build -t myapp:1.0 .

Output yang Dihasilkan:

text
[+] Building 23.9s (13/13) FINISHED
 => [internal] load build definition from Dockerfile                    0.1s
 => => transferring dockerfile: 176B                                    0.0s
 => [internal] load .dockerignore                                       0.0s
 => => transferring context: 2B                                         0.0s
 => [internal] load metadata for docker.io/library/node:22-alpine       1.2s
 => [auth] node:22-alpine pull token from registry-1.docker.io          0.9s
 => [internal] load build context                                      11.9s
 => => transferring context: 148.21MB                                  11.9s
 => [1/5] FROM docker.io/library/node:22-alpine                         0.0s
 => => resolve docker.io/library/node:22-alpine                         1.2s
 => [2/5] WORKDIR /app                                                  0.0s
 => [3/5] COPY . .                                                      0.7s
 => [4/5] RUN npm ci                                                   9.6s
 => [5/5] EXPOSE 8000                                                   0.0s
 => exporting to image                                                  1.2s
 => => exporting layers                                                 0.9s
 => => writing image sha256:3e8c1a5f9b42d687f0a19c3b5e72d84f6b91c0e4a2d7f38b5c6019e4d2f7a83b 0.1s
 => => naming to docker.io/library/myapp:1.0                            0.0s

Mari kita telaah baris-baris output di atas satu per satu:

  1. [+] Building 23.9s (13/13) FINISHED — baris ringkasan: build berjalan 23,9 detik, 13 langkah internal dan Dockerfile diselesaikan, status akhirnya FINISHED. Dua penyumbang waktu terbesar sudah terlihat: transfer context 11,9 detik dan npm ci 9,6 detik.
  2. load build definition + transferring dockerfile: 176B — BuildKit membaca berkas Dockerfile sepanjang 176 byte sebagai definisi build; inilah resep yang akan dievaluasi.
  3. load .dockerignore + transferring context: 2B — langkah pencarian berkas penyaring context. Berkasnya belum ada di folder myapp — kita baru membuatnya pada Chapter .dockerignore — namun langkahnya tetap tampil dengan transfer 2 byte sebagai penanda bahwa penyaringnya kosong.
  4. load metadata + [auth] pull token — BuildKit bertanya ke Docker Hub digest mana yang saat ini ditunjuk tag node:22-alpine, lalu mengambil token unduhan sebagai pengunjung anonim. Tag yang bisa berpindah adalah jebakan lama dari Chapter Memahami Image Tag, dan verifikasi metadata inilah gerbangnya.
  5. [1/5] FROM dengan durasi 0.0s — basis tidak diunduh ulang karena node:22-alpine sudah tersimpan lokal sejak Section Dockerfile; baris resolve 1,2 detik adalah pengesahan bahwa digest lokal masih cocok dengan metadata terbaru.
  6. load build context + transferring context: 148.21MB selama 11.9s — hampir separuh waktu build habis hanya untuk mengirim isi folder ke builder. Angka 148.21MB itu adalah seluruh isi folder myapp — termasuk node_modules 146MB yang sebenarnya tidak dibutuhkan utuh oleh builder. Kenapa seluruhnya terkirim padahal akhirnya disalin juga? Itu pertanyaan besar yang kita jawab tuntas pada Chapter Build Context.
  7. [3/5] COPY . . dan [4/5] RUN npm ci — dua langkah termahal kedua setelah transfer: penyalinan seluruh context menjadi satu layer, lalu pemasangan dependensi 9,6 detik yang melahirkan layer kedua. Perhatikan bahwa EXPOSE tampil sebagai langkah bernomor [5/5] meski selesai seketika, sedangkan CMD tidak tampil sebagai langkah bernomor — isi instruksi tersebut melebur ke konfigurasi image saat ekspor, persis kebiasaan yang kita kenal pada Chapter Membangun Image dengan Dockerfile.
  8. exporting to image hingga naming to — seluruh layer dirakit menjadi image final dengan ID sha256:3e8c1a5f..., lalu tag myapp:1.0 ditempelkan. Angka [1/5] sampai [5/5] di sisi kiri menghitung instruksi yang membentuk langkah bernomor; langkah-langkah [internal] berjalan di luar penomoran tersebut.

Image Baru di Penyimpanan Lokal #

Build selesai, dan Budi memeriksa hasilnya di penyimpanan lokal:

bash
$ docker images myapp

Output yang Dihasilkan:

text
REPOSITORY   TAG      IMAGE ID       CREATED         SIZE
myapp        1.0      3e8c1a5f9b42   1 minute ago    424MB
myapp        dev      105c9de1d398   2 days ago      85.4MB

Mari kita telaah dua baris di atas:

  1. myapp:1.0 seukuran 424MB — image baru yang gemuk. node_modules tercatat di dua layer: sekali pada layer COPY . . (148MB hasil salinan), dan sekali lagi pada layer RUN npm ci (146MB hasil pemasangan ulang) — layer lama tidak bisa diubah, sehingga keduanya menumpuk di dalam image. Detail mekanisme tumpukan ini adalah materi Chapter Konsep Image Layer; obatnya menyusul pada Chapter .dockerignore dan Build Cache.
  2. myapp:dev 85.4MB berumur dua hari — sisa iterasi harian Budi dari masa Section Dockerfile, image 105c9de1d398 yang masih tersimpan dan menyita ruang. Image myapp:1.0 lama dari chapter tersebut kini kehilangan tagnya setelah tag 1.0 pindah ke build baru — skenario dangling yang persis kita saksikan pada Chapter Melihat Image Lokal.
  3. IMAGE ID 3e8c1a5f9b42 — 12 karakter pertama dari sha256:3e8c1a5f... pada output build; output BuildKit dan tabel docker images menunjuk image yang sama.

Cara Kerja BuildKit di Balik Layar #

Output di atas menjadi jauh lebih mudah dibaca bila kita tahu alur kerja engine yang menghasilkannya. BuildKit tidak mengeksekusi Dockerfile baris demi baris seperti pembaca; ia memprosesnya melalui empat tahap berikut:

Mari kita telaah keempat tahap pada diagram di atas:

  1. Tahap 1 — resolve frontend. Frontend adalah komponen yang tahu cara membaca Dockerfile; BuildKit menyelesaikan frontend ini lebih dulu, lengkap dengan versinya. Berkat desain ini, fitur Dockerfile baru bisa dipakai tanpa menunggu Docker Engine diperbarui — kita manfaatkan hal ini pada Chapter Buildx dan BuildKit.
  2. Tahap 2 — evaluasi DAG instruksi. Frontment mengubah Dockerfile menjadi graf ketergantungan (DAG): tiap instruksi menjadi satu node, dan panah antar-node menyatakan “aku menunggu hasilmu”. RUN npm ci menunggu COPY . .; COPY . . menunggu FROM dan context; instruksi yang tidak saling menunggu dikenali sebagai kandidat paralel.
  3. Tahap 3 — eksekusi paralel. BuildKit menjalankan node-node yang siap secara bersamaan — itulah sebabnya load metadata, load build context, dan load .dockerignore pada output kita berjalan hampir bersamaan, bukan berurutan. Eksekusi tiap node menghasilkan diff layer atau metadata, dan hasilnya disimpan sebagai build cache.
  4. Tahap 4 — export image. Node terakhir selesai, seluruh hasil dirakit menjadi image: layer-layer diekspor ke penyimpanan lokal, ID sha256 dihitung dari isinya, lalu tag ditempelkan — baris exporting layers, writing image sha256, dan naming to yang kita lihat di akhir output.

Flag yang Paling Sering Dipakai #

Perintah docker buildx build membawa puluhan flag, tetapi delapan berikut menutupi hampir semua kebutuhan harian:

FlagFungsiContoh pemakaian
-tMemberi nama image; boleh diulang untuk beberapa tag sekaligusdocker build -t myapp:1.0 -t myapp:latest .
-fMembaca Dockerfile dari berkas laindocker build -f Dockerfile.prod -t myapp:prod .
--build-argMengisi nilai ARG saat builddocker build --build-arg WAKTU_BUILD=2026-10-13T09:41:30Z .
--targetBerhenti pada stage tertentu di Dockerfile multi-stagedocker build --target tester -t katalog:test .
--no-cacheMengabaikan seluruh build cachedocker build --no-cache -t myapp:audit .
--pullSelalu menarik ulang image basis terbarudocker build --pull -t myapp:1.0 .
--labelMenempel label metadata pada imagedocker build --label versi=1.0 .
--progressMengatur gaya output: auto, tty, plain, rawdocker build --progress=plain .

Flag -t, -f, dan --progress kita demonstrasikan langsung di bawah; --build-arg dan --no-cache menjadi pemain utama pada Chapter Build Cache, sedangkan --target menunggu chapter multi-stage bersama proyek baru bernama katalog.

Demo: Satu Build Dua Tag #

Budi kini ingin rilis sekaligus menandai versi termutakhir, jadi ia menjalankan satu build dengan dua flag -t:

bash
$ docker build -t myapp:1.0 -t myapp:latest .

Output yang Dihasilkan:

text
[+] Building 12.3s (13/13) FINISHED
 => [internal] load build definition from Dockerfile                    0.1s
 => => transferring dockerfile: 176B                                    0.0s
 => [internal] load .dockerignore                                       0.0s
 => => transferring context: 2B                                         0.0s
 => [internal] load metadata for docker.io/library/node:22-alpine       1.1s
 => [auth] node:22-alpine pull token from registry-1.docker.io          0.8s
 => [internal] load build context                                      11.9s
 => => transferring context: 148.21MB                                  11.9s
 => CACHED [1/5] FROM docker.io/library/node:22-alpine                  0.0s
 => => resolve docker.io/library/node:22-alpine                         1.1s
 => CACHED [2/5] WORKDIR /app                                           0.0s
 => CACHED [3/5] COPY . .                                               0.0s
 => CACHED [4/5] RUN npm ci                                             0.0s
 => CACHED [5/5] EXPOSE 8000                                            0.0s
 => exporting to image                                                  0.3s
 => => exporting layers                                                 0.0s
 => => writing image sha256:3e8c1a5f9b42d687f0a19c3b5e72d84f6b91c0e4a2d7f38b5c6019e4d2f7a83b 0.1s
 => => naming to docker.io/library/myapp:1.0                            0.0s
 => => naming to docker.io/library/myapp:latest                         0.0s

Mari kita telaah temuan penting pada output kedua ini:

  1. Seluruh langkah Dockerfile tampil CACHED — FROM hingga EXPOSE dilayani dari build cache karena isinya sama persis dengan build sebelumnya; npm ci yang tadinya 9,6 detik kini selesai 0,0 detik tanpa dieksekusi ulang.
  2. Transfer context TETAP penuh: 148.21MB selama 11.9 detik. Inilah temuan terpenting demo ini: build cache bekerja pada level instruksi, bukan pada level context. node_modules sebesar 146MB tetap dikirim lengkap ke builder setiap kali kita membangun — meski tidak satu byte pun darinya dibutuhkan lagi. Angka ini akan kita bongkar mekanismenya pada Chapter Build Context dan jinakkan pada Chapter .dockerignore.
  3. Dua baris naming to — satu image yang sama (sha256 identik dengan build pertama) kini menjawab dua nama: myapp:1.0 dan myapp:latest. Input yang identik menghasilkan image yang identik — sifat content-addressable dari Chapter Konsep Image Layer.
  4. Total waktu 12.3 detik — hampir seluruhnya adalah tagihan transfer context; bandingkan dengan 23,9 detik build pertama yang memuat eksekusi npm ci. Begitu transfer dijinakkan, build seperti ini akan turun ke kisaran dua detik.

Dockerfile Alternatif dan Output untuk CI #

Dua flag terakhir yang kita janjikan tadi menyelesaikan dua kebutuhan nyata. Flag -f membaca definisi build dari berkas selain Dockerfile — Budi misalnya menyimpan varian produksi bernama Dockerfile.prod di folder yang sama:

bash
$ docker build -f Dockerfile.prod -t myapp:prod .

Output yang Dihasilkan:

text
[+] Building 2.1s (13/13) FINISHED
 => [internal] load build definition from Dockerfile.prod               0.1s
 => => transferring dockerfile: 189B                                    0.0s
 => [internal] load metadata for docker.io/library/node:22-alpine       1.1s
 => CACHED [1/5] FROM docker.io/library/node:22-alpine                  0.0s
...
 => => naming to docker.io/library/myapp:prod                           0.0s

Yang penting dicatat: flag -f hanya mengubah dari mana definisi dibaca, bukan di mana context dicari — titik di akhir perintah tetap menentukan foldernya, dan di situlah letak kejutan yang sudah kita alami pada Chapter COPY dan ADD ketika context dan lokasi Dockerfile tidak lagi sejajar.

Adapun flag --progress=plain mengubah output dari tampilan animasi terminal menjadi baris log mentah yang bernomor — format yang ramah bagi lingkungan tanpa terminal interaktif:

bash
$ docker build --progress=plain -t myapp:1.0 .

Output yang Dihasilkan:

text
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 176B done
#1 DONE 0.1s
#2 [internal] load .dockerignore
#2 transferring context: 2B done
#2 DONE 0.0s
#3 [internal] load metadata for docker.io/library/node:22-alpine
#3 DONE 1.1s
#4 [internal] load build context
#4 transferring context: 148.21MB 11.9s done
#4 DONE 11.9s
#5 [4/5] RUN npm ci
#5 0.412 npm warn deprecated [email protected]: This module is not supported...
#5 9.412 added 812 packages in 9.6s
...

Mari kita telaah dua manfaat format plain di atas:

  1. Baris log instruksi ikut tampil — pada mode TTY, keluaran npm ci tersembunyi di balik satu angka durasi; mode plain menampilkan peringatan dan ringkasan npm baris demi baris dengan awalan nomor langkah dan stempel waktu, sehingga kegagalan build bisa dilacak ke baris log persisnya.
  2. Cocok untuk CI — server CI tidak punya terminal interaktif; baris log datar seperti ini tersimpan rapi sebagai artefak pipeline. Kita akan memakai mode ini lagi pada Chapter Build Context justru untuk membuktikan sesuatu yang mengejutkan tentang context.

Kita kini mengenal anatomi perintahnya, membaca outputnya, dan memahami empat tahap kerja BuildKit di baliknya. Namun satu angka tadi terus mengganjal: 148.21MB terkirim setiap build, baik saat build pertama maupun saat seluruh langkah sudah CACHED. Kenapa builder bersikeras menerima isi folder yang bahkan tidak dibutuhkannya? Jawabannya ada pada mekanisme build context — argumen titik di ujung perintah kita — yang kita bedah tuntas pada Chapter Build Context.

Referensi #