Angka 148.21MB pada dua build terakhir Budi menyimpan teka-teki yang belum terjawab: build kedua menampilkan CACHED di seluruh langkah Dockerfile, tidak satu instruksi pun dieksekusi ulang, tetapi transfer context tetap menghabiskan 148.21MB dan 11.9 detik — sama persis seperti build pertama. Build cache ternyata sama sekali tidak menyentuh urusan ini. Pertanyaannya kembali ke argumen paling sederhana dari perintah build kita: satu titik di akhir docker build -t myapp:1.0 .. Chapter ini membedah makna titik tersebut — build context: apa yang dikirim, kenapa seluruh isinya dikirim, dari mana saja builder menerima isinya, dan bagaimana proyek besar seperti monorepo mengatur pilihan context-nya.

Konteks Adalah Argumen PATH Terakhir #

Build context adalah folder (atau sumber lain) yang kita tunjuk sebagai argumen terakhir docker build. Builder bekerja terisolasi dari filesystem lokal kita — ia tidak bisa mengintip ke folder mana pun di luar yang kita berikan; satu-satunya jendela pandangnya adalah isi context yang dikirimkan. Chapter COPY dan ADD sudah membuktikan konsekuensi pertamanya: path sumber COPY selalu relatif terhadap context, bukan lokasi Dockerfile. Konsekuensi keduanya yang kini menjadi fokus kita:

  1. Seluruh isi context diarsipkan menjadi satu tarball. Sebelum satu instruksi Dockerfile dievaluasi, BuildKit menarik seluruh isi folder context — termasuk berkas yang tidak pernah dirujuk COPY — ke dalam arsip tunggal.
  2. Checksum dihitung untuk setiap berkas. Nilai hash ini menjadi bahan cache key instruksi COPY dan ADD; tanpa checksum, BuildKit tidak bisa mengetahui apakah isi context berubah sejak build terakhir.
  3. Tarball dikirim ke builder. Pada instalasi Linux dengan daemon lokal, pengiriman menempuh socket daemon; pada Docker Desktop, ia menyeberangi batas VM ke engine di dalamnya — ukuran context menjadi tagihan transfer sungguhan, bukan sekadar formalitas.

Builder memang tidak tahu di muka berkas mana yang akan dibutuhkan COPY hingga frontend selesai mengevaluasi Dockerfile, dan checksum tetap harus dihitung dari sumber aslinya — dua alasan yang membuat desain “kirim semuanya lebih dulu” ini masuk akal meski terasa boros. Mari kita lihat betapa borosnya untuk myapp versi baru:

bash
$ tree -a -L 1 myapp

Output yang Dihasilkan:

text
myapp
├── .env
├── .env.example
├── .git
├── Dockerfile
├── README.md
├── docs
├── node_modules
├── package-lock.json
├── package.json
└── server.js

3 directories, 7 files
bash
$ du -sh myapp/node_modules

Output yang Dihasilkan:

text
146M    myapp/node_modules

Folder node_modules sendirian menyumbang 146MB — hampir seluruh dari 148.21MB context; sisanya sekitar 2MB berasal dari riwayat .git, lock file yang membesar, dan dokumentasi. Padahal Dockerfile myapp hanya benar-benar membutuhkan package.json, package-lock.json, dan server.js sebagai masukan COPY . . — sisanya penumpang gelap yang ikut membayar tiket transfer penuh.

Membuktikannya dengan –progress=plain #

Klaim “seluruh context tetap terkirim meski semuanya CACHED” bukan tebakan — kita bisa membuktikannya langsung dengan mode output plain yang kita kenal di akhir chapter sebelumnya. Budi menjalankan build yang sama untuk ketiga kalinya tanpa mengubah satu byte pun:

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 [1/5] FROM docker.io/library/node:22-alpine
#5 resolve docker.io/library/node:22-alpine 1.1s done
#5 CACHED
#6 [2/5] WORKDIR /app
#6 CACHED
#7 [3/5] COPY . .
#7 CACHED
#8 [4/5] RUN npm ci
#8 CACHED
#9 [5/5] EXPOSE 8000
#9 CACHED
#10 exporting to image
#10 exporting layers done
#10 writing image sha256:3e8c1a5f9b42d687f0a19c3b5e72d84f6b91c0e4a2d7f38b5c6019e4d2f7a83b done
#10 naming to docker.io/library/myapp:1.0 done

