Chapter Buildx dan BuildKit ditutup dengan satu kabar penting: builder modern yang sekarang menjadi default bukan sekadar pengganti yang lebih cepat, melainkan pintu masuk ke fitur Dockerfile yang tidak pernah dikenal builder lama. Fitur-fitur itu diekspresikan lewat flag --mount pada instruksi RUN, dan buah pertamanya kita petik sekarang. Masalah yang hendak dipecahkan sudah akrab bagi Budi: berkat urutan manifest-dulu dari Chapter Build Cache, npm ci memang tetap CACHED selama hanya server.js yang berubah, tetapi layer cache itu rapuh — cukup satu baris di package.json berubah, seluruh unduhan dependensi diulang dari nol. Chapter ini memperkenalkan cache mount: tempat penyimpanan sementara yang hidup di builder, di luar layer image, sehingga cache bertahan justru pada saat layer cache runtuh.

Layer Cache yang Mudah Runtuh #

Mari kita rekap dulu posisi terakhir myapp. Sejak Chapter Build Cache, Dockerfile myapp memakai urutan yang sudah dirapikan: manifest dependensi disalin lebih dulu, npm ci berdiri di atasnya, dan kode aplikasi menyusul di belakang. Selama Budi hanya menyunting server.js, instruksi COPY package.json package-lock.json ./ tidak berubah, jadi RUN npm ci dilayani dari layer cache dengan tanda CACHED, dan build selesai di bawah satu detik. Mekanisme ini bekerja karena BuildKit membandingkan checksum berkas manifest dan teks instruksi sebelum memutuskan eksekusi ulang.

Rapuhnya terletak pada syaratnya: validitas layer cache mensyaratkan seluruh layer di atasnya tidak berubah, sedangkan package.json adalah berkas yang memang berubah rutin. Setiap kali Budi menambahkan satu pustaka, menaikkan versi satu dependensi, atau sekadar menaikkan versi myapp sendiri, checksum manifest berubah, layer COPY runtuh, dan RUN npm ci dieksekusi ulang dari awal. Budi mengalaminya saat menambahkan fitur halaman /waktu: ia menyunting server.js sekaligus menambahkan pustaka dayjs ke package.json, lalu membangun versi baru:

bash
$ cd myapp
$ docker build -t myapp:1.3 .

Output yang Dihasilkan:

