Build myapp kini sudah enak sekiranya: context yang terkirim hanya 1.87MB berkat .dockerignore, dan layer-layer lama masih bisa dilayani cache. Namun hasil rebuild terakhir masih menyimpan keganjilan yang kita tandai sendiri: npm ci selama 9.6 detik dieksekusi ulang dari nol hanya karena COPY . . di atasnya kehilangan cache — padahal package-lock.json tidak berubah sedikit pun. Build cepat yang sesungguhnya menuntut satu hal lagi: Dockerfile yang disusun sesuai cara kerja cache. Chapter ini membedah mesin cache BuildKit sampai ke kuncinya — bagaimana cache key tiap instruksi dihitung, apa yang meruntuhkannya, dan bagaimana aturan emas “jarang berubah di atas” memotong waktu build Budi dari 23.9 detik menjadi 1.8 detik — lalu menutupnya dengan alat ukur docker buildx du untuk melihat betapa besar tagihan yang kita hemat.

Setiap Instruksi Punya Cache Key #

BuildKit tidak mengingat build lama sebagai satu blok utuh. Setiap instruksi Dockerfile menghasilkan hasilnya sendiri — satu layer atau satu catatan metadata — dan setiap hasil disimpan dengan sebuah cache key: pengenal yang dihitung dari bahan-bahan tertentu. Saat build berikutnya menemukan instruksi yang cache key-nya sudah ada di simpanan, hasil lama langsung dipakai dan baris CACHED muncul pada output. Bahan-bahan pembentuk kunci itu berbeda menurut jenis instruksinya:

InstruksiKomponen cache keyArtinya bagi kita
RUNTeks instruksi + cache key parentSuntingan sekecil apa pun pada teks perintah membatalkan cache
COPY / ADDChecksum seluruh berkas sumber + cache key parentSatu berkas sumber berubah = seluruh langkah dianggap baru
ENV, LABEL, EXPOSE, CMDTeks instruksi + cache key parentNilai metadata berubah = cache gugur, walau layer kosong

Dua kolom terakhir menyimpan konsekuensi terbesar bagi kecepatan build kita, dan semuanya bermuara pada satu kata: parent.

Tiga Pemicu Invalidasi #

Cache sebuah instruksi gugur — istilahnya, terinvalidasi — ketika salah satu dari tiga hal berikut terjadi:

  1. Teks instruksinya berubah. RUN npm ci menjadi RUN npm ci --omit=dev adalah instruksi baru dengan cache key baru, berapa pun miripnya keduanya.
  2. Isi berkas sumbernya berubah. Checksum package-lock.json yang bergeser satu versi paket saja membuat COPY package.json package-lock.json ./ dianggap berbeda — meski teks instruksinya karakter demi karakter sama.
  3. Cache key parent-nya berubah. Instruksi tidak berdiri sendiri: hasilnya dibangun di atas hasil instruksi sebelumnya, dan cache key parent menjadi bagian dari kunci anak. Parent gugur, anak gugur, cucu ikut gugur — rantai invalidasi menjalar ke seluruh keturunan sampai baris terakhir Dockerfile.

Rantai itulah yang harus kita bayangkan setiap menyusun Dockerfile. Diagram berikut menggambarkan rantai cache untuk Dockerfile myapp versi lama — versi yang COPY . .-nya berada di atas npm ci:

Mari kita telaah jalannya invalidasi pada rantai di atas: saat Budi menyunting server.js untuk mengubah satu sapaan, checksum COPY . . berubah — node C gugur. Karena D membutuhkan hasil C sebagai parent, RUN npm ci ikut gugur meskipun package-lock.json tidak tersentuh; E menyusul setelahnya. Satu suntingan sepele meruntuhkan seluruh paruh bawah Dockerfile dan memaksa npm ci 9.6 detik dijalankan ulang pada setiap iterasi — inilah biaya tersembunyi urutan yang salah.

Aturan Emas: Jarang Berubah di Atas #

Obatnya satu kalimat: taruh instruksi yang jarang berubah di atas, dan yang sering berubah di bawah. Budi menata ulang Dockerfile myapp — hanya menukar posisi, tidak menambah konsep baru:

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

