Docker Image Kamu 2 GB? Ini Masalahnya
Jadi ceritanya gue lagi setup CI/CD pipeline buat project sampingan. Build jalan, image kelar, terus gue push ke registry. Ukurannya? 2.1 GB. Deploy ke VPS 2 core 4 GB RAM — timeout. Pull image-nya aja makan waktu 4 menit.
Itu bukan masalah koneksi. Itu masalah image yang bloated.
Kalau kamu pernah docker images dan lihat ukuran yang bikin mata kedutan, artikel ini buat kamu. Kita bahas teknik minimize Docker image size build dari yang paling basic sampai yang langsung bikin ukuran turun drastis — dengan kode yang bisa langsung kamu coba.
Kenapa Docker Image Bisa Gemuk Banget?
Sebelum fix, kita perlu tau akar masalahnya. Ada beberapa penyebab umum:
1. Base image yang terlalu besar
Kalau kamu pakai FROM node:18 atau FROM python:3.11, kamu udah narik image Ubuntu/Debian full. Ukurannya bisa 300–900 MB cuma untuk base-nya.
2. Layer yang numpuk sampah
Setiap RUN di Dockerfile bikin layer baru. Kalau kamu install package terus hapus cache di command terpisah, layer lama tetap nyimpen file-file itu.
3. Source code dan dev dependencies ikut masuk
Node modules buat development, test files, .git folder — semua ini sering ikut ke dalam image production kalau nggak difilter.
4. Nggak pakai .dockerignore
Tanpa file ini, COPY . . bakal nyalin semua hal termasuk node_modules lokal, .env, dan folder .git yang bisa ratusan MB.
Teknik 1: Ganti Base Image ke Alpine atau Slim
Ini langkah pertama yang paling gampang. Cukup ganti base image.
# Sebelum — image penuh
FROM node:18
# Sesudah — pakai Alpine
FROM node:18-alpine
# Atau pakai slim (lebih kompatibel dari Alpine)
FROM node:18-slim
Perbandingan ukuran base image:
| Base Image | Ukuran |
|---|---|
node:18 |
~950 MB |
node:18-slim |
~240 MB |
node:18-alpine |
~170 MB |
Gotcha: Alpine pakai musl libc bukan glibc. Beberapa native module Node.js (kayak bcrypt, sharp) bisa gagal compile di Alpine. Kalau kamu kena error cannot find -lgcc_s, coba node:18-slim dulu sebelum debugging lebih jauh.
Untuk Python:
# Sebelum
FROM python:3.11
# Sesudah
FROM python:3.11-slim-bookworm
Teknik 2: Multi-Stage Build — Ini yang Paling Ngaruh
Ini teknik paling powerful untuk minimize Docker image size build. Konsepnya: kamu pakai satu stage untuk build, satu stage lagi untuk run. Hanya artifact yang dibutuhkan yang dibawa ke stage final.
Contoh untuk Node.js (Express/Next.js)
# Stage 1: Builder
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files dulu (biar layer cache optimal)
COPY package*.json ./
# Install semua dependencies termasuk devDependencies
RUN npm ci
COPY . .
# Build aplikasi
RUN npm run build
# Stage 2: Production runner
FROM node:18-alpine AS runner
WORKDIR /app
# Set environment ke production
ENV NODE_ENV=production
# Copy hanya package files
COPY package*.json ./
# Install HANYA production dependencies
RUN npm ci --omit=dev && npm cache clean --force
# Copy hasil build dari stage builder
COPY --from=builder /app/dist ./dist
# Kalau pakai Next.js, copy folder .next
# COPY --from=builder /app/.next ./.next
# COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "dist/index.js"]
Hasil? Image yang tadinya 1.2 GB bisa turun ke 150–200 MB.
Contoh untuk Go (hasilnya paling ekstrem)
Go compiled binary bisa jalan di scratch image — literally image kosong.
# Stage 1: Build binary
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Build dengan CGO disabled supaya binary static
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/server
# Stage 2: Jalankan di scratch
FROM scratch
# Kalau butuh SSL certificates (untuk HTTPS calls)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/main /main
EXPOSE 8080
ENTRYPOINT ["/main"]
Image Go dengan scratch? Bisa sekecil 8–15 MB. Bandingkan dengan image Go biasa yang 800 MB+.
Gotcha untuk Go + scratch: Kalau aplikasi kamu baca file (template HTML, config YAML), file itu nggak ada di scratch image. Kamu harus COPY juga, atau embed file ke dalam binary pakai //go:embed.
Teknik 3: Optimalkan Layer Caching
Ini bukan cuma soal ukuran — ini juga soal kecepatan build. Layer yang nggak berubah di-cache Docker, jadi nggak perlu di-build ulang.
Aturannya simpel: taruh yang jarang berubah di atas, yang sering berubah di bawah.
# SALAH — source code berubah, tapi npm install ikut jalan ulang
FROM node:18-alpine
WORKDIR /app
COPY . . # <-- semua file dicopy dulu
RUN npm ci # <-- ini jalan ulang setiap ada perubahan kode
# BENAR — npm install hanya jalan ulang kalau package.json berubah
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./ # <-- copy package files dulu
RUN npm ci # <-- ini di-cache selama package.json sama
COPY . . # <-- baru copy source code
Perbedaannya? Kalau kamu cuma ubah satu baris di index.js, dengan pattern yang benar npm ci nggak jalan ulang — langsung pakai cache. Build bisa dari 3 menit jadi 20 detik.
Teknik 4: Gabungkan RUN Commands dan Bersihkan Cache
Setiap RUN bikin layer baru. Layer yang lama tetap ada meski kamu hapus file di layer berikutnya.
# SALAH — apt cache tetap tersimpan di layer pertama
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# BENAR — semua dalam satu layer, cache dihapus di layer yang sama
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
Flag --no-install-recommends juga penting — ini mencegah apt install package-package rekomendasi yang sering nggak kamu butuhkan.
Untuk Alpine (pakai apk):
RUN apk add --no-cache curl git
--no-cache di Alpine sudah otomatis skip download index cache, jadi nggak perlu cleanup manual.
Teknik 5: .dockerignore yang Proper
File .dockerignore itu seperti .gitignore tapi untuk Docker build context. Tanpa ini, COPY . . bisa nyalin ratusan MB file yang nggak perlu.
Buat file .dockerignore di root project:
# Version control
.git
.gitignore
# Dependencies (akan di-install ulang di container)
node_modules
venv
__pycache__
*.pyc
# Build artifacts lokal
dist
build
.next
# Environment dan secrets
.env
.env.local
.env.*.local
# Editor dan OS files
.DS_Store
.vscode
.idea
*.swp
# Test dan docs
coverage
.nyc_output
*.test.js
__tests__
docs
README.md
# Docker files sendiri (opsional)
Dockerfile*
docker-compose*
Gotcha: Kalau kamu punya monorepo, pastikan .dockerignore nggak exclude folder yang dibutuhkan oleh package lain. Ini sering jadi source of confusion waktu build tiba-tiba gagal karena module nggak ketemu.
Teknik 6: Audit Image dengan dive
Setelah apply semua teknik di atas, kamu perlu tau layer mana yang masih makan space. Tool-nya namanya dive.
Install:
# macOS
brew install dive
# Linux
wget https://github.com/wagoodman/dive/releases/download/v0.12.0/dive_0.12.0_linux_amd64.deb
sudo apt install ./dive_0.12.0_linux_amd64.deb
Jalankan:
dive your-image-name:tag
dive bakal kasih kamu tampilan interaktif layer per layer — kamu bisa lihat file apa yang ditambah, diubah, atau dihapus di setiap layer. Kalau ada layer yang ukurannya gede tapi isinya file-file aneh, ini tempat kamu debug.
Kamu juga bisa pakai command ini untuk quick check tanpa interactive mode:
# Lihat ukuran tiap layer
docker history your-image-name:tag
# Format lebih readable
docker history --no-trunc --format "{{.Size}}\t{{.CreatedBy}}" your-image-name:tag | sort -rh | head -20
Hasil Nyata: Sebelum vs Sesudah
Ini perbandingan dari project Express.js yang gue optimize:
| Tahap | Ukuran Image | Waktu Build |
|---|---|---|
| Awal (node:18, no optimization) | 1.8 GB | 4m 20s |
| Ganti ke node:18-alpine | 980 MB | 3m 10s |
| Tambah .dockerignore | 820 MB | 2m 50s |
| Fix layer caching | 820 MB | 45s (cached) |
| Multi-stage build | 185 MB | 1m 10s |
| Gabungkan RUN + cleanup | 162 MB | 1m 05s |
Dari 1.8 GB ke 162 MB. Deploy ke VPS jadi 10x lebih cepat, dan storage registry nggak jebol.
Langkah Selanjutnya
Kalau kamu udah apply semua teknik di atas, ini yang bisa kamu explore lebih lanjut:
Pakai BuildKit — Enable BuildKit untuk parallel build dan cache yang lebih smart:
export DOCKER_BUILDKIT=1
docker build -t myapp .
Atau tambahkan di docker-compose.yml:
services:
app:
build:
context: .
dockerfile: Dockerfile
x-bake:
group:
default:
targets: ["app"]
Distroless images — Google punya koleksi image minimal yang bahkan nggak punya shell. Cocok untuk production workload yang butuh security ketat:
FROM gcr.io/distroless/nodejs18-debian12
Scan vulnerability — Image kecil biasanya juga punya attack surface lebih kecil. Pakai docker scout atau trivy untuk scan CVE sebelum push ke production.
Gue sendiri sekarang udah jadiin multi-stage build sebagai default untuk semua project — bahkan yang kecil sekalipun. Kebiasaan ini yang bikin deploy tetap ringan meski project makin gede. Coba apply satu teknik dulu, ukur hasilnya dengan docker images, terus lanjut ke teknik berikutnya.