Membawa aplikasi Axum ke produksi: multi-stage Dockerfile dengan image minimal (scratch/distroless), pengaturan worker & env, lalu deploy ke VPS atau platform managed seperti Fly.io dan Render.

Semua fitur sudah dibangun — sekarang saatnya aplikasi hidup di luar laptop kalian. Episode ini membahas deployment: containerization dengan Docker (image sekecil mungkin), pengaturan worker/runtime, dan pilihan platform — VPS mandiri atau managed seperti Fly.io dan Render.
Mengapa deployment dibahas sebagai episode tersendiri? Karena aplikasi Axum memiliki kebutuhan khas: binary Rust yang statis, konfigurasi via env vars (episode 16), graceful shutdown (episode 16), dan arsitektur reverse proxy (episode 20). Memahami cara mem-packaging binary ini dengan benar menentukan apakah deploy berjalan mulus atau jadi siklus "kenapa gagal di server padahal jalan di laptop".
Keunggulan Rust: binary yang dihasilkan statis dan mandiri — tidak butuh runtime interpreter. Kita bisa menghasilkan image sekecil puluhan MB, bahkan beberapa MB jika di-strip. Ini jauh lebih kecil dari image Node/Python (500MB+).
# ---------- Stage 1: build ----------
FROM rust:1.80 AS builder
WORKDIR /app
# Dependency caching: salin Cargo.toml dulu, build deps,
# lalu salin source — perubahan source tidak mengulang build deps
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release
# ---------- Stage 2: runtime ----------
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /app/target/release/axum-api /usr/local/bin/app
ENV RUST_LOG=info
EXPOSE 3000
CMD ["app"]Dua stage memisahkan build (berat, penuh toolchain) dari runtime (ringan). Lapisan yang dibawa ke produksi hanya yang ada di stage kedua.
Note
Binary Rust Rust terhubung ke libc (glibc pada rust:1.80). Karena itu stage runtime memakai debian:bookworm-slim + ca-certificates (untuk TLS — wajib agar koneksi HTTPS keluar bekerja). Target x86_64-unknown-linux-musl bisa menghasilkan binary fully static untuk scratch — kita bahas di bawah.
Untuk image yang lebih minimal, dua opsi:
Image tanpa package manager, shell, atau tool apa pun — hanya binary:
FROM gcr.io/distroless/cc-debian12
WORKDIR /app
COPY --from=builder /app/target/release/axum-api /usr/local/bin/app
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 3000
CMD ["app"]Jika binary di-build dengan target musl (fully static), bisa jalan di image kosong total:
# Stage 1: build dengan musl
FROM rust:1.80 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
# Stage 2: image kosong
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/axum-api /app
# jalankan sebagai user non-root
USER 1000:1000
EXPOSE 3000
ENTRYPOINT ["/app"]Kelebihan scratch: image beberapa MB. Kekurangan: tidak ada shell — debugging masuk container jadi terbatas (docker exec tidak berguna tanpa /bin/sh). Untuk produksi modern, distroless adalah titik manis: kecil namun tetap bisa di-debug.
Warning
Di image scratch/distroless, tidak ada shell dan tidak ada user root konvensional. Jangan pasang HEALTHCHECK yang butuh curl/wget — gunakan check TCP via Docker/K8s, atau sertakan binary healthcheck statis. Dan jalankan sebagai USER 1000:1000 — jangan root di production.
Rust/tokio tidak butuh tuning worker seperti Node (cluster) atau Gunicorn — tokio sudah multi-thread out of the box. Hal yang perlu diatur:
| Aspek | Pengaturan |
|---|---|
| Thread count | TOKIO_WORKER_THREADS (default = core CPU) |
| Worker threads | --threads flag saat runtime (jika perlu) |
RUST_LOG | Dikontrol env var (episode 17) |
| File descriptor | Batas ulimit container harus cukup (untuk banyak koneksi WebSocket) |
| Memory | Batasi di orchestrator, bukan di aplikasi |
Contoh pengaturan di platform managed:
RUST_LOG=axum_api=info
TOKIO_WORKER_THREADS=4
BIND_ADDR=0.0.0.0:8080Catatan penting: BIND_ADDR harus 0.0.0.0:8080 di dalam container, bukan 127.0.0.1 — port di-expose container, dan proxy/ingress platform mengarahkan ke sana. Port 8080 sering menjadi konvensi platform (Render, Cloud Run).
Arsitektur VPS yang benar memakai reverse proxy dari episode 20:
Langkah-langkah di VPS:
# 1. Build image
docker build -t axum-api:1.0 .
# 2. Jalankan dengan env yang benar
docker run -d --name axum-api \
-p 127.0.0.1:3000:3000 \
--env-file .env.prod \
--restart unless-stopped \
axum-api:1.0
# 3. nginx di depan (konfigurasi dari episode 20)
systemctl reload nginx
# 4. Verifikasi
curl -sI https://api.kita.id/api/healthPoin penting: -p 127.0.0.1:3000:3000 — port hanya diekspos ke lokal, bukan ke publik; nginx yang menghadap dunia. --env-file memuat secret tanpa menaruhnya di command line history.
Untuk update: build image baru, stop, start. Untuk otomasi lengkap (CI/CD), episode 26 membahas pipeline; di sini fokus pada kebenaran langkah manual.
Platform managed menghilangkan manajemen VPS — kalian hanya menyediakan image dan env.
Render mengenali Rust/Axum; deploy via service web dengan image container atau build dari repo:
services:
- type: web
name: axum-api
runtime: image
image:
url: registry.hub.docker.com/user/axum-api:latest
envVars:
- key: DATABASE_URL
sync: false # diisi lewat dashboard
- key: RUST_LOG
value: info
healthCheckPath: /api/health
numInstances: 1Render menangani restart, log, dan HTTPS untuk domain .onrender.com.
Fly membangun dari Dockerfile langsung — flyctl launch mendeteksi project:
fly launch # deteksi Dockerfile, buat konfigurasi
fly secrets set DATABASE_URL=postgres://... JWT_SECRET=...
fly deployFile fly.toml yang relevan:
[env]
BIND_ADDR = "0.0.0.0:8080"
RUST_LOG = "info"
[[services]]
internal_port = 8080
protocol = "tcp"
[[services.ports]]
http_port = 80
[[services.ports]]
http_port = 443Fly mengirim SIGTERM saat scale-down — dan aplikasi kita (episode 16) sudah menanganinya dengan graceful shutdown. Inilah alasan kita membangun itu lebih dulu.
Tip
Platform managed mengekspos health check. Pastikan route /api/health (yang kita bangun di episode 9) mengembalikan 200 dan tidak butuh auth. Platform menandai instance "unhealthy" jika check gagal — endpoint ini menentukan keandalan restart otomatis.
| Kriteria | VPS | Managed (Fly/Render) |
|---|---|---|
| Kontrol | Penuh | Terbatas pada abstraksi |
| Biaya | Murah, tapi kelola sendiri | Lebih mahal, tapi hemat waktu |
| DevOps effort | Tinggi (server, TLS, update) | Rendah |
| Scaling | Manual | Semi-otomatis |
| Cocok untuk | Engineer berpengalaman | Tim kecil / cepat rilis |
Tidak ada jawaban benar universal — kalian bisa juga kombinasi: VPS untuk kontrol, managed untuk kecepatan. Episode ini membekali keduanya.
Checklist sebelum menganggap deploy sukses:
# 1. Health check publik
curl -s -o /dev/null -w "%{http_code}\n" https://api.kita.id/api/health
# 2. TLS berfungsi (episode 20)
curl -sI https://api.kita.id/api/health | head -1
# 3. Endpoint ber-auth masih bekerja
curl -s -o /dev/null -w "%{http_code}\n" \
https://api.kita.id/me -H "Authorization: Bearer $TOKEN"
# 4. Restart mulus (SIGTERM)
# deploy versi baru, amati: koneksi tidak terputus
# 5. Log observability masuk ke aggregator (episode 17)Pada episode 24 ini aplikasi kalian siap hidup di produksi:
debian-slim / distroless / scratch.ca-certificates wajib untuk TLS keluar.BIND_ADDR=0.0.0.0:8080, RUST_LOG, worker threads via env.Di episode 25 selanjutnya kita tingkatkan performa: performance & benchmarking — zero-copy, allocation tuning, biaya middleware, dan benchmark dengan criterion + load test. Sampai jumpa di episode 25!