Biner yang sudah dioptimasi dan di-strip tetap membutuhkan lingkungan yang benar saat dijalankan: versi glibc yang cocok, sertifikat CA yang tersedia, dan sistem operasi yang sama dengan mesin build. Docker menyembuhkan penyakit klasik “berjalan di mesin saya” dengan membungkus aplikasi beserta kebutuhannya menjadi satu image yang identik di laptop pengembang, server CI, dan cluster produksi. Karakter Rust yang menghasilkan biner mandiri membuat image Docker-nya bisa dibuat sangat kecil melalui multi-stage build. Satu pengakuan jujur sebelum mulai: lingkungan penulisan chapter ini tidak memasang Docker, sehingga keluaran docker build yang sesungguhnya tidak bisa kami tampilkan — semua Dockerfile di bawah tetap kami susun dan kami bedah lengkap, sementara angka ukuran image kami ambil langsung dari API registri Docker Hub dan gcr.io.

Kenapa Biner Rust Cocok untuk Container Kecil #

Image Docker disusun dari layer: image dasar ditumpuki layer tambahan hasil setiap instruksi Dockerfile, dan semakin ringan isinya semakin cepat image ditarik ke server. Aplikasi Rust beruntung karena hasil akhirnya satu file biner beberapa ratus kilobyte — tidak seperti aplikasi berinterpreter atau JVM yang membawa runtime ikut menumpang. Tantangannya satu: proses kompilasi justru membutuhkan toolchain raksasa ratusan megabyte. Multi-stage build hadir untuk memisahkan dua kebutuhan yang bertolak belakang ini.

Multi-Stage Build: Tahap Builder dan Runtime #

Ide multi-stage sederhana: Dockerfile menyimpan dua tahap, dan hanya tahap terakhir yang menjadi image jadi. Tahap pertama (builder) memakai image besar berisi Rust toolchain untuk mengompilasi; tahap kedua (runtime) memakai image kecil dan hanya menyalin biner jadi, sehingga ratusan megabyte compiler tidak pernah ikut ke image akhir.

Kita pakai kembali proyek penghitung-prima beserta pengaturan strip = "symbols" dari chapter Strip. Dockerfile berikut diletakkan di akar proyek; baris kosong di tengah memisahkan tahap builder (lima baris pertama) dari tahap runtime:

dockerfile
FROM rust:1.99-slim AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release

FROM debian:bookworm-slim
RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates \
    && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/penghitung-prima /usr/local/bin/penghitung-prima
USER nobody
ENTRYPOINT ["penghitung-prima"]

