Ukuran Image #
Dalam praktik modern software engineering, Docker image bukan sekadar artefak build — ia adalah bagian dari supply chain aplikasi. Ukuran image yang terlalu besar sering kali jadi sumber masalah tersembunyi: build CI lambat, pull image lama di cluster, bandwidth registri membengkak, cold start container berat, dan attack surface yang lebih luas. Semua biaya ini berulang di setiap deployment, di setiap node baru, di setiap autoscaling event.
Artikel ini adalah artikel konseptual yang membahas prinsip umum dan pola pikir dalam mengecilkan Docker image, tanpa terikat pada bahasa pemrograman tertentu. Untuk implementasi teknis spesifik, setiap bahasa (Go, Java, Python, Rust, Node.js, Ruby, PHP) dibahas terpisah di artikel lanjutan.
Tujuan utama artikel ini adalah membentuk mental model yang benar: mengapa image bisa membengkak, apa saja faktor utamanya, dan strategi umum apa yang bisa diterapkan lintas bahasa dan framework. Setelah memahami fondasinya, pembahasan teknis per bahasa tidak lagi terasa sebagai kumpulan trik optimasi, melainkan bagian dari satu pendekatan desain yang utuh.
Mengapa Ukuran Image Penting? #
Sebelum masuk ke teknis, penting untuk memahami dampak ukuran image di production. Image besar bukan hanya masalah estetika — ia punya konsekuensi nyata yang berulang setiap hari.
1. Kecepatan CI/CD. Pipeline CI yang membangun image 1 GB akan butuh waktu jauh lebih lama dari yang membangun image 100 MB. Ini terjadi di setiap commit, di setiap pull request, di setiap release tag. Total waktu yang terbuang bisa signifikan.
2. Waktu deploy dan scaling. Di Kubernetes, ECS, atau Swarm, node baru harus menarik image sebelum container bisa jalan. Image besar memperlambat autoscaling, memperlambat rolling update, dan memperlambat recovery saat incident. Saat traffic spike, node baru butuh waktu lebih lama untuk siap menerima request.
3. Biaya infrastruktur. Bandwidth registri, storage registri, dan waktu compute meningkat seiring ukuran image. Pada skala ratusan deployment per hari, biaya ini nyata. Cloud provider sering mengenakan biaya untuk data transfer dari registri — image besar = biaya lebih tinggi.
4. Keamanan dan attack surface. Setiap package, library, dan tool dalam image adalah potensi vulnerability. Image besar biasanya membawa lebih banyak OS package dan dependency yang tidak terpakai. Lebih banyak paket = lebih banyak CVE yang harus dipatch. Lebih banyak tooling = lebih banyak vektor serangan.
5. Operasional dan debugging. Image ramping lebih mudah dipahami, diaudit, dan di-debug. Engineer baru bisa lebih cepat memahami apa yang ada di dalam image. Security audit juga lebih mudah karena ada lebih sedikit hal yang harus dicek.
6. Cold start untuk serverless container. Untuk AWS Fargate, Google Cloud Run, atau Azure Container Apps, cold start time dipengaruhi ukuran image. Image 50 MB bisa start dalam 200ms, image 1 GB bisa butuh 3-5 detik. Untuk workload interaktif, ini user experience yang terasa.
flowchart LR
A[Image Besar] --> B[Build CI Lambat]
A --> C[Pull Image Lambat]
A --> D[Deploy Scaling Lambat]
A --> E[Biaya Infrastruktur Naik]
A --> F[Attack Surface Luas]
A --> G[Debugging Sulit]
B & C & D & E & F & G --> H[Operasional Mahal & Berisiko]
Apa Penyebab Docker Image Menjadi Besar? #
Memahami akar penyebab image membengkak adalah langkah pertama untuk bisa mengendalikannya. Beberapa penyebab umum terjadi di hampir semua bahasa dan framework.
1. Base Image Terlalu Besar #
Base image menentukan fondasi ukuran image kamu. Jika kamu mulai dari ubuntu:latest (~70 MB) atau node:latest (~900 MB), kamu sudah memulai dengan beban yang berat — bahkan sebelum menambahkan satu baris kode aplikasi.
Image kecil dimulai dari base image kecil. Image besar dimulai dari base image besar. Kedengarannya jelas, tapi di production masih banyak Dockerfile yang menggunakan ubuntu atau python:latest tanpa alasan teknis yang kuat.
2. Build Tools Ikut ke Runtime Image #
Ini adalah penyebab paling umum dari image yang tidak perlu besar. Saat kamu RUN apt-get install -y gcc atau RUN apk add --no-cache build-base di Dockerfile, tool tersebut digunakan untuk meng-compile dependency. Tapi setelah build selesai, tidak ada alasan untuk membawa compiler ke runtime image.
Contoh konkret:
// ✗ Compiler dan tool build masuk ke runtime image
FROM python:3.12
RUN apt-get update && apt-get install -y gcc libpq-dev
RUN pip install psycopg2
# Image sekarang punya gcc, libpq-dev, header file, dll — semua tidak dibutuhkan runtime
// ✓ Multi-stage: compiler di build stage, runtime image ramping
FROM python:3.12 AS builder
RUN apt-get update && apt-get install -y gcc libpq-dev
RUN pip install --prefix=/install psycopg2
FROM python:3.12-slim
COPY --from=builder /install /usr/local
# Runtime image hanya punya library psycopg2, tanpa compiler
3. Cache Package Manager Tidak Dibersihkan #
Saat apt-get install, cache download disimpan di /var/lib/apt/lists/. Saat apk add, cache di /var/cache/apk/. Saat pip install, cache di ~/.cache/pip/. Cache ini bisa ratusan MB dan tidak akan otomatis dihapus.
// ✗ Cache menumpuk di image
RUN apt-get update
RUN apt-get install -y curl
# Image punya cache apt + curl
// ✓ Cache dihapus di layer yang sama dengan instalasi
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
Prinsip ini berlaku untuk semua package manager: apt, apk, yum, dnf, pip, npm, gem, composer. Selalu hapus cache di layer yang sama dengan instalasi.
4. Build Context Terlalu Besar #
Docker daemon menerima seluruh build context sebelum build dimulai. Jika context 500 MB, build akan lambat bahkan sebelum instruksi pertama dijalankan. Sumber umum context besar:
node_modules/lokal yang sudah di-install di laptop.- Folder
.git/yang penuh history. - File log, output build, dan file temporary.
- Dokumentasi internal.
- File editor
.vscode/,.idea/. - Asset besar (gambar, video, dataset).
Solusinya: .dockerignore yang tegas.
# .dockerignore
node_modules
.git
.env
*.log
dist
build
.vscode
.idea
.DS_Store
coverage
.dockerignore mempercepat build, mengurangi ukuran context, dan mencegah kebocoran data sensitif.
5. “One Image Fits All” #
Image yang sama dipakai untuk development, testing, CI, dan production. Akibatnya image membawa tool debugging, test runner, dokumentasi, dan dependency development yang tidak dibutuhkan production.
Solusi yang lebih bersih:
- Base Dockerfile dengan setup minimal.
Dockerfile.devdengan tambahan tooling untuk development.- Multi-target dalam satu Dockerfile dengan
targetargumen.
# Single Dockerfile dengan multi-target
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS dev
RUN npm install --save-dev nodemon
CMD ["npm", "run", "dev"]
FROM base AS prod
COPY . .
RUN npm run build
CMD ["node", "dist/index.js"]
Build dengan target:
docker build --target dev -t myapp:dev .
docker build --target prod -t myapp:prod .
6. Dependency Tidak Di-audit #
Banyak proyek membawa dependency yang tidak benar-benar dipakai, atau yang hanya relevan di development. Tanpa audit rutin, dependency ini numpuk di image.
Strategi audit:
- Pisahkan
requirements.txt(prod) danrequirements-dev.txt. - Gunakan
npm ci --omit=devuntuk install hanya production dependency. - Audit berkala dengan
depcheck(Node.js),pip-autoremove(Python), ataucargo machete(Rust). - Hapus starter/boilerplate dependency yang tidak terpakai.
Prinsip Dasar Mengurangi Ukuran Image #
Bagian ini adalah inti dari seluruh strategi. Tujuh prinsip ini berlaku untuk hampir semua bahasa dan framework.
1. Pisahkan Build Environment dan Runtime Environment #
Prinsip paling fundamental dalam optimasi image:
Apa yang dibutuhkan saat build belum tentu dibutuhkan saat runtime.
Cara mencapainya: multi-stage build. Stage pertama berisi tool build. Stage kedua hanya menyalin artifact yang dibutuhkan. Compiler, header file, dan tool build tetap di stage pertama dan tidak pernah masuk ke image akhir.
flowchart LR
subgraph Build[Build Stage - dibuang]
A[Base Image + Compiler]
B[Source Code]
C[Compile / Build]
A --> B --> C
end
C --> D[Artifact / Binary]
subgraph Runtime[Runtime Stage - di-ship]
E[Base Image Minimal]
D --> F[Copy Artifact]
F --> G[Image Ramping]
end
2. Pilih Base Image yang Tepat #
Base image yang ideal adalah yang paling sesuai dengan kebutuhan runtime, bukan yang paling kecil secara absolut.
Urutan umum dari kecil ke besar:
| Base Image | Ukuran | Use Case |
|---|---|---|
scratch |
0 MB | Static binary Go, Rust |
distroless |
10-30 MB | Java, Node.js, Python production |
alpine |
5-10 MB | Bahasa dengan glibc compatibility issue |
-slim |
50-150 MB | Default yang aman |
| default tag | 200-900 MB | Development, prototyping |
ubuntu/debian |
70-100 MB | Ketika butuh tool OS lengkap |
Base image bukan soal “yang paling kecil”, tapi “yang paling tepat”.
Kadang image alpine yang sangat kecil tidak bisa dipakai karena kompatibilitas library. Image slim yang lebih besar adalah pilihan yang benar. Yang penting adalah kesadaran akan trade-off dan konsistensi keputusan.
3. Minimalkan Isi Image Final #
Tanyakan pada diri sendiri, untuk setiap file dan tool di image:
- Apakah ini benar-benar dibutuhkan saat aplikasi berjalan?
- Apakah ini hanya untuk debugging?
- Apakah ini dokumentasi yang relevan di production?
- Apakah ini dev dependency yang ikut masuk?
Prinsip: image production seharusnya hanya berisi apa yang dibutuhkan untuk menjalankan aplikasi. Tidak lebih.
4. Optimalkan Layer #
Layer Docker yang rapi menghasilkan image yang lebih kecil dan build yang lebih cepat.
Prinsip umum:
- Gabungkan perintah yang saling berkaitan dalam satu
RUN. - Hapus cache dan file temporary di layer yang sama dengan pembuatannya.
- Urutkan instruksi untuk cache yang efektif.
- Hindari
apt-get updateterpisah dariapt-get install— selalu gabungkan.
// ✗ Tiga layer, cache tidak dibersihkan
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
// ✓ Satu layer, semua bersih
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
5. Kelola Build Context dengan .dockerignore
#
.dockerignore adalah senjata yang sering diremehkan. Build context yang besar = build lambat, dan potensi kebocoran data.
Idealnya, Docker hanya menerima file yang relevan untuk build:
- Source code aplikasi.
- File konfigurasi build.
- File dependency definition (
package.json,requirements.txt,Cargo.toml).
Bukan:
- Dependency lokal yang sudah di-install (
node_modules,vendor). - Artefak build lama (
dist,build,target). - File editor dan OS.
- File rahasia (
.env, credentials).
6. Bedakan Image Development dan Production #
Image untuk development boleh lebih besar karena akan dipakai untuk iterasi cepat. Image production harus minimal karena akan di-deploy ribuan kali.
Strategi umum:
- Target berbeda dalam satu Dockerfile (lihat contoh multi-target di atas).
- Dockerfile terpisah untuk dev dan prod.
- Override entrypoint di development image untuk hot reload.
7. Audit dan Bersihkan secara Berkala #
Ukuran image bukan sesuatu yang “diatur sekali lalu dilupakan”. Dependency berubah, base image dapat update, dan praktik terbaik berevolusi. Audit berkala adalah investasi kecil yang mencegah image membengkak seiring waktu.
Strategi Teknis Lintas Bahasa #
Meskipun setiap bahasa punya nuansa, ada beberapa strategi teknis yang berlaku umum.
Pilih Base Image yang Sesuai Bahasa #
| Bahasa | Rekomendasi Base Image | Catatan |
|---|---|---|
| Go | gcr.io/distroless/static, alpine, scratch |
Go menghasilkan static binary |
| Java | gcr.io/distroless/java17, eclipse-temurin:*-jre-alpine |
JRE saja, tanpa JDK |
| Node.js | gcr.io/distroless/nodejs20, node:*-slim, node:*-alpine |
Hindari node:latest |
| Python | python:*-slim, python:*-alpine |
Hindari python:latest |
| Rust | gcr.io/distroless/cc, debian:*-slim, alpine, scratch |
Butuh glibc untuk dynamic linking |
| PHP | php:*-fpm-alpine |
Plus composer di build stage |
| Ruby | ruby:*-slim, ruby:*-alpine |
Hindari ruby:latest |
Pisahkan Production vs Dev Dependency #
Hampir semua package manager punya cara memisahkan dependency:
- Node.js:
npm ci --omit=devatau pisahkandependenciesvsdevDependenciesdipackage.json. - Python: pisahkan
requirements.txt(prod) danrequirements-dev.txt. - Ruby:
bundle config set without 'development test'. - PHP:
composer install --no-dev. - Go:
go mod tidymemastikan hanya dependency yang di-import yang masuk. - Rust:
Cargo.toml[dependencies]vs[dev-dependencies]. - Java/Maven:
<scope>provided</scope>atau<scope>test</scope>dipom.xml.
Optimasi Package Manager #
Tiap package manager punya opsi untuk mengecilkan output:
- apt:
apt-get install -y --no-install-recommends(skip rekomendasi opsional). - pip:
pip install --no-cache-dir(skip cache). - npm:
npm ci --omit=dev --no-audit(skip dev dan audit). - gem:
bundle install --no-cacheataugem: --no-document. - composer:
composer install --no-dev --optimize-autoloader --no-scripts.
Gunakan Layer Cache dengan Disengaja #
Urutan instruksi Dockerfile adalah strategi caching:
// Pola yang memaksimalkan cache
FROM base:tag
WORKDIR /app
# Step 1: OS dependency (jarang berubah)
RUN apt-get update && apt-get install -y --no-install-recommends \
package1 package2 \
&& rm -rf /var/lib/apt/lists/*
# Step 2: Application dependency definition (jarang berubah)
COPY package*.json ./
RUN npm ci
# Step 3: Source code (sering berubah)
COPY . .
# Step 4: Build (tergantung source)
RUN npm run build
Dengan pola ini:
- OS dep cache di-reuse selama OS dependency tidak berubah.
- App dep cache di-reuse selama
package*.jsontidak berubah. - Source code layer di-rebuild setiap perubahan kode — tapi layer setelahnya tidak dipengaruhi.
Mengukur dan Memantau Ukuran Image #
Optimasi tanpa pengukuran hanya tebakan. Beberapa tools yang bisa kamu pakai untuk mengukur dan memantau:
Built-in Docker:
# Lihat ukuran image
docker images myapp
# Detail ukuran per layer
docker history myapp
Tool pihak ketiga:
- dive — interactive tool untuk explore layer dan lihat apa yang berubah di tiap layer.
- docker-slim — automated image minification. Bisa mengecilkan image lebih lanjut dengan menghilangkan file yang tidak terpakai.
- crane — extract image metadata dan lihat manifest.
Integrasi CI/CD:
Tambahkan step pengukuran ukuran image di pipeline:
SIZE=$(docker images myapp:latest --format "{{.Size}}")
echo "Image size: $SIZE"
# Fail build jika ukuran di atas threshold
Dashboard:
Untuk monitoring jangka panjang, export ukuran image ke metrics system (Prometheus, Datadog, dll) dan buat alert jika ukuran tiba-tiba membengkak.
Trade-off yang Wajib Dipahami #
Optimasi ukuran image punya trade-off yang harus disadari. Tidak ada solusi yang selalu benar.
Distroless vs Alpine vs Slim:
| Aspek | Distroless | Alpine | Slim |
|---|---|---|---|
| Ukuran | Sangat kecil | Kecil | Sedang |
| libc | glibc | musl | glibc |
| Shell | Tidak | Ya | Ya |
| Package manager | Tidak | Ya (apk) | Ya (apt) |
| Debugging | Sulit | Mudah | Mudah |
| Stabilitas binary | Tinggi | Kadang masalah | Tinggi |
| Cocok untuk | Production mature | Default | Default aman |
Kapan pilih apa:
- Distroless — production high-maturity, security-first, observability sudah solid.
- Alpine — butuh tool di container, ukuran penting, aware dengan isu musl.
- Slim — default yang aman, tidak mau ambil risiko kompatibilitas.
- Default tag — development, prototyping, atau butuh tool OS lengkap.
Ukuran vs observability:
Image yang sangat kecil (distroless, scratch) menyulitkan debugging interaktif. Kamu tidak bisa docker exec -it container sh. Solusinya: invest di observability — structured logging ke STDOUT, metrics endpoint, distributed tracing. Image kecil + observability yang baik > image besar + debugging manual.
Ukuran vs build time:
Image yang sangat optimal sering butuh build time lebih lama (multi-stage, kompilasi, dll). Trade-off ini biasanya sepadan untuk production, tapi bisa jadi beban untuk development.
Dampak Positif Image yang Ramping #
Jika prinsip-prinsip di atas diterapkan dengan benar, dampaknya langsung terasa:
- Build CI lebih cepat — cache bekerja optimal, build berulang tidak perlu dari nol.
- Pull image lebih ringan — node baru siap lebih cepat.
- Deployment lebih responsif — rolling update selesai lebih cepat, recovery lebih singkat.
- Surface keamanan berkurang — lebih sedikit CVE yang harus ditrack.
- Biaya infrastruktur turun — bandwidth, storage, dan compute lebih hemat.
- Cold start lebih cepat — penting untuk serverless dan autoscaling.
- Arsitektur container lebih disiplin — engineer terdorong untuk paham boundary build vs runtime.
Dan yang paling penting: image ramping adalah cerminan dari engineering maturity. Image yang asal-asalan biasanya ditulis oleh engineer yang belum paham trade-off. Image yang ramping ditulis dengan kesadaran penuh akan biaya, keamanan, dan operasional.
Ringkasan #
- Ukuran image bukan masalah estetika — ia berdampak langsung ke CI/CD, scaling, biaya, keamanan, dan cold start.
- Penyebab utama image membengkak: base image besar, build tools ikut ke runtime, cache tidak dibersihkan, build context besar, dan dependency tidak di-audit.
- Prinsip utama: pisahkan build vs runtime, pilih base image yang tepat, minimalkan image final, optimalkan layer, dan kelola
.dockerignore.- Multi-stage build adalah standar untuk memisahkan build environment dari runtime. Compiler dan tool build tidak boleh masuk ke image akhir.
- Bedakan image development dan production — image production harus minimal, image development boleh membawa tool.
- Audit dependency secara berkala — pisahkan prod vs dev dependency, hapus yang tidak dipakai.
- Trade-off yang harus dipahami: distroless vs alpine vs slim punya trade-off ukuran vs observability, dan image kecil butuh investasi di observability.
- Ukur dan pantau — optimasi tanpa pengukuran hanya tebakan. Gunakan
docker history,dive, dan integrasi CI untuk monitoring.- Image ramping adalah cerminan engineering maturity. Ia bukan tujuan akhir, tapi efek samping dari arsitektur build yang bersih.