Mari kita telaah tiga perubahan kunci pada susunan baru di atas:

  1. COPY package.json package-lock.json ./ naik ke atas — dua berkas manifest ini berubah hanya saat dependensi sengaja ditambah atau dinaikkan versinya, kejadian yang jarang dibanding suntingan kode.
  2. RUN npm ci menempel tepat di bawahnya — cache key-nya kini hanya bergantung pada checksum dua manifest dan parent di atasnya; suntingan server.js tidak lagi menyentuhnya.
  3. COPY . . turun ke bawah — langkah yang paling sering berubah kini berada di puncak rantai invalidasi: ia boleh gugur sesering apa pun tanpa menyeret npm ci ke kuburannya. Bersama .dockerignore dari chapter sebelumnya, COPY . . kini hanya membawa berkas aplikasi yang ramping.

Sebelum dan Sesudah: 23.9 Detik Menjadi 1.8 Detik #

Mari kita ukur dampaknya dengan jujur, termasuk biaya transisinya. Build pertama dengan urutan baru tetap harus mengeksekusi segalanya — teks Dockerfile berubah membuat seluruh rantai dianggap baru — jadi build ini yang mengisi cache untuk keturunannya:

bash
$ docker build -t myapp:1.0 .

Output yang Dihasilkan:

text
[+] Building 11.6s (14/14) FINISHED
 => [internal] load build definition from Dockerfile                    0.1s
 => => transferring dockerfile: 224B                                    0.0s
 => [internal] load .dockerignore                                       0.0s
 => => transferring context: 80B                                        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                                       0.1s
 => => transferring context: 1.87MB                                     0.1s
 => [1/6] FROM docker.io/library/node:22-alpine                         0.0s
 => => resolve docker.io/library/node:22-alpine                         1.2s
 => [2/6] WORKDIR /app                                                  0.0s
 => [3/6] COPY package.json package-lock.json ./                        0.1s
 => [4/6] RUN npm ci                                                   9.6s
 => [5/6] COPY . .                                                      0.1s
 => [6/6] EXPOSE 8000                                                   0.0s
 => exporting to image                                                  0.7s
 => => exporting layers                                                 0.5s
 => => writing image sha256:7b2e94c1f8a5d209c3b46e1f7a2b5c8d9e04f173a6b5c8d2e9f4a1b7c3d5e8f 0.1s
 => => naming to docker.io/library/myapp:1.0                            0.0s

Build pengisi cache di atas menghabiskan 11.6 detik — npm ci 9.6 detik dijalankan satu kali terakhir untuk melahirkan layer dependensi di posisi barunya. Tag myapp:1.0 kini pindah ke image hasil urutan baru, dan image lama kehilangan namanya menjadi dangling, persis skenario Chapter Melihat Image Lokal. Sesudah investasi sekali itu, build berikutnya tanpa perubahan apa pun berjalan sebagai berikut:

bash
$ docker build -t myapp:1.0 .

Output yang Dihasilkan:

text
[+] Building 1.8s (14/14) FINISHED
 => [internal] load build definition from Dockerfile                    0.1s
 => => transferring dockerfile: 224B                                    0.0s
 => [internal] load .dockerignore                                       0.0s
 => => transferring context: 80B                                        0.0s
 => [internal] load metadata for docker.io/library/node:22-alpine       0.7s
 => [auth] node:22-alpine pull token from registry-1.docker.io          0.5s
 => [internal] load build context                                       0.1s
 => => transferring context: 1.87MB                                     0.1s
 => CACHED [1/6] FROM docker.io/library/node:22-alpine                  0.0s
 => => resolve docker.io/library/node:22-alpine                         0.7s
 => CACHED [2/6] WORKDIR /app                                           0.0s
 => CACHED [3/6] COPY package.json package-lock.json ./                 0.0s
 => CACHED [4/6] RUN npm ci                                             0.0s
 => CACHED [5/6] COPY . .                                               0.0s
 => CACHED [6/6] EXPOSE 8000                                            0.0s
 => exporting to image                                                  0.2s
 => => exporting layers                                                 0.0s
 => => writing image sha256:7b2e94c1f8a5d209c3b46e1f7a2b5c8d9e04f173a6b5c8d2e9f4a1b7c3d5e8f 0.1s
 => => naming to docker.io/library/myapp:1.0                            0.0s

