Setiap layer yang kamu kirim dalam image Docker produksi adalah permukaan serangan. Base image Debian atau Ubuntu penuh membawa shell, package manager, coreutils, dan puluhan library yang tidak pernah disentuh aplikasimu, dan semuanya menjadi daftar temuan setiap kali vulnerability scanner dijalankan terhadap registry-mu.
Image distroless menghilangkan semua itu, hanya menyisakan runtime bahasa dan shared library yang benar benar dipakai binary-mu. Dipadukan dengan Dockerfile multi-stage, hasilnya adalah image produksi yang lebih kecil, lebih cepat diunduh, dan jauh lebih sedikit temuan pada laporan CVE. Artikel ini membahas cara kerja distroless, cara membangunnya dengan Dockerfile multi-stage, angka ukuran nyata, cara debug container tanpa shell, dan kapan pendekatan ini tidak tepat dipakai.
Apa saja yang benar benar dibuang oleh distroless
Image distroless, yang dirawat oleh proyek container tools milik Google, dibangun di atas satu gagasan: masukkan hanya aplikasimu dan dependensi runtime-nya ke dalam image akhir. Semua hal yang biasanya dibawa distribusi Linux untuk pemakaian interaktif dihilangkan secara sengaja.
- Tidak ada binary shell seperti bash atau sh, sehingga tidak ada titik masuk interaktif bagi penyerang yang berhasil mendapatkan eksekusi kode di dalam container
- Tidak ada package manager seperti apt atau yum, yang menghilangkan seluruh kategori trik supply chain dan eskalasi privilese
- Tidak ada coreutils standar, sehingga perintah umum pasca eksploitasi memang tidak ada di filesystem
- Runtime bahasa beserta shared library dan sertifikat CA-nya tetap disertakan, karena aplikasi memang membutuhkannya untuk berjalan dan melakukan panggilan TLS keluar
Google merilis varian distroless per bahasa: nodejs, python3, java, dan base statis untuk binary Go. Pilih yang cocok dengan runtime-mu daripada mencoba membangun sendiri dari nol di hari pertama.
Membangun dengan Dockerfile multi-stage
Multi-stage build memungkinkan satu Dockerfile memakai beberapa instruksi FROM, satu per stage. Stage pertama mendapat toolchain lengkap: compiler, npm, dependensi development, semua yang dibutuhkan untuk menghasilkan artefak build. Stage terakhir dimulai dari nol dengan base distroless dan hanya menyalin hasil kompilasi, menggunakan instruksi COPY dengan flag from yang menunjuk ke stage sebelumnya lewat nama.
# Stage 1: build the app with full Node.js tooling
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: runtime — no shell, no package manager, no npm
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./package.json
USER nonroot
EXPOSE 3000
CMD ["dist/server.js"]Tidak ada satupun dari stage build yang bocor ke image akhir kecuali kamu menyalinnya secara eksplisit. SDK, berkas sumber TypeScript, dependensi development, dan artefak build sementara tetap tinggal di stage yang dibuang begitu build selesai. Beri nama tiap stage dengan klausa AS agar Dockerfile tetap masuk akal kalau instruksinya diurutkan ulang nanti, dan pertimbangkan flag target saat development lokal supaya kamu bisa membangun dan memeriksa stage build saja tanpa menunggu langkah penyalinan akhir.
Angka ukuran nyata, berdampingan
Angkanya berbeda beda tergantung aplikasi, tapi pola di bawah ini konsisten pada layanan Node.js. Ini adalah perkiraan ukuran image akhir untuk API server kecil yang sama, dibangun dengan tiga cara berbeda.
| Base image | Perkiraan ukuran | Catatan |
|---|---|---|
| node:20 | ~1.1 GB | Debian penuh, termasuk seluruh toolchain build yang tidak pernah dibutuhkan aplikasi yang berjalan |
| node:20-alpine | ~180 MB | Berbasis musl libc, lebih kecil tapi kadang membuat native addon Node yang mengharapkan glibc jadi rusak |
| gcr.io/distroless/nodejs20-debian12 | ~120 MB | Berbasis glibc, tanpa shell atau package manager, paling mendekati jejak runtime sebenarnya |
Penurunan ukuran ini penting untuk waktu unduh di setiap deploy dan setiap start pod autoscale, tapi kemenangan yang lebih besar biasanya ada di laporan vulnerability scan. Semakin sedikit paket berarti semakin sedikit CVE yang bisa ditemukan scanner seperti Trivy atau Grype, karena memang tidak ada apa apa yang terpasang untuk dicocokkan dengan database CVE sejak awal.
Debug container yang tidak punya shell
Pertama kali perintah exec masuk lewat bash gagal dengan error entrypoint tidak ditemukan pada container distroless, rasanya membingungkan. Memang tidak ada bash di image itu, dan tidak akan pernah ada, itulah inti dari distroless. Tetap ada tiga cara praktis untuk melakukan debug.
- Pakai varian bertanda debug dari image distroless yang sama saat development, yang membawa shell busybox dan bisa dijalankan dengan mengganti entrypoint secara eksplisit, lalu kembali ke tanda non debug untuk deploy sesungguhnya
- Pakai perkakas docker debug dari CLI Docker sendiri, yang menyambungkan shell interaktif lengkap dengan utilitas siap pakai seperti vim dan curl ke container yang berjalan atau ke image statis tanpa pernah mengubah image itu sendiri
- Reproduksi kegagalannya secara lokal pada stage build di Dockerfile-mu, yang masih punya shell lengkap, karena kebanyakan bug aplikasi muncul identik baik stage akhirnya punya bash ataupun tidak
# Attach an interactive debug shell to a running distroless container
docker debug my-running-container
# Or inspect the image itself, before running it
docker debug gcr.io/distroless/nodejs20-debian12Perubahan yang dibuat lewat shell debug yang tersambung ke container yang berjalan tidak pernah tersimpan kembali ke image. Kalau kamu memasang tool atau mengedit berkas untuk menguji sebuah teori, catat apa yang kamu pelajari langsung di Dockerfile, karena container akan kembali ke kondisi semula begitu sesi debug berakhir.
Mengapa permukaan CVE benar benar menyusut
Vulnerability scanner bekerja dengan mencocokkan versi paket yang terpasang terhadap database CVE yang dikenal. Base image OS penuh membawa ratusan paket yang terpasang untuk pemakaian umum, sebagian besar tidak pernah dijalankan aplikasimu tapi tetap saja ditandai begitu CVE baru muncul untuk, misalnya, versi lama sebuah text editor yang tidak pernah dibuka siapapun.
- Menghilangkan package manager juga menghilangkan kemampuan penyerang memasang tool baru di tengah serangan, yang secara berarti menaikkan biaya untuk mendapatkan pijakan pasca eksploitasi
- Menghilangkan shell menghilangkan cara paling umum penyerang berpindah dari bug eksekusi kode menjadi akses interaktif
- Jumlah paket yang lebih kecil berarti lebih sedikit paket yang perlu ditambal dan discan ulang setiap kali update base image dirilis, yang mengurangi beban pemeliharaan berkelanjutan sebanyak pengurangan permukaan serangan awal
Kapan distroless bukan pilihan yang tepat
Distroless bukan tanpa biaya. Tim yang baru memakainya kehilangan waktu di minggu pertama karena kebiasaan debug yang familiar seperti masuk lewat bash berhenti berfungsi, dan dependensi runtime apapun yang dipanggil aplikasimu saat berjalan, seperti memanggil binary sistem lewat child process, harus disertakan secara eksplisit karena tidak akan ada secara default. Kalau timmu deploy cepat dan debug lewat log serta health check level aplikasi ketimbang masuk langsung ke dalam container yang berjalan, biaya transisinya kecil. Kalau proses deploy timmu sangat bergantung pada masuk ke container yang berjalan untuk memeriksa isinya, sisihkan waktu untuk menyesuaikan workflow sebelum menerapkannya secara luas.
Urutan rollout yang baik: ubah satu layanan dengan traffic rendah ke Dockerfile multi-stage distroless lebih dulu, pastikan logging dan health check-mu memberi sinyal yang cukup tanpa akses shell, baru kemudian terapkan pola ini ke sisa layanan lainnya.
Distroless dipadukan dengan multi-stage build adalah salah satu perubahan berdampak paling besar yang bisa kamu lakukan pada pipeline deploy berbasis Docker. Penurunan ukurannya memang bagus, tapi keuntungan sesungguhnya adalah image produksi yang hampir tidak menyisakan apapun bagi penyerang untuk dipakai setelah masuk, serta laporan vulnerability scan yang benar benar mencerminkan jejak runtime-mu, bukan seluruh distribusi Linux serba guna yang sejak awal tidak pernah kamu butuhkan.

