Tutorial Docker Multistage Build: Optimasi Image Size hingga 90%
AW
Axel W

Dipublikasikan 17 Juni 2026

Tutorial Docker Multistage Build: Optimasi Image Size hingga 90%

Docker image yang besar bukan hanya boros storage, tetapi juga memperlambat deployment dan meningkatkan attack surface. Banyak developer Indonesia masih membangun image Docker dengan cara monolith, menyertakan semua build dependency ke dalam image production. Padahal, teknik multistage build hadir sebagai solusi native Docker yang bisa mengurangi ukuran image hingga 90 persen.

Tutorial ini akan membahas cara mengoptimasi Docker image menggunakan multistage build. Kita akan membandingkan Dockerfile tanpa multistage versus dengan multistage, mengukur perbedaan ukurannya, dan menerapkan best practices yang digunakan oleh tim platform engineering di perusahaan teknologi global.

Mengapa Ukuran Image Penting

Sebelum masuk ke teknik, penting untuk memahami dampak ukuran image. Image berukuran 1 GB membutuhkan waktu transfer jauh lebih lama dibanding image 100 MB, terutama di environment dengan bandwidth terbatas. Di CI/CD pipeline, image besar berarti build time lebih lama dan biaya storage yang lebih tinggi.

Lebih penting lagi, image besar biasanya mengandung banyak package yang tidak diperlukan di production. Package build seperti compiler, header file, dan development library bisa menjadi celah keamanan jika tersedia di container yang berjalan. Prinsip least privilege menuntut kita untuk hanya menyertakan apa yang benar-benar dibutuhkan.

Langkah 1: Membuat Aplikasi Sample

Untuk demonstrasi, kita akan menggunakan aplikasi Go sederhana. Go dipilih karena menghasilkan binary statis yang sempurna untuk multistage build. Buat direktori project dan file main.go:

mkdir docker-multistage-demo
cd docker-multistage-demo

# main.go
package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello from optimized container!")
    })
    http.ListenAndServe(":8080", nil)
}

Verifikasi aplikasi berjalan di lokal:

go run main.go

Aplikasi ini akan listen di port 8080 dan mengembalikan pesan sederhana. Kunjungi golang.org/dl jika belum menginstall Go.

Langkah 2: Dockerfile Tanpa Multistage

Buat Dockerfile tradisional yang menempatkan build dan runtime dalam satu stage. Ini adalah pola yang paling umum ditemui di codebase lama.

FROM golang:1.22

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main .

EXPOSE 8080
CMD ["./main"]

Build image dan catat ukurannya:

docker build -t go-app-monolith .
docker images go-app-monolith

Image hasil build akan berukuran sekitar 800 MB hingga 1 GB karena base image golang:1.22 menyertakan compiler, toolchain, dan dependency development yang tidak diperlukan saat runtime.

Langkah 3: Implementasi Multistage Build

Sekarang, ubah Dockerfile untuk menggunakan dua stage: build stage dan runtime stage. Build stage menggunakan image lengkap, sedangkan runtime stage menggunakan image minimal.

# Stage 1: Build
FROM golang:1.22 AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# Stage 2: Runtime
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/

COPY --from=builder /app/main .

EXPOSE 8080
CMD ["./main"]

Build image baru:

docker build -t go-app-multistage .
docker images go-app-multistage

Perbedaannya signifikan. Image multistage biasanya berukuran sekitar 10-20 MB, turun dari 1 GB. Itu adalah pengurangan lebih dari 90 persen. Base image Alpine hanya berukuran sekitar 5 MB, ditambah binary aplikasi kita.

Langkah 4: Optimasi Lebih Lanjut dengan Scratch

Untuk aplikasi yang benar-benar minimal, kita bisa menggunakan base image scratch. Scratch adalah image kosong dari Docker yang tidak berisi apa pun. Ini adalah ukuran terkecil yang mungkin dicapai.

# Stage 1: Build
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# Stage 2: Minimal Runtime
FROM scratch
COPY --from=builder /app/main /main
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/main"]

Build dan verifikasi:

docker build -t go-app-scratch .
docker images go-app-scratch

Image dengan scratch biasanya berukuran kurang dari 10 MB. Namun, perlu diperhatikan bahwa scratch tidak memiliki shell, package manager, atau debugging tools. Jika aplikasi memerlukan DNS resolution atau timezone data, kamu perlu menyalin file tersebut secara manual dari builder stage.

Langkah 5: Best Practices untuk Tim

Menerapkan multistage build di organisasi memerlukan standar yang jelas. Berikut pola yang direkomendasikan:

  • Gunakan nama stage yang deskriptif: builder, tester, production

  • Hindari menyalin file yang tidak perlu dengan .dockerignore

  • Gunakan cache layer secara efisien: urutkan COPY command dari yang paling jarang berubah ke yang paling sering berubah

  • Tambahkan health check di Dockerfile untuk production

  • Scan image dengan docker scan atau Trivy untuk mendeteksi vulnerability

Contoh .dockerignore:

*.md
.git
.env
Dockerfile
docker-compose.yml

Untuk scanning vulnerability, install Trivy dari GitHub Aqua Security Trivy:

trivy image go-app-multistage

Langkah 6: Benchmark dan CI/CD Integration

Ukur dampak nyata dari optimasi ini. Buat script sederhana untuk membandingkan build time dan image size:

echo "Monolith build time:"
time docker build -t go-app-monolith -f Dockerfile.monolith .

echo "Multistage build time:"
time docker build -t go-app-multistage -f Dockerfile.multistage .

echo "Image sizes:"
docker images --format "{{.Repository}}	{{.Size}}" | grep go-app

Di CI/CD pipeline, tambahkan step untuk memvalidasi ukuran image. Jika image melebihi threshold tertentu, gagalkan build. Ini mencegah regresi ukuran di masa depan.

# Contoh GitLab CI
validate_size:
  script:
    - SIZE=$(docker images --format "{{.Size}}" go-app-multistage | sed 's/MB//')
    - if [ "$SIZE" -gt 50 ]; then echo "Image too large"; exit 1; fi

Kesimpulan

Multistage build adalah teknik sederhana namun sangat powerful untuk mengoptimasi Docker image. Dengan memisahkan environment build dan runtime, kita bisa mengurangi ukuran image drastis, meningkatkan keamanan, dan mempercepat deployment. Teknik ini bisa diterapkan tidak hanya untuk Go, tetapi juga untuk Node.js, Python, Rust, dan bahasa lainnya.

Langkah selanjutnya: pelajari distroless image dari Google untuk alternatif Alpine, dan eksplorasi BuildKit untuk fitur cache mount yang lebih canggih. Dokumentasi resmi Docker tersedia di docs.docker.com/build/building/multi-stage.