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:
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:
$ cd myapp
$ docker build --build-arg TOKEN_NPM=rahasia123 -t myapp:1.5 .Output yang Dihasilkan:
[+] 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.0sBaris [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:
$ docker history myapp:1.5Output yang Dihasilkan:
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 TOKEN_NPM=rahasia123— notasi ini adalah cara BuildKit mencatat bahwa instruksiRUNmemakai satuARGbeserta nilainya; teks perintahnya sendiri masih memuat${TOKEN_NPM}yang belum terurai, tetapi prefix|1menuliskan nilai aslinya dengan terang-terangan.npm config deletetidak menolong sama sekali — penghapusan konfigurasi hanya memengaruhi isi berkas.npmrcpada layer hasilRUN; nilai ARG sudah terlanjur terpahat di metadata riwayat yang dibawa image ke mana-mana.- ENV sama buruknya —
ENV TOKEN_NPM=rahasia123tercetak utuh sebagai baris riwayat tersendiri, dan nilainya juga tersimpan pada konfigurasi image sehingga tampak didocker inspect; ARG dan ENV sama-sama bukan tempat kredensial. - Siapa pun yang bisa pull bisa membaca —
docker historytidak 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:
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:
$ 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:
--secrethidup di sisi klien — flag ini dibacadocker builddi mesin Budi; Dockerfile tetap bersih karena hanya memuat nama secret, bukan nilainya.id=token_npm— jembatan identitas: nilai yang dikirim flag ini diserahkan hanya kepada instruksiRUNyang meminta--mount=type=secret,id=token_npmdengan nama yang sama.env=TOKEN_NPM— sumber nilai: environment variableTOKEN_NPMpada mesin tempat build dijalankan; builder membaca isinya, mengirimkannya ke builder sebagai berkas sementara/run/secrets/token_npm, dan berkas itu lenyap begituRUNselesai.
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:
$ docker history myapp:1.5Output yang Dihasilkan:
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:
$ docker save myapp:1.5 | grep -a -c rahasia123Output yang Dihasilkan:
0Angka 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:
$ 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:
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:
$ 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:
[+] 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.0sMari kita telaah mekanisme di balik dua baris perintah di atas:
--mount=type=ssh— me-forward socket ssh-agent dari mesin build ke dalam instruksiRUN; git di dalam container build memakai agent itu seolah agent berjalan lokal, tanpa satu pun berkas kunci privat yang tersalin.--ssh default=$SSH_AUTH_SOCK—defaultadalah nama mount yang dipakai--mount=type=sshtanpaid=;$SSH_AUTH_SOCKadalah environment variable bawaan ssh-agent yang berisi path socket-nya.- Kunci tidak pernah menyentuh layer — autentikasi terjadi di agent di luar container build; saat
RUNselesai, socket dilepas dan layer hanya berisi hasil clone;docker historykembali bersih dari jejak kunci. 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:
| Aspek | ARG / ENV | Secret mount |
|---|---|---|
| Sumber nilai | flag --build-arg atau baris Dockerfile | env var atau berkas di mesin build |
Jejak di docker history | terlihat — nilai ARG pada prefix ` | 1`, ENV tercetak utuh |
| Keberadaan setelah build | nilai tinggal di riwayat/konfigurasi image | tidak ada — mount mati saat RUN usai |
| Kegunaan yang tepat | nilai build non-rahasia: nomor versi, flag fitur | kredensial: 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 #
- Build Secrets — panduan resmi secret mount beserta sumber env dan berkas.
- SSH Mount
— pola penerusan ssh-agent ke dalam
RUN. - Dockerfile Reference
— sintaks
RUN --mountuntuk jenissecretdanssh.