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:
- 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. - Checksum dihitung untuk setiap berkas. Nilai hash ini menjadi bahan cache key instruksi
COPYdanADD; tanpa checksum, BuildKit tidak bisa mengetahui apakah isi context berubah sejak build terakhir. - 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:
$ tree -a -L 1 myappOutput yang Dihasilkan:
myapp
├── .env
├── .env.example
├── .git
├── Dockerfile
├── README.md
├── docs
├── node_modules
├── package-lock.json
├── package.json
└── server.js
3 directories, 7 files$ du -sh myapp/node_modulesOutput yang Dihasilkan:
146M myapp/node_modulesFolder 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:
$ docker build --progress=plain -t myapp:1.0 .Output yang Dihasilkan:
#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 doneMari kita telaah bukti yang tersimpan di output di atas:
- Langkah
#4 [internal] load build contextberdurasi penuh —transferring context: 148.21MB 11.9s doneterjadi tanpa peduli bahwa seluruh instruksi berikutnya akan dilayani dari cache. - Lima langkah Dockerfile semuanya
CACHED—FROMhinggaEXPOSEtidak dieksekusi ulang; cache bekerja sempurna pada level instruksi. - 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.
- 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
.dockerignoreyang 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:
- Pengarsipan dan transfer. Seluruh isi folder harus ditarsi dan dikirim setiap build — 146MB
node_modulesmenempuh 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. - Checksum seluruh berkas. Ribuan berkas kecil di dalam
node_modulesdi-hash satu per satu untuk keperluan cache key; proses ini sendiri memakan waktu CPU dan I/O sebelum transfer dimulai. - Invalidasi cache
COPY . .. Mekanisme cache yang kita bedah tuntas pada Chapter Build Cache menghitung checksum seluruh isi context untuk instruksiCOPY . .— satu berkas apa pun berubah di folder, sekecil log npm atau catatan diREADME.md, seluruh langkah itu gugur bersama seluruh keturunannya.node_modulesyang 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:
$ docker build -t myapp:login [email protected]:budi/myapp.git#fitur-loginOutput yang Dihasilkan:
[+] 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.0sMari kita telaah perintah di atas:
[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.#fitur-login— fragmen di akhir URL adalah ref: nama cabang, tag, atau bahkan commit. Budi membangun cabangfitur-logintanpa perlu menariknya ke disk lebih dulu — berguna untuk membangun persis kode yang ada di server uji.- Folder
.gitikut menjadi context — sesuatu yang jarang diinginkan, jadi berkas.dockerignoretetap diperlukan pada jalur ini juga.
Dua varian stdin melengkapi daftar sumber. Tarball yang diarahkan ke - dikirim apa adanya sebagai context:
$ docker build -t katalog:1.0 - < katalog.tarSedangkan teks yang diarahkan ke - dibaca sebagai Dockerfile dari stdin dengan context kosong:
$ printf 'FROM alpine:3.20\nCMD ["echo", "halo dari stdin"]\n' | docker build -t halo:stdin -Output yang Dihasilkan:
[+] 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.0sPerhatikan 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 context | Bentuk perintah | Kapan dipakai |
|---|---|---|
| Folder lokal (PATH) | docker build . | Pengembangan harian; context = isi folder |
| URL Git | docker build [email protected]:budi/myapp.git#fitur-login | Membangun kode dari cabang/tag/commit tanpa kloning manual |
| Tarball via stdin | docker build - < katalog.tar | Context yang sudah dikemas; pipeline yang mengirim arsip |
| Dockerfile via stdin | printf ... | 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:
toko/
├── services/
│ ├── api/
│ │ ├── Dockerfile
│ │ └── main.go
│ └── web/
│ ├── Dockerfile
│ └── app.js
└── shared/
└── konfigurasi.jsLayanan 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:
$ 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:
- Pola pertama: context di akar monorepo. Titik menunjuk folder
toko, flag-fmengarahkan definisi keservices/api/Dockerfile, dan instruksiCOPY shared/konfigurasi.js /app/di dalamnya sah karenashared/berada di dalam context. Seluruh monorepo (termasukservices/webyang tak dibutuhkan) ikut ditransfer. - Pola kedua: context di subfolder. Context dipersempit ke
services/apisehingga transfer jauh lebih kecil, tetapi konsekuensinya keras: builder hanya melihat isiservices/api, sehinggaCOPY ../../shared/konfigurasi.jspasti gagal. - Aturannya satu dan tak bisa ditawar:
COPYhanya 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:
$ cd services/api && docker build -t toko-api:1.0 .Output yang Dihasilkan:
[+] 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 #
- Build Contexts — jenis-jenis context (PATH, Git, stdin) beserta perilaku pengirimannya.
- docker buildx build — CLI Reference — flag konteks dan cara BuildKit membaca argumen PATH terakhir.