Mari kita telaah hasil pengukuran sebelum dan sesudah penyusunan ulang:

  1. RUN npm ci: 9.6s menjadi CACHED 0.0s — langkah termahal kini dilayani dari simpanan cache; tidak ada satu paket pun yang diunduh atau dipasang ulang.
  2. Total build: 23.9s menjadi 1.8s — dari waktu build pertama Budi di awal section ke iterasi harian yang sehat; sisanya hanya tarif langkah internal (metadata basis, token, transfer context 1.87MB) dan ekspor image.
  3. Skenario suntingan kode kini murah. Saat Budi mengubah server.js dan membangun ulang, hanya COPY . . yang dieksekusi (0.1s) — COPY manifest dan npm ci tetap CACHED karena bahan kuncinya tidak tersentuh; build iterasi berlangsung sekitar dua detik, tinggal berapa lama langkah internal berjalan.
  4. Urutan mengubah kecepatan, bukan isi image — image hasil urutan baru berukuran sama dengan hasil urutan lama pasca-.dockerignore (sekitar 278MB, dengan dependensi kini tercatat sekali di layer npm ci); yang berubah hanyalah ID sha256:7b2e94c1... karena susunan layer dan riwayatnya berbeda.

Instruksi RUN yang Merusak Cache Sendiri #

Ada satu kategori instruksi yang menggagalkan aturan emas dari dalam: RUN yang hasilnya berbeda setiap dieksekusi meski teksnya tidak pernah berubah.

Perbandingan berikut memperlihatkan kedua kutubnya. Bentuk pertama menanam jam sistem ke dalam layer setiap kali cache gugur:

dockerfile
RUN date -u +"%Y-%m-%dT%H:%M:%SZ" > /app/waktu-build.txt

Bentuk kedua menerima waktu dari luar sebagai argumen build — argumen yang dipakai menjadi bagian cache key, sehingga hasilnya terkendali dan tercatat:

dockerfile
ARG WAKTU_BUILD=belum-diset
RUN echo "dibangun: ${WAKTU_BUILD}" > /app/waktu-build.txt
bash
$ docker build --build-arg WAKTU_BUILD=2026-10-13T09:41:30Z -t myapp:1.0 .

Mari kita telaah pasangan di atas: teks instruksi RUN echo ... tidak pernah berubah, nilai WAKTU_BUILD yang menentukan isi layer — ganti argumennya, cache key ikut berubah secara sadar dan tercatat pada riwayat build; biarkan kosong, nilai bawaan belum-diset yang terpakai. Pola argumen-build-ini-lah yang kita andalkan nanti untuk menyuntikkan nomor versi atau sidik jari commit tanpa merusak reproducibility.

–no-cache dan –pull #

Dua flag build memangkas cache secara sadar, masing-masing untuk keadaan yang berbeda:

  1. --no-cache mengabaikan seluruh simpanan cache untuk build itu — setiap instruksi dieksekusi dari nol. Pakai ketika kita menduga hasil build tidak konsisten dengan isi Dockerfile (debugging klasik “di mesinku jalan”), ketika menguji bahwa image benar-benar bisa dibangun dari awal, atau ketika instruksi RUN bergantung pada keadaan luar yang diam-diam berubah.
  2. --pull memaksa FROM selalu menarik versi terbaru dari registry alih-alih memakai image basis lokal. Ingat pelajaran Chapter Memahami Image Tag: tag bisa berpindah — basis lokal kita mungkin tertinggal dari yang ditunjuk tag hari ini. Flag ini wajar dipakai berkala, dan lazim dinyalakan pada build CI agar basis selalu segar.

Keduanya boleh dipadukan — docker build --no-cache --pull -t myapp:audit . adalah audit penuh dari nol — dengan kesadaran bahwa harganya adalah 23.9 detik semacam build pertama kita, bukan 1.8 detik.

Mengukur Dampak Cache dengan docker buildx du #

Sejauh ini kita mengukur cache lewat durasi build. BuildKit juga menyimpan daftar rekam simpanannya, dan docker buildx du membacanya:

