Dockerfile Anda Mungkin Terlalu Besar untuk Produksi
Anda baru saja selesai menulis aplikasi. Aplikasi berhasil dikompilasi, berjalan dengan baik di lokal, dan Anda siap untuk mengirimkannya. Anda menulis Dockerfile, membangun image, mendorongnya ke registry, dan men-deploy ke server. Deployment berhasil, tetapi ada yang terasa tidak pas. Image-nya 1,2 GB. Proses penarikan (pull) di server memakan waktu tiga menit. Biaya penyimpanan terus meningkat. Dan di suatu sudut pikiran Anda, Anda bertanya-tanya: apakah saya benar-benar membutuhkan sistem operasi lengkap dengan package manager dan shell hanya untuk menjalankan binary Go saya?
Inilah momen di mana sebagian besar tim menyadari bahwa membangun image dan membangun image yang layak untuk pengiriman (delivery) adalah dua hal yang berbeda. Dockerfile yang Anda tulis untuk pengujian lokal tidak sama dengan Dockerfile yang harus Anda gunakan di produksi. Jika image Anda besar, lambat dibangun, atau penuh dengan alat yang tidak perlu, seluruh pipeline Anda akan menderita. Setiap proses penarikan memakan waktu lebih lama. Setiap pembangunan membuang waktu. Setiap pemindai kerentanan menemukan lebih banyak masalah dari yang seharusnya.
Kabar baiknya adalah memperbaiki hal ini tidak memerlukan penulisan ulang infrastruktur secara total. Anda hanya perlu memahami tiga hal: bagaimana Dockerfile sebenarnya bekerja, bagaimana menjaga image tetap kecil, dan bagaimana membuat build dapat direproduksi (reproducible).
Bagaimana Dockerfile Menjadi Image
Dockerfile adalah file teks berisi instruksi. Setiap instruksi membuat satu lapisan (layer). Saat Anda membangun ulang image, Docker memeriksa lapisan mana yang tidak berubah dan menggunakannya kembali dari cache. Inilah mengapa build kedua lebih cepat dari build pertama: lapisan yang tidak berubah tidak dibangun ulang.
Masalahnya adalah kebanyakan orang menulis Dockerfile seolah-olah mereka menulis skrip pengaturan. Mereka menginstal semuanya, menyalin semuanya, dan baru kemudian menyadari bahwa image akhir berisi compiler, alat debug, dan utilitas sistem yang tidak ada hubungannya dengan menjalankan aplikasi. Setiap alat tambahan dalam image adalah beban ekstra saat penarikan, permukaan tambahan untuk kerentanan, dan kompleksitas tambahan saat Anda mencoba memahami apa yang sebenarnya ada di dalam image.
Perbaikannya bukan tentang menulis lebih sedikit kode. Ini tentang memisahkan lingkungan build dari lingkungan runtime.
Multi-Stage Build: Pola Paling Penting
Multi-stage build memungkinkan Anda mendefinisikan beberapa tahap (stage) dalam satu Dockerfile. Tahap pertama berisi semua yang diperlukan untuk mengompilasi aplikasi: SDK lengkap, compiler, alat build, dan dependensi. Tahap kedua hanya berisi apa yang diperlukan untuk menjalankan artefak yang telah dikompilasi: binary, library runtime, dan tidak ada yang lain.
Berikut adalah contoh untuk aplikasi Go:
Diagram di bawah mengilustrasikan bagaimana dua tahap bekerja bersama:
# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o myapp .
# Stage 2: Run
FROM alpine:3.19
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]
Tahap pertama menggunakan SDK Go lengkap. Tahap kedua menggunakan image Alpine minimal yang hanya berisi kebutuhan dasar. SDK Go, kode sumber, dan cache build tidak pernah masuk ke image akhir. Hasilnya adalah image yang seringkali berukuran 20 hingga 30 megabyte, bukan lebih dari satu gigabyte.
Pola yang sama berlaku untuk bahasa yang dikompilasi lainnya. Untuk bahasa interpretasi seperti Python atau Node.js, Anda dapat menggunakan multi-stage build untuk menginstal dependensi di satu tahap dan hanya menyalin paket yang sudah diinstal ke tahap akhir, meninggalkan cache package manager dan alat build.
Keamanan Dimulai dari Apa yang Anda Tinggalkan
Setiap alat dalam image Anda adalah potensi kerentanan. Shell seperti Bash atau package manager seperti apt mungkin tampak tidak berbahaya, tetapi jika penyerang mendapatkan akses ke kontainer Anda, alat-alat tersebut menjadi senjata. Mereka memungkinkan penyerang untuk menginstal perangkat lunak baru, mengunduh payload, atau berpindah ke sistem lain.
Prinsipnya sederhana: jangan sertakan apa pun yang tidak diperlukan untuk menjalankan aplikasi. Jika Anda perlu melakukan debug pada kontainer yang sedang berjalan sesekali, jangan simpan alat debugging di image produksi. Sebagai gantinya, buat image debugging terpisah atau gunakan kontainer sementara (ephemeral) yang memasang alat yang diperlukan saat runtime.
Untuk image produksi, pertimbangkan untuk menggunakan base image distroless. Image ini hanya berisi runtime dan library yang dibutuhkan aplikasi Anda. Tanpa shell, tanpa package manager, tanpa utilitas. Google menyediakan image distroless untuk beberapa bahasa, termasuk Go, Python, Java, dan Node.js. Image ini kecil, minimal, dan secara signifikan mengurangi permukaan serangan.
Jika distroless terasa terlalu ketat, Alpine adalah alternatif yang masuk akal. Alpine berukuran kecil dan menyertakan shell, tetapi menggunakan musl libc sebagai pengganti glibc, yang dapat menyebabkan masalah kompatibilitas dengan beberapa aplikasi. Uji aplikasi Anda di Alpine sebelum berkomitmen menggunakannya.
Reproduksibilitas Bukanlah Opsional
Image yang tidak dapat dibangun ulang secara identik enam bulan dari sekarang adalah sebuah kewajiban (liability). Penyebab paling umum dari build yang tidak dapat direproduksi adalah penggunaan tag latest untuk base image. latest berubah setiap kali maintainer mendorong versi baru. Build hari ini mungkin menggunakan Go 1.22, tetapi build bulan depan mungkin menggunakan Go 1.23, dan Anda tidak akan tahu sampai sesuatu rusak.
Selalu kunci (pin) base image Anda ke tag versi tertentu, atau lebih baik lagi, ke hash digest. Hash digest adalah checksum kriptografis dari konten image. Hash ini tidak pernah berubah. Jika Anda menggunakan golang:1.22-alpine@sha256:abc123..., Anda dijamin akan mendapatkan base image yang persis sama setiap saat, terlepas dari kapan atau di mana Anda membangun.
FROM golang:1.22-alpine@sha256:abc123def456 AS builder
Ini berlaku untuk setiap image yang Anda tarik, termasuk image perantara yang digunakan dalam multi-stage build. Jika Anda tidak dapat menemukan digest untuk versi tertentu, gunakan tag yang paling spesifik yang tersedia, seperti golang:1.22.0-alpine3.19 daripada golang:1.22-alpine.
Urutan Lapisan Mempengaruhi Kecepatan Build
Docker menyimpan cache lapisan berdasarkan urutan instruksi. Jika Anda mengubah file yang disalin di awal Dockerfile, semua lapisan berikutnya akan menjadi tidak valid dan dibangun ulang. Jika Anda mengubah file yang disalin di akhir, hanya lapisan setelah titik itu yang dibangun ulang.
Aturan praktisnya adalah: letakkan instruksi yang jarang berubah di bagian atas, dan instruksi yang sering berubah di bagian bawah.
- Instal dependensi sistem terlebih dahulu.
- Salin manifes dependensi (seperti
go.moddango.sum) dan jalankan langkah unduhan dependensi. - Salin kode sumber terakhir.
Dengan cara ini, jika Anda hanya mengubah kode sumber, langkah instalasi dependensi akan digunakan kembali dari cache. Build menjadi lebih cepat karena Docker tidak mengunduh ulang paket yang tidak berubah.
Daftar Periksa Praktis untuk Dockerfile Anda
Sebelum Anda mendorong image berikutnya ke produksi, jalankan daftar periksa ini:
- Apakah image akhir hanya berisi apa yang diperlukan untuk menjalankan aplikasi?
- Apakah alat build, SDK, dan kode sumber dikecualikan dari tahap akhir?
- Apakah base image dikunci ke versi atau digest tertentu, bukan
latest? - Apakah base image minimal (distroless atau Alpine) daripada image OS penuh?
- Apakah instruksi yang jarang berubah ditempatkan sebelum instruksi yang sering berubah?
- Apakah tidak ada shell atau package manager di image produksi kecuali benar-benar diperlukan?
Apa yang Paling Penting
Dockerfile bukan sekadar skrip build. Ini adalah kontrak antara proses build Anda dan lingkungan produksi Anda. Dockerfile yang baik menghasilkan image yang kecil, aman, dan dapat direproduksi. Dockerfile yang buruk menghasilkan image yang lambat ditarik, sulit di-debug, dan tidak mungkin dibangun ulang dengan percaya diri.
Lain kali Anda menulis Dockerfile, mulailah dengan image akhir dalam pikiran. Tanyakan pada diri sendiri: apa yang sebenarnya dibutuhkan aplikasi ini untuk berjalan? Kemudian tinggalkan yang lainnya. Pipeline Anda akan lebih cepat, deployment Anda akan lebih mulus, dan tim keamanan Anda akan memiliki satu hal yang tidak perlu dikhawatirkan.