TLDR #
Docker image sering gemuk karena menyertakan seluruh toolchain build (compiler, dependency dev, source) padahal yang dibutuhkan produksi hanyalah artefak akhir. Multi-stage build memisahkan proses build dan runtime ke dalam dua tahap, sehingga image final hanya berisi yang benar-benar diperlukan — bisa memangkas ukuran dari ratusan MB jadi puluhan MB.
Pendahuluan #
Image Docker yang besar memperlambat pull, memakan disk, dan memperluas permukaan serangan. Penyebab paling umum: semua yang dipakai saat build (compiler, SDK, dependency development) ikut terbawa ke image produksi.
Multi-stage build adalah solusinya. Kamu memakai satu FROM untuk tahap build, lalu FROM kedua untuk tahap runtime, dan hanya menyalin artefak yang diperlukan.
Contoh Tanpa Multi-Stage (Gemuk) #
Contoh aplikasi Go sederhana:
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go build -o myapp .
CMD ["./myapp"]Masalahnya: image ini mengandung seluruh toolchain Go (ratusan MB) padahal aplikasi yang berjalan hanya butuh binary hasil build.
Versi Multi-Stage (Ramping) #
# Tahap 1: build
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .
# Tahap 2: runtime
FROM alpine:3.20
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]Perbedaannya besar:
| Image | Ukuran khas |
|---|---|
golang:1.22 (tanpa multi-stage) |
~800 MB |
alpine + binary (multi-stage) |
~15 MB |
Konsep Utama #
AS — memberi nama tahap
#
FROM node:20 AS builderCOPY --from — menyalin antar tahap
#
COPY --from=builder /app/dist /usr/share/nginx/htmlKamu bisa menyalin dari tahap mana pun yang sudah diberi nama.
Contoh Node.js #
Tanpa optimasi #
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["node", "dist/index.js"]Multi-stage #
# Tahap build
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Tahap runtime
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev
CMD ["node", "dist/index.js"]npm ci --omit=dev hanya menginstal dependency produksi, bukan dependency development.
Contoh Python #
# Tahap build
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Tahap runtime
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY app/ ./app
CMD ["python", "-m", "app"]Tips Optimasi Lanjutan #
| Teknik | Efek |
|---|---|
Pilih base image minimal (alpine, slim) |
Kurangi ukuran drastis |
Pisahkan COPY package.json dulu |
Manfaatkan cache layer |
npm ci bukan npm install |
Install deterministik & cepat |
--no-cache-dir (pip) |
Hindari cache pip |
.dockerignore |
Jangan salin file tak perlu |
Gabungkan perintah RUN |
Kurangi jumlah layer |
.dockerignore
#
Buat file .dockerignore:
node_modules
.git
*.log
.envFile yang di-ignore tidak ikut terkirim ke build context, mempercepat build dan menjaga image bersih.
Memeriksa Ukuran Image #
docker imagesBandingkan ukuran sebelum dan sesudah multi-stage. Untuk melihat riwayat layer:
docker history nama-imageTroubleshooting #
Binary tidak bisa dijalankan di alpine #
Aplikasi yang di-build dengan glibc (misal Go tanpa CGO) biasanya aman di alpine. Tapi jika error not found, coba:
FROM debian:bookworm-slimatau aktifkan CGO_ENABLED=0 saat build.
Dependency hilang di tahap runtime #
Pastikan menyalin semua artefak yang dibutuhkan runtime (bukan hanya source), misalnya node_modules produksi atau library sistem.
Cache layer tidak berfungsi #
Urutan COPY memengaruhi cache. Salin file yang jarang berubah (seperti package.json) lebih dulu.
Ringkasan #
| Konsep | Inti |
|---|---|
FROM ... AS name |
Definisikan tahap build |
COPY --from=name |
Salin artefak ke tahap runtime |
| Base minimal | alpine / slim |
.dockerignore |
Kurangi build context |
Dokumentasi: docs.docker.com/build/building/multi-stage/