Pertanyaan tim audit di akhir chapter sebelumnya terdengar sederhana: kalau laporan:1.2 dibangun lagi besok dari kode yang sama, apakah hasilnya image yang sama? Pertanyaan semacam ini bukan kuriositas — audit keamanan perlu membuktikan bahwa image produksi identik dengan image yang diuji, tim operasional perlu memastikan dua server menjalankan bit yang sama, dan penyelidikan insiden perlu bisa memutar ulang sejarah build dengan hasil yang persis. Sayangnya, jawaban bawaan Docker adalah tidak: dua build dari Dockerfile yang sama, tanpa satu perubahan pun, menghasilkan image dengan ID yang berbeda. Chapter penutup section ini membedah sumber-sumber ketidakpastian itu satu per satu, lalu menguncinya berlapis — pin digest, lock file, SOURCE_DATE_EPOCH, sampai attestasi provenance — sampai dua build yang sama lahir dengan IMAGE ID yang identik.

Dua Build yang Tidak Identik #

Mari kita buktikan dulu klaim pembukanya. Budi membangun myapp apa adanya, membersihkan build cache agar builder benar-benar bekerja dari nol, lalu membangun lagi tanpa menyentuh satu berkas pun:

bash
$ docker build -t myapp:uji .
$ docker builder prune -f

Output yang Dihasilkan:

text
Total reclaimed space: 1.2GB
bash
$ docker build -t myapp:uji2 .
$ docker images myapp

Output yang Dihasilkan:

text
REPOSITORY   TAG    IMAGE ID       CREATED             SIZE
myapp        uji    e18e0476eecd   About a minute ago  278MB
myapp        uji2   9a9f77d65fc2   20 seconds ago      278MB

Mari kita telaah hasil yang memalukan itu:

  1. Dua IMAGE ID berbeda — e18e0476eecd dan 9a9f77d65fc2; ukurannya sama, isinya “seharusnya” sama, tetapi identitasnya berbeda, dan identitas itulah yang diperiksa audit.
  2. Kolom CREATED membocorkan biang keroknya — dua image lahir dengan selisih satu menit; waktu pembuatan tercatat di konfigurasi image, dan waktu yang berbeda otomatis menghasilkan hash konfigurasi yang berbeda, sehingga ID image ikut berubah meski isinya identik.
  3. docker builder prune bukan penyebab, hanya pemperjelas — tanpa pembersihan cache, build kedua akan dilayani penuh dari cache dan menghasilkan ID yang sama; memangkas cache memaksa builder bekerja ulang dan menyingkapkan sifat aslinya.

Empat Sumber Non-Determinisme #

Build yang tidak bisa diulang dengan hasil identik punya beberapa sumber yang saling bertumpuk:

  1. Base image yang bergeser — FROM node:22-alpine menyebut tag, dan tag adalah penunjuk yang bisa dipindahkan penerbitnya kapan pun; dua build pada hari berbeda bisa diam-diam memakai dua isi base yang berbeda — jebakan yang kita buktikan sendiri pada Chapter Image Digest.
  2. Stempel waktu pada layer dan konfigurasi — waktu pembuatan layer hasil RUN serta kolom created pada konfigurasi image mengikuti jam dinding saat build berjalan; inilah penyebab demo di atas.
  3. Perintah yang membaca jam — instruksi seperti RUN date > build.info menuliskan waktu build ke dalam berkas yang menjadi bagian layer; setiap build melahirkan berkas — dan layer — yang berbeda.
  4. Dependensi yang tidak dipin — npm install tanpa lock file, apt-get install tanpa versi, atau pip install dengan rentang versi membiarkan registry menentukan versi mana yang terpasang hari itu.

Solusinya karenanya berlapis: setiap sumber harus dikunci dengan alatnya masing-masing.

Pin Base dengan Digest #

Kunci pertama dan terpenting menyangkut FROM. Satu-satunya cara menjamin base image tidak bergeser adalah menyebut digest-nya secara langsung — materi yang dibuktikan tuntas pada Chapter Image Digest:

dockerfile
FROM node:22-alpine@sha256:876b4dd003fc4641a962d7809db28064d7a14291ff4da8430ef347043f332084

Baris di atas memuat bentuk lengkapnya: nama image, tag sebagai dokumentasi versi, lalu @sha256: diikuti 64 karakter heksadesimal yang mengunci manifest base secara permanen. Tag boleh tetap ditulis untuk keterbacaan — yang menentukan tetap digest, karena bagian setelah @ menimpa penunjuk tag. Nilai digest ini Budi ambil dari docker images --digests setelah menarik base-nya, dan seluruh ekosistem paket lain mengikuti prinsip pin yang sama:

EkosistemKunci pinCatatan
Base imageFROM image@sha256:...digest manifest, bukan tag
Node.jspackage-lock.json + npm cisudah berjalan di myapp sejak Chapter Build Cache
Gogo.sum + go mod downloadchecksum setiap modul diverifikasi
RustCargo.lock + cargo build --lockedbuild menolak versi yang tidak tercatat
Pythonrequirements.txt dengan ==versi eksplisit, bukan rentang >=
Paket sistemapt-get install paket=versiversi per paket ditulis eksplisit

Menandai Versi dengan ARG dan LABEL #

Kunci kedua berlaku untuk versi aplikasi sendiri. Kebutuhannya: image yang dibangun dari commit berbeda harus bisa dibedakan tanpa membongkar isinya, dan angka versi itu harus tercatat di metadata image, bukan di teks Dockerfile yang mudah lupa disunting. Polanya memakai ARG yang justru kita legitimasikan kembali setelah chapter Secret dan SSH Mount mengecapnya bukan tempat rahasia:

dockerfile
ARG VERSI=2.0.0
LABEL version=$VERSI

Nilai bawaan bisa ditimpa saat build — docker build --build-arg VERSI=2.0.1 . — tanpa menyentuh satu berkas pun di repositori. Hasilnya terbaca dari metadata image:

bash
$ docker inspect --format '{{json .Config.Labels}}' myapp:2.0

Output yang Dihasilkan:

text
{"version":"2.0.0"}

Format {{json .Config.Labels}} pada template docker inspect menyajikan seluruh LABEL sebagai satu objek JSON — teknik format yang sudah kita kenal pada Chapter Image Inspect dan History. Dengan LABEL terpasang, dua image yang tampak sama di docker images tetap bisa dibedakan versinya dalam satu perintah.

Menstabilkan Waktu dengan SOURCE_DATE_EPOCH #

Kunci ketiga menyerang sumber non-determinisme yang paling diam-diam: waktu. BuildKit mengenali satu nama ARG istimewa, SOURCE_DATE_EPOCH, yang nilainya dibaca sebagai jumlah detik sejak epoch Unix; bila nilai itu ada, BuildKit memakainya sebagai stempel waktu bagi layer yang dibuat instruksi RUN dan bagi konfigurasi image, menggantikan jam dinding mesin build. myapp versi 2.0.0 dirilis pada 1 Oktober 2026, jadi Budi mengunci epoch tanggal itu — 1790812800 detik — langsung di Dockerfile:

dockerfile
FROM node:22-alpine@sha256:876b4dd003fc4641a962d7809db28064d7a14291ff4da8430ef347043f332084
ARG VERSI=2.0.0
ARG SOURCE_DATE_EPOCH=1790812800
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
LABEL version=$VERSI
EXPOSE 8000
CMD ["node", "server.js"]

Mari kita telaah perubahan yang dibawa tiga baris barunya:

  1. FROM ...@sha256:876b4dd... — base dikunci digest; base yang identik dijamin berlaku untuk kedua build kapan pun dijalankan.
  2. ARG SOURCE_DATE_EPOCH=1790812800 — stempel waktu seluruh layer dan konfigurasi image dipatok ke 1 Oktober 2026 00:00:00 UTC; jam dinding mesin build kehilangan kuasa atas hasil akhir, dan instruksi seperti RUN date pun berhenti melahirkan kejutan karena environment SOURCE_DATE_EPOCH ikut tersedia di dalam RUN.
  3. RUN --mount=type=cache,target=/root/.npm npm ci — cache mount dari Chapter Cache Mount tetap dipertahankan justru karena aman: isinya hidup di luar layer, sehingga cache yang hangat maupun kosong tidak pernah mengubah satu byte pun hasil build.
  4. LABEL version=$VERSI — nomor versi tercatat di metadata; nilai ARG yang berbeda otomatis melahirkan image yang berbeda, dan memang seharusnya begitu.

Bukti: Build Ulang, ID Sama #

Saatnya membayar tagihan klaim chapter ini. Budi mengulang ritual pembuktian yang tadi gagal — build, hapus cache total, build lagi:

bash
$ docker build -t myapp:2.0 .
$ docker builder prune -af
$ docker build -t myapp:2.0-uji .
$ docker images myapp

Output yang Dihasilkan:

text
REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
myapp        2.0       2ee42e4f06c6   12 days ago    278MB
myapp        2.0-uji   2ee42e4f06c6   12 days ago    278MB

