Chapter sebelumnya menutup dengan janji bahwa keluarga --mount punya anggota lain yang jauh lebih sensitif: me-mount rahasia. Kebutuhannya nyata di kantor Budi — timnya berlangganan akun organisasi npm dan mempublikasikan paket internal dengan scope @kantor/* sebagai paket privat, sehingga npm ci hanya berhasil bila membawa token autentikasi. Insting pertama Budi sederhana: lewatkan token sebagai ARG, pakai di RUN, selesai. Sayangnya langkah itu mengukir kredensial secara permanen pada image — dan image adalah benda yang didorong ke registry, diambil siapa saja dengan akses pull, dan dibedah tanpa perlu menjalankannya. Chapter ini membuktikan kebocorannya lewat docker history, lalu memperbaikinya dengan secret mount dan ssh mount.

Token yang Telanjang di docker history #

Coba kita jalankan dulu insting pertama Budi supaya kebocorannya terlihat mata. Dockerfile berikut mengambil token lewat ARG, menuliskannya ke konfigurasi npm, menjalankan npm ci, lalu menghapus konfigurasi itu lagi — Budi merasa sudah bersikap hati-hati karena token “dibersihkan” setelah dipakai:

dockerfile
FROM node:22-alpine
ARG TOKEN_NPM
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm config set //registry.npmjs.org/:_authToken ${TOKEN_NPM} \
    && npm ci \
    && npm config delete //registry.npmjs.org/:_authToken
COPY . .
EXPOSE 8000
CMD ["node", "server.js"]

Build-nya berjalan mulus dan token memang bekerja — paket @kantor/validasi terpasang tanpa galat:

bash
$ cd myapp
$ docker build --build-arg TOKEN_NPM=rahasia123 -t myapp:1.5 .

Output yang Dihasilkan:

text
[+] Building 10.9s (10/10) FINISHED
 => [internal] load build definition from Dockerfile                    0.1s
 => => transferring dockerfile: 264B                                    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 config set //registry.npmjs.org/:_authToken ${TOK     9.5s
 => [5/6] COPY . .                                                      0.1s
 => [6/6] EXPOSE 8000                                                   0.0s
 => exporting to image                                                  0.5s
 => => exporting layers                                                 0.3s
 => => writing image sha256:fd0b7ffdf22aaee6fe5e57a6358268ce665f4514542f45b3f555d8938f15a2aa 0.1s
 => => naming to docker.io/library/myapp:1.5                            0.0s

Baris [4/6] tampak lazim saja — perintahnya bahkan terpotong tepat sebelum nama variabelnya selesai tertulis. Masalahnya baru muncul ketika kita membuka lemari riwayat build yang sudah kita kenal pada Chapter Image Inspect dan History:

bash
$ docker history myapp:1.5

Output yang Dihasilkan:

text
IMAGE          CREATED         CREATED BY                                    SIZE      COMMENT
fd0b7ffdf22a   2 minutes ago   CMD ["node" "server.js"]                      0B
<missing>      2 minutes ago   EXPOSE 8000                                   0B
<missing>      2 minutes ago   COPY . .                                      1.9MB
<missing>      2 minutes ago   RUN |1 TOKEN_NPM=rahasia123 npm config set    146MB
<missing>      2 minutes ago   COPY package.json package-lock.json ./        489kB
...

Mari kita telaah kebocoran pada output di atas:

  1. |1 TOKEN_NPM=rahasia123 — notasi ini adalah cara BuildKit mencatat bahwa instruksi RUN memakai satu ARG beserta nilainya; teks perintahnya sendiri masih memuat ${TOKEN_NPM} yang belum terurai, tetapi prefix |1 menuliskan nilai aslinya dengan terang-terangan.
  2. npm config delete tidak menolong sama sekali — penghapusan konfigurasi hanya memengaruhi isi berkas .npmrc pada layer hasil RUN; nilai ARG sudah terlanjur terpahat di metadata riwayat yang dibawa image ke mana-mana.
  3. ENV sama buruknya — ENV TOKEN_NPM=rahasia123 tercetak utuh sebagai baris riwayat tersendiri, dan nilainya juga tersimpan pada konfigurasi image sehingga tampak di docker inspect; ARG dan ENV sama-sama bukan tempat kredensial.
  4. Siapa pun yang bisa pull bisa membaca — docker history tidak butuh hak akses khusus; cukup memiliki image, dan registry perusahaan menyimpan image itu persis seperti adanya, token termasuk.

Secret Mount: Rahasia yang Tidak Menyentuh Layer #

Solusi BuildKit memakai kerangka yang sama dengan cache mount, tetapi dengan jenis secret. Polanya: Dockerfile hanya menyebut nama secret, nilai dikirim oleh docker build saat dijalankan, dan builder menyajikannya sebagai berkas sementara pada /run/secrets/<nama> — hanya selama instruksi RUN yang memintanya berjalan. Dockerfile myapp yang diperbaiki terlihat seperti ini:

dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=token_npm \
    npm config set //registry.npmjs.org/:_authToken "$(cat /run/secrets/token_npm)" \
    && npm ci \
    && npm config delete //registry.npmjs.org/:_authToken
COPY . .
EXPOSE 8000
CMD ["node", "server.js"]

Perintah npm config set registry tidak diperlukan di sini karena paket @kantor/* tinggal di registry npm resmi — cukup token untuk membuka pintunya. Untuk registry swakelola seperti Verdaccio atau Nexus di kantor lain, tambahkan satu baris npm config set registry dengan alamat registry tersebut sebelum blok di atas; pola mount-nya tidak berubah.

Build-nya sekarang mengambil nilai dari environment variable di mesin Budi:

bash
$ export TOKEN_NPM=rahasia123
$ docker build --secret id=token_npm,env=TOKEN_NPM -t myapp:1.5 .

Mari kita telaah tiga bagian perintah di atas:

  1. --secret hidup di sisi klien — flag ini dibaca docker build di mesin Budi; Dockerfile tetap bersih karena hanya memuat nama secret, bukan nilainya.
  2. id=token_npm — jembatan identitas: nilai yang dikirim flag ini diserahkan hanya kepada instruksi RUN yang meminta --mount=type=secret,id=token_npm dengan nama yang sama.
  3. env=TOKEN_NPM — sumber nilai: environment variable TOKEN_NPM pada mesin tempat build dijalankan; builder membaca isinya, mengirimkannya ke builder sebagai berkas sementara /run/secrets/token_npm, dan berkas itu lenyap begitu RUN selesai.

Bila flag --secret lupa disertakan, build langsung menolak jalan dengan pesan bahwa secret token_npm tidak ditemukan — kegagalan yang justru baik, karena Dockerfile dengan --mount=type=secret tidak akan pernah diam-diam melewati kredensial.

Memverifikasi Kebersihan Image #

Klaim “tidak bocor” jangan diambil begitu saja; kita buktikan dua kali. Pertama, riwayat build pada image baru:

bash
$ docker history myapp:1.5

Output yang Dihasilkan:

text
IMAGE          CREATED         CREATED BY                                    SIZE      COMMENT
c16343b07696   About a minute ago   CMD ["node" "server.js"]                  0B
<missing>      About a minute ago   EXPOSE 8000                                 0B
<missing>      About a minute ago   COPY . .                                    1.9MB
<missing>      About a minute ago   RUN npm config set //registry.npmjs.org/:_  146MB
<missing>      About a minute ago   COPY package.json package-lock.json ./      489kB
...

Baris RUN masih memuat perintah pembacaan berkas rahasia — tetapi yang tercetak hanyalah perintah cat terhadap path yang sudah tidak berisi apa-apa; prefix |1 beserta nilainya lenyap karena tidak ada ARG yang dipakai. Perhatikan pula kolom IMAGE yang kini menunjuk c16343b07696 — image hasil perbaikan memang benda baru yang berbeda ID dari image bocor, sebagaimana kelaziman layer baru yang kita bahas pada Chapter Konsep Image Layer. Verifikasi kedua lebih bruteforce: bongkar seluruh isi image menjadi arsip dan cari token di dalamnya:

bash
$ docker save myapp:1.5 | grep -a -c rahasia123

Output yang Dihasilkan:

text
0

Angka 0 berarti tidak satu baris pun dari seluruh layer dan konfigurasi image memuat string rahasia123 — token sudah benar-benar tidak tinggal di dalam benda yang didorong ke registry. Budi juga membuang image lama yang bocor (docker image rm melepas tag, lalu pembersihan dangling image sebagaimana dibiasakan pada Chapter Melihat Image Lokal), karena image itu masih menyimpan token di riwayatnya selama masih tersimpan.

Sumber Lain: Berkas #

Env var bukan satu-satunya sumber nilai. Flag --secret menerima src= untuk membaca nilai dari sebuah berkas di mesin build:

bash
$ docker build --secret id=token_npm,src=/home/budi/.token-npm -t myapp:1.5 .

Bentuk berkas cocok untuk pipeline CI tempat secret manager menuliskan kredensial ke berkas sementara sebelum memanggil build. Satu aturan penting menyertainya: simpan berkas token itu di luar build context, atau paling tidak pastikan namanya tercantum di .dockerignore — berkas di dalam context yang tidak tersaring akan ikut terkirim ke builder dan berpeluang tersalin oleh COPY . ., kecelakaan yang sudah kita saksikan pada Chapter .dockerignore.

SSH Mount untuk Repo Privat #

Kebutuhan kedua tim Budi tidak berupa token tetapi kunci SSH: pustaka bersama lib disimpan di repo privat GitHub kantor, dan ada layanan yang membangun dirinya dengan meng-clone repo itu saat build. Secret mount tidak cukup di sini karena git lewat SSH tidak membaca berkas token — git berbicara dengan ssh-agent. BuildKit menjawabnya dengan --mount=type=ssh:

dockerfile
FROM node:22-alpine
RUN apk add --no-cache git openssh-client
WORKDIR /app
RUN --mount=type=ssh git clone [email protected]:budi/lib.git lib
COPY . .
EXPOSE 8000
CMD ["node", "server.js"]

Sebelum membangun, kunci privat harus berada di dalam ssh-agent yang hidup, lalu socket agent-nya diteruskan ke builder:

bash
$ ssh-add /home/budi/.ssh/id_ed25519
Identity added: /home/budi/.ssh/id_ed25519 (budi@laptop)
$ docker build --ssh default=$SSH_AUTH_SOCK -t myapp:1.6 .

Output yang Dihasilkan:

text
[+] Building 9.4s (12/12) FINISHED
 => [1/6] FROM docker.io/library/node:22-alpine                         0.0s
 => => resolve docker.io/library/node:22-alpine                         0.7s
 => [2/6] RUN apk add --no-cache git openssh-client                     3.9s
 => [3/6] WORKDIR /app                                                  0.0s
 => [4/6] RUN --mount=type=ssh git clone [email protected]:budi/lib.git    2.0s
 => [5/6] COPY . .                                                      0.1s
 => [6/6] EXPOSE 8000                                                   0.0s
 => exporting to image                                                  0.4s
 => => exporting layers                                                 0.2s
 => => writing image sha256:d50994c7954385f8d38189cb98abb1a6c744acee3111c12c552b4436bac8af1f 0.1s
 => => naming to docker.io/library/myapp:1.6                            0.0s

Mari kita telaah mekanisme di balik dua baris perintah di atas:

  1. --mount=type=ssh — me-forward socket ssh-agent dari mesin build ke dalam instruksi RUN; git di dalam container build memakai agent itu seolah agent berjalan lokal, tanpa satu pun berkas kunci privat yang tersalin.
  2. --ssh default=$SSH_AUTH_SOCK — default adalah nama mount yang dipakai --mount=type=ssh tanpa id=; $SSH_AUTH_SOCK adalah environment variable bawaan ssh-agent yang berisi path socket-nya.
  3. Kunci tidak pernah menyentuh layer — autentikasi terjadi di agent di luar container build; saat RUN selesai, socket dilepas dan layer hanya berisi hasil clone; docker history kembali bersih dari jejak kunci.
  4. git clone --depth 1 — variasi yang layak dipertimbangkan untuk repo besar: hanya revisi terbaru yang diunduh, jadi layer hasil clone jauh lebih ramping; pola ini kita rangkum kembali pada Chapter Mengurangi Ukuran Image.

Secret Mount versus ARG #

Kapan memakai yang mana? Perbandingan berikut merangkum keputusannya:

AspekARG / ENVSecret mount
Sumber nilaiflag --build-arg atau baris Dockerfileenv var atau berkas di mesin build
Jejak di docker historyterlihat — nilai ARG pada prefix `1`, ENV tercetak utuh
Keberadaan setelah buildnilai tinggal di riwayat/konfigurasi imagetidak ada — mount mati saat RUN usai
Kegunaan yang tepatnilai build non-rahasia: nomor versi, flag fiturkredensial: token registry, kunci SSH

Garis pembatasnya sederhana: bila nilai boleh dibaca siapa pun yang membaca Dockerfile, ARG sah dipakai — nomor versi rilis adalah contoh klasiknya yang akan kita pakai lagi pada Chapter Reproducible Build. Bila nilai membuat jantung berdebar bila dilihat orang lain, satu-satunya jawaban adalah secret mount atau ssh mount.

Dengan cache, secret, dan ssh, kita telah membekali builder dengan tiga jenis mount yang menutup hampir semua kebutuhan build sehari-hari. Namun ada satu dimensi yang belum tersentuh dan kerap menyakitkan di lapangan: image yang kita bangun selama ini diam-diam hanya mengenal satu arsitektur. Pelanggan berikutnya myapp ternyata seorang Raspberry Pi — dan di sanalah exec format error menunggu.

Referensi #