Mari kita bedah tugas setiap barisnya:

  1. Baris 1 (FROM rust:1.99-slim AS builder): mengambil image Rust resmi varian slim versi 1.99 lalu menamainya builder; varian slim memuat gcc untuk penautan tanpa perkakas build tambahan.
  2. Baris 2 (WORKDIR /app): menetapkan direktori kerja tahap builder; semua COPY dan RUN berikutnya relatif terhadap /app.
  3. Baris 3 (COPY Cargo.toml Cargo.lock ./): menyalin manifes dependensi sebelum source code — urutan ini untuk trik caching di bagian berikutnya.
  4. Baris 4 (COPY src ./src): menyalin isi folder src ke dalam image.
  5. Baris 5 (RUN cargo build --release): mengompilasi program dalam mode release; biner jadi berada di /app/target/release/penghitung-prima.
  6. Baris 7 (FROM debian:bookworm-slim): memulai tahap kedua dari nol; isi tahap builder tidak dibawa serta.
  7. Baris 8-10 (RUN apt-get update ... rm -rf /var/lib/apt/lists/*): memasang ca-certificates untuk aplikasi yang memanggil HTTPS, lalu membersihkan cache apt.
  8. Baris 11 (COPY --from=builder /app/target/release/...): menyalin satu file biner ke tahap runtime — jembatan satu-satunya antar tahap.
  9. Baris 12 (USER nobody): menjalankan container tanpa hak istimewa, sehingga proses yang dibajak tidak punya akses root.
  10. Baris 13 (ENTRYPOINT ["penghitung-prima"]): menetapkan biner kita sebagai proses utama setiap docker run.

Ukuran Image: Dari Raksasa ke Ringan #

Pilihan image pada kedua tahap menentukan ukuran akhir. Tabel berikut memuat ukuran terkompresi amd64 yang kami periksa langsung dari API registri saat chapter ini ditulis; angka Anda akan sedikit berbeda:

Image dasarUkuran terkompresi amd64Peran
rust:1.99±602 MBtoolchain lengkap, terlalu besar untuk runtime
rust:1.99-slim±327 MBbuilder pada tahap satu
debian:bookworm-slim±28 MBruntime glibc untuk biner gnu
gcr.io/distroless/cc-debian12±9 MBruntime glibc tanpa shell dan package manager
alpine:3.20±4 MBruntime musl untuk biner musl
scratch0 MBimage kosong mutlak

Image jadi kita kira-kira setara debian:bookworm-slim ditambah biner 337 KB — puluhan megabyte, jauh di bawah image aplikasi Java atau Python yang lazim ratusan megabyte. Dua baris teratas tabel juga memperlihatkan pemborosan yang kita hindari: tanpa pemisahan tahap, toolchain 327 MB ikut terkirim ke setiap server yang menarik image.

Trik Layer Caching untuk Dependensi #

Docker menyimpan setiap instruksi Dockerfile sebagai layer yang di-cache; cache hanya batal bila berkas atau instruksi yang mendahuluinya berubah. Dockerfile kita menyalin Cargo.toml sebelum src, tetapi RUN cargo build berjalan setelah COPY src — artinya setiap penyuntingan kode membuat layer build hangus dan seluruh dependensi dikompilasi ulang. Trik standarnya: bangun dependensi dulu dengan source palsu:

dockerfile
FROM rust:1.99-slim AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir -p src && echo 'fn main() { println!("stub"); }' > src/main.rs
RUN cargo build --release
COPY src ./src
RUN touch src/main.rs && cargo build --release
  1. Baris 4 (RUN mkdir -p src && echo ... > src/main.rs): membuat source sementara berisi fungsi main stub agar cargo build berjalan dan mengompilasi semua dependensi proyek.
  2. Baris 5 (RUN cargo build --release): layer dependensi terkompilasi; hanya dibangun ulang bila Cargo.toml atau Cargo.lock berubah, bukan saat kita menyunting kode.
  3. Baris 6 (COPY src ./src): source asli menimpa stub; layer mulai titik ini berubah.
  4. Baris 7 (RUN touch src/main.rs && cargo build --release): touch memaksa Cargo membangun ulang hanya crate utama; dependensi di target/ dipakai ulang dari layer sebelumnya.

Dengan trik ini, iterasi harian cukup menanggung kompilasi ulang kode sendiri. Untuk workspace multi-crate, teknik yang sama dipakai: salin semua Cargo.toml crate dan buat stub per crate sebelum menyalin source yang sesungguhnya.

Varian Terkecil: distroless dan scratch #

Dua pilihan bisa memangkas image lebih jauh. Pertama, gcr.io/distroless/cc-debian12 — keluarga image Google yang hanya berisi glibc, libgcc, dan berkas sistem minimum, tanpa shell maupun package manager; serangan yang mencoba membuka shell di dalam container tidak menemukan shell, dan biner target gnu yang menaut libgcc_s.so.1 berjalan mulus di varian cc. Kedua, scratch — dasar kosong mutlak yang hanya berisi apa yang kita salin, cocok dipadukan dengan biner musl static linking dari chapter Cross Compilation:

dockerfile
FROM rust:1.99-slim AS builder
RUN rustup target add x86_64-unknown-linux-musl
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release --target x86_64-unknown-linux-musl

FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/penghitung-prima /penghitung-prima
ENTRYPOINT ["/penghitung-prima"]
  1. Baris 2 (RUN rustup target add ...): memasang pustaka standar untuk target musl di dalam image builder, langkah yang kita kenal dari chapter Cross Compilation.
  2. Baris 6 (RUN cargo build --release --target ...): mengompilasi biner static linking di dalam container.
  3. Baris 8 (FROM scratch): tahap runtime dimulai dari image kosong — tidak ada glibc, tidak ada shell, tidak ada apa pun.
  4. Baris 9 (COPY --from=builder ...): satu-satunya isi image adalah biner kita; dengan pengaturan strip = "symbols" dari chapter Strip, total image hanya beberapa ratus kilobyte.

Perlu dicatat dua batasan scratch: tanpa /etc/passwd image tidak bisa memakai USER non-root, dan aplikasi HTTPS perlu menyalin sertifikat CA sendiri. Varian distroless menyelesaikan keduanya dengan tetap jauh lebih kecil dari debian:bookworm-slim.

Membangun dan Menjalankan Image #

Di mesin yang memasang Docker, dua perintah ini mengubah Dockerfile menjadi image lalu menjalankannya:

bash
docker build -t penghitung-prima:1.0 .
docker run --rm penghitung-prima:1.0

Perintah docker build membuat image penghitung-prima berversi 1.0 dari Dockerfile di folder saat ini; docker run --rm menjalankan image itu sekali lalu membuang containernya. Sesuai pengakuan di awal chapter, kedua perintah tidak bisa kami jalankan di lingkungan penulisan — which docker tidak mencetak apa pun dan keluar dengan exit code 1 — sehingga keluarannya tidak kami tampilkan alih-alih mengarang. Di mesin Anda, periksa docker images untuk ukuran image dan docker history untuk kontribusi tiap layer.

Ringkasan #

Multi-stage build memisahkan dua dunia yang bertolak belakang: tahap builder memakai image Rust ratusan megabyte untuk mengompilasi, tahap runtime hanya membawa image dasar kecil plus satu biner release. Dengan debian:bookworm-slim image kita puluhan megabyte; dengan distroless ±9 MB; dengan scratch plus biner musl statis, image bisa di bawah satu megabyte. Trik caching dependensi menjaga iterasi tetap cepat, sedangkan disiplin non-root dan pin versi menjaga image aman dan bisa direproduksi. Karena Docker tidak terpasang di lingkungan penulisan, seluruh Dockerfile disajikan tanpa keluaran eksekusi asli dan angka ukurannya dikutip dari API registri yang kami periksa langsung. Image terkirim, tugas terakhir section ini adalah membuktikan klaim kecepatan secara jujur di chapter Benchmark Sederhana.