Mari kita telaah bukti yang tersimpan di output di atas:

  1. Langkah #4 [internal] load build context berdurasi penuh — transferring context: 148.21MB 11.9s done terjadi tanpa peduli bahwa seluruh instruksi berikutnya akan dilayani dari cache.
  2. Lima langkah Dockerfile semuanya CACHED — FROM hingga EXPOSE tidak dieksekusi ulang; cache bekerja sempurna pada level instruksi.
  3. Checksum tetap dihitung dari context yang dikirim — itulah satu-satunya cara BuildKit memastikan isi context belum berubah sejak build terakhir yang mengisi cache. Transfer penuh adalah harga verifikasi tersebut.
  4. Kesimpulan praktisnya tegas — menghemat waktu transfer hanya bisa dilakukan dengan mengecilkan context itu sendiri, bukan dengan mengandalkan cache. Dua alatnya: memilih folder context yang tepat, dan menyaring isinya dengan berkas .dockerignore yang kita bahas pada chapter berikutnya.

Tiga Alasan Konteks Besar Membuat Build Lambat #

Ukuran context bukan cuma soal satu transfer yang lama. Tiga tagihan menumpuk sekaligus pada folder yang membengkak seperti myapp:

  1. Pengarsipan dan transfer. Seluruh isi folder harus ditarsi dan dikirim setiap build — 146MB node_modules menempuh perjalanan yang sama berulang-ulang, baik ke daemon lokal maupun ke VM Docker Desktop, dan pada builder jarak jauh tagihannya berlipat karena menempuh jaringan sungguhan.
  2. Checksum seluruh berkas. Ribuan berkas kecil di dalam node_modules di-hash satu per satu untuk keperluan cache key; proses ini sendiri memakan waktu CPU dan I/O sebelum transfer dimulai.
  3. Invalidasi cache COPY . .. Mekanisme cache yang kita bedah tuntas pada Chapter Build Cache menghitung checksum seluruh isi context untuk instruksi COPY . . — satu berkas apa pun berubah di folder, sekecil log npm atau catatan di README.md, seluruh langkah itu gugur bersama seluruh keturunannya. node_modules yang berubah-ubah antar percobaan di laptop adalah mesin invalidasi yang paling rajin.

Sumber Konteks Selain Folder Lokal #

Argumen terakhir docker build tidak harus folder di disk. Builder menerima empat sumber context, dan tiga di antaranya hidup di luar mesin kita:

bash
$ docker build -t myapp:login [email protected]:budi/myapp.git#fitur-login

Output yang Dihasilkan:

text
[+] Building 25.7s (12/12) FINISHED
 => [internal] load git source [email protected]:budi/myapp.git
 => => # git clone in progress
 => => # git checkout fitur-login
 => [internal] load build definition from Dockerfile                    0.1s
 => CACHED [1/5] FROM docker.io/library/node:22-alpine                  0.0s
...
 => => naming to docker.io/library/myapp:login                         0.0s

Mari kita telaah perintah di atas:

  1. [email protected]:budi/myapp.git — context berupa repositori Git; BuildKit mengkloning repositori tersebut sendiri, lalu hasil kloningnya menjadi context. Bentuk dengan skema https di depan alamat repositori juga didukung untuk repositori publik; kita menampilkan bentuk SSH di sini karena kunci privat Budi sudah terpasang.
  2. #fitur-login — fragmen di akhir URL adalah ref: nama cabang, tag, atau bahkan commit. Budi membangun cabang fitur-login tanpa perlu menariknya ke disk lebih dulu — berguna untuk membangun persis kode yang ada di server uji.
  3. Folder .git ikut menjadi context — sesuatu yang jarang diinginkan, jadi berkas .dockerignore tetap diperlukan pada jalur ini juga.

Dua varian stdin melengkapi daftar sumber. Tarball yang diarahkan ke - dikirim apa adanya sebagai context:

bash
$ docker build -t katalog:1.0 - < katalog.tar

Sedangkan teks yang diarahkan ke - dibaca sebagai Dockerfile dari stdin dengan context kosong:

bash
$ printf 'FROM alpine:3.20\nCMD ["echo", "halo dari stdin"]\n' | docker build -t halo:stdin -

Output yang Dihasilkan:

text
[+] Building 1.9s (5/5) FINISHED
 => [internal] load build definition from Dockerfile                    0.0s
 => => transferring dockerfile: 57B                                     0.0s
 => [internal] load build context                                       0.0s
 => => transferring context: 2B                                         0.0s
 => [1/1] FROM docker.io/library/alpine:3.20                            0.9s
 => exporting to image                                                  0.1s
 => => writing image sha256:8a1f4c2e9b7d305862f1c4a7e2b5d8c3f6a90124e5b8a7c6d5e4f3a2b1c0d9e8f 0.0s
 => => naming to docker.io/library/halo:stdin                           0.0s

Perhatikan transferring context: 2B pada output di atas: Dockerfile yang datang dari stdin tidak membawa context apa pun, sehingga image sekecil ini hanya bisa dibangun dari instruksi yang tidak memerlukan berkas — pola yang pas untuk image utilitas sekali pakai. Empat sumber context tersebut dirangkum dalam tabel berikut:

Sumber contextBentuk perintahKapan dipakai
Folder lokal (PATH)docker build .Pengembangan harian; context = isi folder
URL Gitdocker build [email protected]:budi/myapp.git#fitur-loginMembangun kode dari cabang/tag/commit tanpa kloning manual
Tarball via stdindocker build - < katalog.tarContext yang sudah dikemas; pipeline yang mengirim arsip
Dockerfile via stdinprintf ... | docker build -Image kecil tanpa berkas; eksperimen cepat

Konteks pada Monorepo #

Tim Budi juga menggarap proyek toko berbentuk monorepo — dua layanan dan satu folder berbagi pakai dalam satu repositori:

text
toko/
├── services/
│   ├── api/
│   │   ├── Dockerfile
│   │   └── main.go
│   └── web/
│       ├── Dockerfile
│       └── app.js
└── shared/
    └── konfigurasi.js

Layanan api membutuhkan shared/konfigurasi.js, dan di sinilah pilihan context menentukan bisa tidaknya build. Ada dua pola yang bisa ditempuh, keduanya dijalankan dari akar toko:

bash
$ docker build -f services/api/Dockerfile -t toko-api:1.0 .
$ cd services/api && docker build -t toko-api:1.0 .

Mari kita telaah kedua pola di atas:

  1. Pola pertama: context di akar monorepo. Titik menunjuk folder toko, flag -f mengarahkan definisi ke services/api/Dockerfile, dan instruksi COPY shared/konfigurasi.js /app/ di dalamnya sah karena shared/ berada di dalam context. Seluruh monorepo (termasuk services/web yang tak dibutuhkan) ikut ditransfer.
  2. Pola kedua: context di subfolder. Context dipersempit ke services/api sehingga transfer jauh lebih kecil, tetapi konsekuensinya keras: builder hanya melihat isi services/api, sehingga COPY ../../shared/konfigurasi.js pasti gagal.
  3. Aturannya satu dan tak bisa ditawar: COPY hanya boleh mengambil dari dalam context. Builder memang tidak punya akses ke apa pun di luar tarball yang dikirimkan kepadanya.

Percobaan menyalin keluar context berakhir pada galat yang jujur:

bash
$ cd services/api && docker build -t toko-api:1.0 .

Output yang Dihasilkan:

text
[+] Building 1.2s (9/9) ERROR
 => [internal] load build definition from Dockerfile                    0.1s
 => ERROR [3/5] COPY ../../shared/konfigurasi.js /app/                  0.0s
------
 > [3/5] COPY ../../shared/konfigurasi.js /app/:
------
failed to compute cache key: "/shared/konfigurasi.js" not found: forbidden path outside the build context: ..()

Pesan forbidden path outside the build context menegaskan batas tersebut. Pilihan antara dua pola adalah tarik-ulur antara keluwesan berbagi berkas (context besar) dan kecepatan transfer (context kecil) — monorepo rapi biasanya memilih pola pertama sambil menyaring sisanya dengan .dockerignore yang menutup services/web saat membangun api, dan sebaliknya.

Context kini bukan lagi sekadar titik di akhir perintah: ia adalah kiriman lengkap yang ditarsi, dihitung checksum-nya, dan dikirim utuh setiap build — 148.21MB untuk myapp yang sebenarnya hanya butuh kurang dari dua megabyte berkasnya. Pertanyaan berikutnya sudah tersedia di ujung jari: bagaimana caranya menyaring isi kiriman itu tanpa memindahkan satu berkas pun keluar dari folder proyek? Jawabannya adalah satu berkas kecil bernama .dockerignore di akar context — materi chapter berikutnya.

Referensi #