text
[+] Building 11.2s (9/9) FINISHED
 => [internal] load build definition from Dockerfile                    0.1s
 => => transferring dockerfile: 201B                                    0.0s
 => [internal] load metadata for docker.io/library/node:22-alpine       0.8s
 => [1/6] FROM docker.io/library/node:22-alpine                         0.0s
 => => resolve docker.io/library/node:22-alpine                         0.8s
 => CACHED [2/6] WORKDIR /app
 => [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.5s
 => => exporting layers                                                 0.3s
 => => writing image sha256:765830f3e9cdf75f9d4a415a0d07776628cfac80bdd8e9e0bb1fb1a760b7fb71 0.1s
 => => naming to docker.io/library/myapp:1.3                            0.0s

Mari kita telaah output di atas:

  1. CACHED [2/6] WORKDIR /app — layer-layer yang inputnya tidak tersentuh masih dilayani dari cache; kerusakan hanya menjalar dari titik perubahan ke bawah, persis aturan linear yang kita bedah pada Chapter Build Cache.
  2. [3/6] COPY package.json package-lock.json ./ dieksekusi ulang — checksum kedua manifest berubah karena dayjs masuk ke daftar dependensi, jadi salinan lama tidak lagi berlaku.
  3. [4/6] RUN npm ci makan 9,6 detik — konsekuensi berantai dari runtuhnya layer salinan; npm mengunduh ulang seluruh dependensi myapp dari registry — ratusan paket tercatat di package-lock.json — padahal hampir semuanya identik dengan unduhan sebelumnya.
  4. [5/6] COPY . . ikut dieksekusi ulang — setelah satu layer runtuh, semua layer di bawahnya dalam stage yang sama dieksekusi ulang; suntingan server.js dan package.json memang keduanya perlu disalin, jadi baris ini wajar.
  5. Angka 9,6 detik yang hilang — pada skala myapp masih terasa ringan, tetapi bayangkan proyek dengan ratusan dependensi atau tim yang menaikkan versi dependensi setiap pekan; waktu tunggu itu dikalikan jumlah developer dan jumlah mesin CI.

Perhatikan juga keanehan struktural di balik angka tersebut: hasil unduhan npm sebenarnya pernah “tersimpan” di layer image lama, tetapi penyimpanan itu tak bisa dipakai ulang karena melekat pada rantai layer yang sudah tidak berlaku. Singkatnya, layer image adalah wadah cache yang salah — cache terkunci pada sejarah build, bukan pada isi datanya.

Cache Mount: Cache yang Hidup di Luar Layer #

BuildKit menawarkan wadah yang tepat: cache mount. Bentuknya adalah flag --mount=type=cache,target=<path> pada instruksi RUN, yang memerintahkan builder untuk me-mount sebuah direktori cache pada path tersebut hanya selama instruksi itu berjalan. Isi cache disimpan oleh builder di luar layer image, dan tetap tersimpan dari build ke build, sehingga eksekusi ulang npm ci tinggal mengambil paket dari cache alih-alih mengunduhnya lagi. Dockerfile myapp kini terlihat seperti ini:

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

Mari kita telaah flag --mount pada baris keempat:

  1. type=cache — jenis mount: direktori cache milik builder. Jenis lain (secret dan ssh) menyusul pada chapter berikutnya, keduanya memakai kerangka flag yang sama.
  2. target=/root/.npm — path di dalam container build tempat cache di-mount; npm menyimpan tarball yang diunduhnya di /root/.npm (rumah user root), jadi path ini tepat berada di lokasi yang dibaca npm.
  3. Dua dunia yang terpisah — selama npm ci berjalan, /root/.npm tampak seperti folder biasa dan terisi tarball; begitu instruksi selesai, mount dilepas, layer hanya berisi hasil node_modules, sementara tarball tinggal di penyimpanan builder menunggu build berikutnya.

Build pertama setelah flag dipasang tetap mengunduh penuh karena cache masih kosong — tidak ada sihir pengisian mundur:

text
 => [3/6] COPY package.json package-lock.json ./                        0.1s
 => [4/6] RUN --mount=type=cache,target=/root/.npm npm ci               9.6s
 => [5/6] COPY . .                                                      0.1s
...

Output di atas dipotong karena baris-baris lain tidak berbeda dari build sebelumnya; yang penting adalah durasi 9,6 detik itu kini tidak hilang begitu saja — setiap tarball yang diunduh menetap di cache builder. Beberapa hari kemudian Budi menambahkan fitur lagi yang membutuhkan pustaka uuid, kedua manifest berubah, dan saat inilah cache mount membayar investasinya:

bash
$ docker build -t myapp:1.4 .

Output yang Dihasilkan:

text
[+] Building 2.9s (9/9) FINISHED
 => [internal] load build definition from Dockerfile                    0.1s
 => => transferring dockerfile: 201B                                    0.0s
 => [internal] load metadata for docker.io/library/node:22-alpine       0.8s
 => [1/6] FROM docker.io/library/node:22-alpine                         0.0s
 => => resolve docker.io/library/node:22-alpine                         0.8s
 => CACHED [2/6] WORKDIR /app
 => [3/6] COPY package.json package-lock.json ./                        0.1s
 => [4/6] RUN --mount=type=cache,target=/root/.npm npm ci               1.3s
 => [5/6] COPY . .                                                      0.1s
 => [6/6] EXPOSE 8000                                                   0.0s
 => exporting to image                                                  0.4s
 => => exporting layers                                                 0.2s
 => => writing image sha256:661e7c82cbc669a2c0148d5663850159a532fd3db3ac72efc201095596446872 0.1s
 => => naming to docker.io/library/myapp:1.4                            0.0s

Mari kita telaah perubahan kuncinya:

  1. [4/6] RUN --mount=type=cache,target=/root/.npm npm ci selesai dalam 1,3 detik — layer tetap dieksekusi ulang karena manifest memang berubah, tetapi seluruh tarball disajikan dari cache lokal; yang benar-benar diunduh hanya uuid beserta dependensinya, sisanya waktu dipakai npm untuk verifikasi integritas dan pemasangan.
  2. CACHED versus cache mount — dua mekanisme ini bekerja pada lapisan berbeda: CACHED berarti instruksi tidak dieksekusi sama sekali karena layer identik ditemukan, sedangkan cache mount berarti instruksi tetap dieksekusi tetapi cepat karena datanya tersimpan; keduanya saling melengkapi, bukan saling menggantikan.
  3. Perhatikan bahwa cache tidak menodai hasil build — layer hasil npm ci ditentukan oleh package.json, package-lock.json, dan perilaku npm; keberadaan cache hanya memperpendek jalan menuju hasil yang sama.

Lokasi Cache Setiap Ekosistem #

Kunci pemakaian cache mount adalah mengetahui di mana tiap ekosistem menyimpan hasil unduhannya. Tabel berikut merangkum path yang paling sering dipakai:

EkosistemPath cacheCatatan
npm/root/.npmtarball paket; dipakai npm ci dan npm install
pip/root/.cache/pipwheel dan hasil unduhan; aktif untuk pip install
apt/var/cache/apt dan /var/lib/apt/listsarsip .deb serta indeks paket; keduanya bisa di-mount agar apt-get update murah
Go/go/pkg/mod dan /root/.cache/go-buildcache modul dan cache kompilasi; dua path berbeda, dua mount berbeda

Untuk proyek Go seperti layanan katalog yang kita rakit pada Chapter Multi-Stage Build, polanya memasang dua cache sekaligus dalam satu instruksi:

dockerfile
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -o /out/katalog .

Mari kita telaah pola dua mount di atas:

  1. /go/pkg/mod — cache modul Go; go mod download dan go build mengambil sumber dependensi dari sini, setara dengan /root/.npm milik npm.
  2. /root/.cache/go-build — cache hasil kompilasi; objek dari paket yang tidak berubah tidak dikompilasi ulang, manfaat ekstra yang bahkan tidak dimiliki npm.
  3. Beberapa flag --mount dirangkai dalam satu RUN — setiap flag dipisah garis miring terbalik seperti kelanjutan baris shell; pola yang sama berlaku untuk jenis mount apa pun.

Opsi Lanjutan: id, sharing, readonly #

Flag --mount=type=cache mengenal beberapa opsi tambahan untuk mengatur identitas dan perilaku berbagi cache:

OpsiNilaiPerilaku
idnama cache, misalnya id=npm-cachememberi nama eksplisit; tanpa opsi ini nama diambil dari path target
sharingshared (bawaan)build-build yang berjalan bersamaan boleh memakai cache yang sama
sharinglockedbuild yang memegang cache mengunci eksklusif; build lain menunggu giliran
sharingprivatesetiap build mendapat salinan cache-nya sendiri, terpisah dari yang lain
readonlytanpa nilaicache di-mount hanya-baca; instruksi boleh membaca tetapi tidak boleh memperbarui isi cache

Opsi id berguna saat dua instruksi pada stage berbeda perlu berbagi cache yang sama meskipun path targetnya berbeda, misalnya builder multi-arsitektur yang ingin berbagi cache modul Go lintas stage. Opsi sharing paling terasa di mesin CI yang menjalankan banyak build paralel: locked mencegah dua build saling menimpa isi cache, sementara private menghindari antrean dengan biaya duplikasi. Opsi readonly cocok untuk build yang hanya boleh membaca cache terkelola tanpa berhak mengubahnya.

Satu catatan penutup mekanisme: RUN --mount hanya dikenali oleh BuildKit, yang sejak Docker Engine 23.0 menjadi engine build bawaan — pada Engine modern seperti milik kita (29.9.0), flag ini langsung bekerja tanpa setting tambahan, sebagaimana dibedah pada Chapter Buildx dan BuildKit. Dengan cache mount, urusan kecepatan instalasi dependensi sudah tertangani. Namun --mount menyembunyikan satu kegunaan lain yang jauh lebih kritis: me-mount rahasia — token registry privat sampai kunci SSH — tanpa meninggalkan jejak sedikit pun di dalam image. Itulah materi chapter berikutnya.

Referensi #