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:
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:
- Baris 1 (
FROM rust:1.99-slim AS builder): mengambil image Rust resmi varian slim versi 1.99 lalu menamainyabuilder; varian slim memuatgccuntuk penautan tanpa perkakas build tambahan. - Baris 2 (
WORKDIR /app): menetapkan direktori kerja tahap builder; semuaCOPYdanRUNberikutnya relatif terhadap/app. - Baris 3 (
COPY Cargo.toml Cargo.lock ./): menyalin manifes dependensi sebelum source code — urutan ini untuk trik caching di bagian berikutnya. - Baris 4 (
COPY src ./src): menyalin isi foldersrcke dalam image. - Baris 5 (
RUN cargo build --release): mengompilasi program dalam mode release; biner jadi berada di/app/target/release/penghitung-prima. - Baris 7 (
FROM debian:bookworm-slim): memulai tahap kedua dari nol; isi tahap builder tidak dibawa serta. - Baris 8-10 (
RUN apt-get update ... rm -rf /var/lib/apt/lists/*): memasangca-certificatesuntuk aplikasi yang memanggil HTTPS, lalu membersihkan cache apt. - Baris 11 (
COPY --from=builder /app/target/release/...): menyalin satu file biner ke tahap runtime — jembatan satu-satunya antar tahap. - Baris 12 (
USER nobody): menjalankan container tanpa hak istimewa, sehingga proses yang dibajak tidak punya akses root. - Baris 13 (
ENTRYPOINT ["penghitung-prima"]): menetapkan biner kita sebagai proses utama setiapdocker 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 dasar | Ukuran terkompresi amd64 | Peran |
|---|---|---|
rust:1.99 | ±602 MB | toolchain lengkap, terlalu besar untuk runtime |
rust:1.99-slim | ±327 MB | builder pada tahap satu |
debian:bookworm-slim | ±28 MB | runtime glibc untuk biner gnu |
gcr.io/distroless/cc-debian12 | ±9 MB | runtime glibc tanpa shell dan package manager |
alpine:3.20 | ±4 MB | runtime musl untuk biner musl |
scratch | 0 MB | image 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:
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- Baris 4 (
RUN mkdir -p src && echo ... > src/main.rs): membuat source sementara berisi fungsimainstub agarcargo buildberjalan dan mengompilasi semua dependensi proyek. - Baris 5 (
RUN cargo build --release): layer dependensi terkompilasi; hanya dibangun ulang bilaCargo.tomlatauCargo.lockberubah, bukan saat kita menyunting kode. - Baris 6 (
COPY src ./src): source asli menimpa stub; layer mulai titik ini berubah. - Baris 7 (
RUN touch src/main.rs && cargo build --release):touchmemaksa Cargo membangun ulang hanya crate utama; dependensi ditarget/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:
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"]- Baris 2 (
RUN rustup target add ...): memasang pustaka standar untuk target musl di dalam image builder, langkah yang kita kenal dari chapter Cross Compilation. - Baris 6 (
RUN cargo build --release --target ...): mengompilasi biner static linking di dalam container. - Baris 8 (
FROM scratch): tahap runtime dimulai dari image kosong — tidak ada glibc, tidak ada shell, tidak ada apa pun. - Baris 9 (
COPY --from=builder ...): satu-satunya isi image adalah biner kita; dengan pengaturanstrip = "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:
docker build -t penghitung-prima:1.0 .
docker run --rm penghitung-prima:1.0Perintah 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.