Mari kita telaah buktinya:

  1. Dua baris, satu IMAGE ID — 2ee42e4f06c6 tampil pada kedua tag meski build kedua dieksekusi penuh tanpa cache; layer-layer yang dihasilkan identik byte demi byte, sehingga hash — dan identitas — ikut identik.
  2. CREATED 12 days ago pada keduanya — kolom ini kini menunjuk 1 Oktober 2026 (epoch yang dipin), bukan momen build dijalankan pada 13 Oktober; stempel waktu yang tadinya biang perbedaan kini menjadi bagian bukti.
  3. npm ci yang dieksekusi ulang tetap deterministik — package-lock.json mematok versi setiap paket, dan mtime berkas hasil pemasangan dinormalisasi SOURCE_DATE_EPOCH; unduhan ulang tanpa cache mount tidak mengubah hasil.

Attestasi: Provenance dan SBOM #

Kunci terakhir tidak lagi berurusan dengan isi image, melainkan dengan bukti tentang bagaimana image itu dibangun. buildx mampu melampirkan dua attestasi otomatis pada saat push: provenance — catatan terstruktur berisi sumber build, parameter, dan identitas builder — dan SBOM — daftar seluruh paket yang terkandung dalam image. Keduanya menyala lewat dua flag:

bash
$ docker buildx build --provenance=mode=max --sbom=true \
    -t registry.example.com/myapp:2.0 --push .

Output yang Dihasilkan:

text
[+] Building 13.8s (20/20) FINISHED
 => [internal] load build definition from Dockerfile                    0.1s
 => => transferring dockerfile: 389B                                    0.0s
 => CACHED [1/6] FROM docker.io/library/node:22-alpine@sha256:876b4dd003fc4641a962d7809db28064d7a14291ff4da8430ef347043f332084 0.0s
...
 => exporting to image                                                  0.1s
 => => exporting layers                                                 0.0s
 => exporting manifest list sha256:f133b06bcc88d1ddca727304a1ea57f24981da880836821bbfd9d02dbf6b491e 0.4s
 => => naming to registry.example.com/myapp:2.0                        0.0s
 => pushing manifest list                                               1.7s

Hasil push-nya bukan lagi satu image polos melainkan manifest list yang memuat image ditambah attestation-nya — dan docker manifest inspect, yang kita kenal dari Chapter Target Platform, memperlihatkan strukturnya:

bash
$ docker manifest inspect registry.example.com/myapp:2.0

Output yang Dihasilkan:

json
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
  "manifests": [
    {
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "size": 739,
      "digest": "sha256:3c6e69f8ea5cd004b5a5a8843d4a8051c2750f6591727fbbc3625d4df22cef9b",
      "platform": {
        "architecture": "amd64",
        "os": "linux"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "size": 1254,
      "digest": "sha256:3c348abdad844c6265fa057090eaf3e8d4c817d72170dcbeeb2a3aa511178363",
      "platform": {
        "architecture": "unknown",
        "os": "unknown"
      },
      "annotations": {
        "vnd.docker.reference.digest": "sha256:3c6e69f8ea5cd004b5a5a8843d4a8051c2750f6591727fbbc3625d4df22cef9b",
        "vnd.docker.reference.type": "attestation-manifest"
      }
    }
  ]
}

Mari kita telaah dua entri di dalamnya:

  1. Entri pertama: image myapp untuk linux/amd64 — manifest image biasa, persis entri-entri manifest list katalog pada Chapter Target Platform; inilah yang dijalankan docker run.
  2. Entri kedua: platform unknown/unknown — bukan image yang bisa dijalankan, melainkan attestation manifest; anotasi vnd.docker.reference.type: attestation-manifest menyatakan jenisnya, dan vnd.docker.reference.digest menunjuk image yang dideskripsikannya.
  3. Provenance dan SBOM hidup di dalam entri kedua — metadata build dan daftar paket tersimpan sebagai objek terpisah yang menempel pada image di registry; pemindai kerentanan dan tool audit membacanya tanpa harus menjalankan image.

Satu catatan penutup untuk bahasa kompilasi: binary punya non-determinisme tambahan berupa path absolut direktori build yang ditanam compiler ke dalam berkas hasil. Go menyuapi obatnya lewat flag -trimpath — go build -trimpath mengganti path mesin build dengan path modul, sehingga binary yang dikompilasi di folder mana pun identik; katalog sudah memakai flag ini sejak Dockerfile-nya dirakit, dan kini kita tahu alasan di baliknya.

Section Image Build dan Optimisasi sampai di sini tuntas perjalanannya: build yang cepat lewat urutan instruksi dan cache mount, rahasia yang tidak membekas, image yang menyeberangi arsitektur, ukuran yang ramping, dan kini identitas yang bisa diverifikasi ulang. Produk section ini — image yang baik — hanyalah separuh kebenaran Docker, karena image mendeskripsikan apa yang dijalankan, bukan bagaimana container menjalankannya. Environment variable, perintah start, user, permission, dan seluruh knob runtime itulah wilayah Section Konfigurasi Container, yang dibuka oleh Chapter Command Override.

Referensi #