bash
$ docker buildx du

Output yang Dihasilkan:

text
ID                        RECLAIMABLE     SIZE      LAST ACCESSED
m4x7h2v9qk3w5c8r1z6t0y4e* true            148.2MB   About an hour ago
p9n2b5f8j4c7v1h6k3s0d8g2* true            146.2MB   About an hour ago
w3e8r5t1y7u9i2o5p8a1s4d6* false           146.2MB   2 minutes ago
q1a6z3x8c5v2b7n4m9k2j5h0f* false           130.1MB   2 minutes ago

Total:  4 records, 570.7MB

Mari kita telaah isi tabel di atas:

  1. Empat rekaman cache beserta ukurannya — dua rekaman berumur sekitar satu jam adalah sisa rantai urutan lama: layer COPY . . (148.2MB yang masih memuat node_modules) dan layer npm ci lama (146.2MB); dua rekaman muda adalah rantai urutan baru yang masih dipakai image myapp:1.0.
  2. Kolom RECLAIMABLE — rekaman bertanda true tidak lagi dirujuk build mana pun; docker buildx du menghitungnya sebagai ruang yang boleh direbut kembali. Rekaman false masih menjadi tulang punggung image aktif dan cache harian.
  3. LAST ACCESSED — usia terakhir rekaman dipakai; rekaman berumur berhari-hari dengan ukuran besar adalah kandidat pembersihan alami berikutnya.
  4. Baris Total — ringkasan seluruh simpanan: empat rekaman, 570.7MB. Bandingkan dengan image aktifnya yang 278MB — hampir separuh simpanan kita adalah sejarah.

Pembersihannya satu perintah: docker builder prune mengambil seluruh rekaman yang RECLAIMABLE:

bash
$ docker builder prune

Output yang Dihasilkan:

text
Total reclaimed space: 294.4MB

Dua rekaman lama senilai 294.4MB (148.2MB + 146.2MB) terhapus; rantai cache baru tidak tersentuh sehingga iterasi harian tetap berjalan 1.8 detik. Perhatikan bahwa builder prune hanya bicara soal cache — membersihkan image seperti myapp:dev lama tetap tugas docker rmi, urusan yang kita tuntaskan di Section Docker Image.

Flag Terkait Cache #

Empat flag berikut mengendalikan perilaku cache pada docker build; dua yang pertama sudah kita kenal, dua yang terakhir menunggu section berikutnya:

FlagEfekKapan dipakai
--no-cacheMengabaikan seluruh cache lokalAudit build dari nol; mencurigai hasil cache basi
--pullMenarik ulang basis dari registryMenyegarkan image basis yang tag-nya bisa berpindah
--cache-from type=registry,ref=...Mengimpor cache dari registryBuild di CI yang mulai dari mesin kosong
--cache-to type=registry,ref=...Mengekspor cache ke registryMenyiapkan cache bagi mesin CI berikutnya

Pasangan --cache-from dan --cache-to type=registry menjawab satu kelemahan cache lokal: mesin CI baru tidak punya simpanan apa pun, sehingga npm ci 9.6 detik harus dibayar setiap pipeline. Dengan mengekspor cache ke registry sebagai artefak, build berikutnya — di mesin mana pun — bisa mengimpornya kembali dan melanjutkan dari CACHED. Detail pemakaiannya kita bahas tuntas pada Section CI/CD dan Automation.

Build myapp kini sehat di kedua sisi: transfer 1.87MB dan cache yang menopang iterasi 1.8 detik. Namun satu bilangan masih belum beranjak: image 278MB untuk aplikasi yang berjalan sebagai satu proses Node. Angka itu adalah sifat base node:22-alpine — runtime yang membawa seluruh Node.js — dan bukan urusan lagi urutan instruksi. Mengecilkan image menuntut pendekatan berbeda: pisahkan tahap yang membutuhkan alat berat dari tahap yang benar-benar berjalan, lalu salin hanya hasil akhirnya. Teknik multi-stage build itu kita kuasai pada chapter berikutnya, bersama proyek kedua Budi yang ditulis dengan Go.

Referensi #