Image #
Docker Image adalah pondasi utama dari seluruh ekosistem Docker. Setiap container yang berjalan — tanpa kecuali — selalu berasal dari sebuah image. Image adalah template immutable yang bisa dipindahkan, di-versioning, di-cache, dan di-share lintas mesin dan lintas cloud. Memahami image secara mendalam bukan sekadar pengetahuan teori; ini menentukan seberapa kecil, aman, dan cepat pipeline deployment-mu.
Artikel ini membahas image dari tiga sudut: konseptual (apa image sebenarnya), teknis (bagaimana image disusun dan dibaca), dan praktis (bagaimana membangun image yang efisien untuk production). Setelah membaca artikel ini, kamu akan bisa menjelaskan mengapa image layer bekerja seperti itu, bagaimana multi-stage build bisa memotong ukuran image hingga 90%, dan kapan menggunakan base image tertentu.
Image sebagai Blueprint #
Image adalah file system snapshot plus metadata. Ia bukan satu file monolitik; ia adalah kumpulan layer yang masing-masing menyimpan perubahan dari layer di bawahnya. Ditambah dengan metadata seperti perintah default, port yang diekspos, dan environment variable, image menjadi paket lengkap yang bisa dijalankan di mana saja.
flowchart LR
subgraph IMAGE["Docker Image (immutable)"]
L1[Layer 1: base OS - ubuntu:22.04]
L2[Layer 2: apt install nginx]
L3[Layer 3: COPY config]
L4[Layer 4: CMD metadata]
end
IMAGE -->|"docker run"| CONTAINER["Docker Container<br/>(runtime instance)"]
subgraph CONTAINER_RUNTIME["Container Runtime"]
WRITABLE["Writable Layer<br/>(runtime changes)"]
IMAGE_RO["Image Layers (read-only, shared)"]
end
CONTAINER --> CONTAINER_RUNTIME
Perhatikan perbedaan penting: image immutable, container mutable (dalam artian ia punya writable layer). Image tidak pernah berubah setelah di-build. Container bisa berubah selama berjalan, tapi begitu dihapus, semua perubahannya hilang — image tetap utuh untuk dijalankan lagi.
Analogi yang sering dipakai: image adalah ISO installer OS, container adalah OS yang sedang berjalan dari ISO itu. ISO tidak berubah ketika kamu install ke laptop; ia hanya menjadi instance yang bisa dimodifikasi sesuai penggunaan.
Detail menarik: “immutable” bukan sekadar jargon pemasaran. Immutability image adalah yang membuat deployment bisa reproducible — image yang kamu build hari ini, jika di-pull tiga tahun dari sekarang, akan menghasilkan container yang persis sama. Inilah yang tidak bisa dijamin oleh VM snapshot atau installer tradisional.
Layered Architecture — Pondasi Image #
Setiap image Docker tersusun dari layer yang disusun bertumpuk. Layer adalah inti dari semua optimasi yang akan kita bahas selanjutnya.
Bagaimana Layer Terbentuk #
Setiap instruksi di Dockerfile menghasilkan satu layer baru. Docker merekam filesystem sebelum dan sesudah instruksi, dan hanya menyimpan delta-nya saja.
FROM ubuntu:22.04 # Layer 1: image ubuntu:22.04 (pulled from registry)
RUN apt-get update # Layer 2: delta dari /var/lib/apt
RUN apt-get install -y nginx # Layer 3: delta dari instalasi nginx
COPY ./app /app # Layer 4: delta dari COPY file
CMD ["nginx", "-g", "daemon off;"] # Metadata only, BUKAN layer
Yang penting dari contoh di atas:
FROMmembuat layer dari image yang sudah ada (biasanya base image). Layer ini sering kali berukuran besar (ubuntu:22.04 sekitar 77 MB).RUN,COPY,ADDmembuat layer baru dari hasil eksekusi perintah.CMD,ENTRYPOINT,ENV,EXPOSE,LABELadalah metadata — mereka tidak membuat layer baru, hanya menambahkan entry ke konfigurasi image.
Build Cache — Mengapa Layer Penting untuk Performa #
flowchart TD
A[docker build dimulai] --> B{Cache hit<br/>untuk layer 1?}
B -- Ya --> C[Gunakan layer 1 dari cache]
B -- Tidak --> D[Build layer 1 dari awal]
C --> E{Cache hit<br/>untuk layer 2?}
D --> E
E -- Ya --> F[Gunakan layer 2 dari cache]
E -- Tidak --> G[Build layer 2]
F --> H{Cache hit<br/>untuk layer 3?}
G --> H
H -- Ya --> I[Gunakan layer 3 dari cache]
H -- Tidak --> J[Build layer 3]
Inilah alasan urutan instruksi di Dockerfile sangat penting. Docker membandingkan setiap layer dengan cache berdasarkan checksum dari instruksi DAN isi file yang dirujuk. Jika kamu mengubah satu baris kode di COPY, semua layer setelahnya akan invalid dan di-rebuild.
# ANTI-PATTERN: source code berubah → semua layer setelah COPY invalid
FROM node:20
RUN apt-get install -y curl # layer A
COPY . /app # layer B - berubah tiap commit
RUN npm install # layer C - INVALID setiap build
CMD ["node", "/app/server.js"] # metadata
# BENAR: pisahkan dep install dari source code
FROM node:20
RUN apt-get install -y curl # layer A
COPY package*.json /app/ # layer B - hanya berubah saat dep berubah
RUN npm install # layer C - cached sampai package.json berubah
COPY . /app # layer D - sering berubah, tapi cuma invalid 1 layer
CMD ["node", "/app/server.js"] # metadata
Prinsip utama: letakkan instruksi yang jarang berubah di atas (base image, system packages), dan yang sering berubah (source code, config) di bawah. Ini memaksimalkan cache hit dan meminimalkan waktu build.
Image Manifest dan Konfigurasi #
Image bukan hanya layer-layer filesystem. Ia juga punya manifest dan konfigurasi yang mendeskripsikan layer-layer tersebut.
Image Manifest (OCI Image Manifest Spec) #
Manifest adalah JSON yang disimpan di registry dan mendeskripsikan image:
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"digest": "sha256:b5b2b2c507a0944348e0303112d8d08..."
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:e692418e4d3c..."
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:3e3b1767b397..."
}
]
}
Field penting:
config.mediaTypedandigest— pointer ke JSON konfigurasi image (yang berisi CMD, ENV, dll).layers[]— daftar layer, masing-masing dengan content-addressable digest (sha256:...). Digest ini yang membuat image bisa diverifikasi integritasnya.
Image Config (Detail Image) #
Konfigurasi image menyimpan metadata yang dieksekusi saat container start. Beberapa field kunci:
| Field | Fungsi |
|---|---|
Cmd |
Perintah default + argumen |
Entrypoint |
Executable yang dijalankan |
Env |
Environment variables (KEY=value) |
ExposedPorts |
Port yang didokumentasikan sebagai “dapat diakses” |
WorkingDir |
Working directory default |
User |
UID/GID default untuk proses |
Volumes |
Mount point default |
Architecture |
Arsitektur target (amd64, arm64, dll) |
Os |
OS target (linux, windows) |
Rootfs.diff_ids |
Daftar digest layer (urutan dari bawah ke atas) |
Multi-Architecture Image #
Manifest bisa menunjuk ke manifest list (atau “fat manifest”) yang menggabungkan image untuk arsitektur berbeda. Inilah kenapa docker pull nginx di laptop Apple Silicon (arm64) dan server Intel (amd64) sama-sama berhasil.
flowchart TB
TAG["nginx:latest (tag)"]
LIST["Manifest List"]
TAG --> LIST
LIST --> AMD64["Image: linux/amd64<br/>(sha256:aaa...)"]
LIST --> ARM64["Image: linux/arm64<br/>(sha256:bbb...)"]
LIST --> ARMv7["Image: linux/arm/v7<br/>(sha256:ccc...)"]
LIST --> PPC64LE["Image: linux/ppc64le<br/>(sha256:ddd...)"]
Saat runtime, Docker mendeteksi arsitektur host (via uname -m) dan memilih image yang sesuai dari manifest list. Ini dilakukan secara transparan — kamu tidak perlu tahu detailnya.
Build untuk banyak arsitektur: gunakan docker buildx dengan flag --platform. Buildx membuat builder instance khusus yang bisa melakukan emulation (QEMU) atau multi-node build, lalu menggabungkan hasilnya menjadi satu manifest list yang di-push ke registry.
Multi-Stage Build — Memangkas Ukuran Image #
Multi-stage build adalah salah satu teknik paling berdampak dalam Docker. Ia memungkinkan kamu memisahkan build environment (dengan compiler, tool, intermediate artifact) dari runtime environment (yang hanya berisi binary akhir dan dependency minimal).
Anatomi Multi-Stage Build #
flowchart LR
subgraph STAGE1["Stage 1: builder"]
S1_BASE["FROM golang:1.22-alpine<br/>(300+ MB)"]
S1_CODE["COPY . ."]
S1_BUILD["RUN go build -o app<br/>(binary 20 MB)"]
end
subgraph STAGE2["Stage 2: runtime"]
S2_BASE["FROM gcr.io/distroless/base<br/>(20 MB)"]
S2_COPY["COPY --from=builder /app/app /"]
end
S1_BASE --> S1_CODE --> S1_BUILD
S2_BASE --> S2_COPY
S1_BUILD -.->|"binary only<br/>20 MB"| S2_COPY
Yang terjadi di balik layar:
- Stage 1 menggunakan image besar (Go SDK 300+ MB) untuk meng-compile kode.
- Stage 2 dimulai dari image minimal (distroless, 20 MB).
COPY --from=buildermenyalin hanya binary akhir dari stage 1 ke stage 2.- Stage 1 (beserta semua compiler dan intermediate artifact) dibuang dan tidak masuk ke image final.
Contoh Praktis #
# Stage 1: build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app/server
# Stage 2: runtime
FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /app
COPY --from=builder /app/server /app/server
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/app/server"]
Image akhir:
- Ukuran: ~10–15 MB (vs ~350 MB jika tanpa multi-stage)
- Attack surface: minimal (tidak ada shell, tidak ada package manager)
- User: nonroot (default di distroless)
- Binary: sudah di-strip simbol-nya (
-s -w)
Perbandingan Image Size #
| Base Image | Ukuran Tipikal | Use Case |
|---|---|---|
ubuntu:22.04 |
~77 MB | General purpose, banyak tool |
node:20 |
~900 MB | Development Node.js |
node:20-alpine |
~130 MB | Production Node.js |
python:3.12 |
~1 GB | Development Python |
python:3.12-slim |
~150 MB | Production Python |
gcr.io/distroless/base |
~20 MB | Production compiled binary |
gcr.io/distroless/static |
~2 MB | Static binary Go/Rust |
scratch |
0 MB | Binary static minimal |
Jebakan umum: memilih base image hanya berdasarkan ukuran terkecil.scratchmisalnya tidak punya apa-apa — tidak ada shell, tidak ada libc, tidak ada sertifikat CA. Jika aplikasimu butuhgetaddrinfo()(hampir semua bahasa kecuali Go dengan CGO_ENABLED=0) atau HTTPS calls, image scratch tidak akan bekerja. Pilih base image yang paling minimal untuk kebutuhanmu, bukan yang paling kecil secara absolut.
Image Tag dan Versioning #
Tag adalah label yang ditempel di image untuk identifikasi. Tag bukan hanya versi — ia bisa berupa string apa pun, dan banyak tim menggunakan tag untuk menandai environment, commit SHA, atau tanggal build.
Anatomi Reference Image #
flowchart LR
REF["Reference Image"] --> REG["Registry"]
REG --> NS["Namespace / Organization"]
NS --> REPO["Repository"]
REPO --> IMG["Image Name"]
IMG --> TAG["Tag (default: latest)"]
REF2["nginx:1.25.3-alpine"] --> REG2["docker.io (default)"]
REG2 --> NS2["library (default untuk official image)"]
NS2 --> REPO2["nginx"]
REPO2 --> TAG2["1.25.3-alpine"]
Format lengkap:
[REGISTRY[:PORT]/][NAMESPACE/]REPOSITORY[:TAG][@DIGEST]
Contoh konkret:
docker.io/library/nginx:1.25.3
ghcr.io/unisbadri/myapi:v2.1.0
123456789012.dkr.ecr.us-east-1.amazonaws.com/prod-api:2026-02-07
myregistry.local:5000/internal/worker@sha256:b5b2b2c507a0944348e0303112d8d08...
Strategi Tag yang Baik vs Buruk #
JANGAN di production:
✗ myapp:latest
- "latest" adalah tag default yang otomatis di-resolve ke
digest TERBARU. Tidak ada jaminan reproducibility.
- Jika kamu push image baru dengan tag "latest" hari ini,
container lama yang di-restart bisa menarik image BARU
yang belum kamu test.
GUNAKAN di production:
✓ myapp:v1.4.2
✓ myapp:v1.4.2-alpine
✓ myapp:1.4.2-build.123
✓ myapp:2026-02-07-abc1234 (tanggal + short SHA)
✓ myapp@sha256:b5b2b2c5... (digest penuh, fully reproducible)
Praktik terbaik industri: tag immutable menggunakan digest SHA256. Ini menjamin bahwa image yang kamu deploy adalah image yang persis sama dengan yang kamu test. Tag semver (v1.4.2) adalah untuk humans, digest adalah untuk machines. Banyak CI/CD modern mengombinasikan keduanya: tag untuk readability, digest untuk eksekusi aktual.
Mutability Tag — Penting untuk CI/CD #
Tag di registry bersifat mutable secara default. Artinya, siapa pun yang punya akses push bisa menimpa tag yang sudah ada. Ini menyebalkan untuk deployment pipeline.
# Setelah push pertama, myapp:v1.0 menunjuk ke digest A
docker push myapp:v1.0
# Push kedua (misal re-build) — tag v1.0 sekarang menunjuk ke digest B
docker push myapp:v1.0
Untuk production, nonaktifkan mutability tag di registry:
- AWS ECR: enable
imageTagMutability: IMMUTABLEper repository. - Docker Hub: tidak support immutable tag di level repository, tapi kamu bisa enforce lewat policy IAM.
- Harbor: support immutable tag di level tag, dengan retention rule terpisah.
- GHCR: support immutable tag sejak 2023.
Image Size — Dampak Nyata di Production #
Ukuran image bukan sekadar angka kosmetik. Ia punya dampak langsung di empat area operasional.
flowchart LR
BIG[Image 1.2 GB] -->|pull| SLOW1[Pull time: 5-10 menit]
SLOW1 -->|deploy| SLOW2[Deploy time: lambat]
SLOW2 -->|scale| SLOW3[Scaling time: tinggi]
SLOW3 -->|cost| SLOW4[Network + storage cost]
SMALL[Image 15 MB] -->|pull| FAST1[Pull time: 2-5 detik]
FAST1 -->|deploy| FAST2[Deploy time: cepat]
FAST2 -->|scale| FAST3[Scaling time: rendah]
FAST3 -->|cost| FAST4[Network + storage cost rendah]
Empat Dampak Ukuran Image #
| Area Dampak | Image 1 GB | Image 50 MB | Selisih |
|---|---|---|---|
| Pull time (100 Mbps) | ~80 detik | ~4 detik | 20× lebih cepat |
| Storage di registry | 1 GB | 50 MB | Hemat 95% |
| Cold start (K8s) | Lama, scheduler tunggu image | Cepat, pod siap dalam detik | UX lebih baik |
| Biaya egress cloud | Tinggi | Rendah | Signifikan di traffic besar |
Teknik Memangkas Ukuran Image #
# ANTI-PATTERN: banyak layer, tidak dibersihkan
FROM node:20
RUN apt-get update
RUN apt-get install -y git
RUN apt-get install -y curl
RUN npm install
COPY . .
CMD ["node", "server.js"]
# BENAR: gabungkan RUN, bersihkan cache
FROM node:20-alpine
RUN apk add --no-cache git curl
COPY package*.json ./
RUN npm ci --only=production
COPY . .
USER node
CMD ["node", "server.js"]
Tujuh teknik utama:
- Pilih base image kecil —
alpine,slim,distroless, atauscratchsesuai kebutuhan. - Gabungkan RUN commands — setiap
RUNadalah satu layer, dan layer kosong (untuk cache) tetap menambah ukuran image. - Bersihkan cache package manager —
apt-get clean && rm -rf /var/lib/apt/lists/*atauapk --no-cache. - Gunakan .dockerignore — exclude
node_modules,.git,tests,docs,*.mddari build context. - Multi-stage build — pisahkan build environment dari runtime.
- Hapus dependency development — install hanya
productiondeps (npm ci --only=production,pip install --no-dev). - Strip binary —
go build -ldflags="-s -w"untuk Go,--strip-alluntuk C/C++.
Tool untuk audit: Dive adalah tool interaktif yang menunjukkan layer mana yang memakan ruang dan artifact apa yang bisa dihapus. docker history myimage juga berguna untuk melihat ukuran per instruksi Dockerfile.
Image Lifecycle — Build, Tag, Push, Pull, Run #
Image punya lifecycle yang konsisten di seluruh ekosistem Docker. Memahami lifecycle ini akan membantumu merancang pipeline CI/CD yang tepat.
flowchart TB
DEV[Developer tulis Dockerfile] --> BUILD[docker build -t myapp:1.0 .]
BUILD --> LOCAL_TEST[Test lokal: docker run]
LOCAL_TEST --> TAG[docker tag myapp:1.0 registry.example.com/myapp:1.0]
TAG --> PUSH[docker push registry.example.com/myapp:1.0]
PUSH --> CI[CI/CD menarik image]
PUSH --> STAGING[Deploy ke staging]
STAGING --> TESTS[Integration test]
TESTS --> PROD[Deploy ke production]
CI --> SCAN[Image scanning]
SCAN --> REPORT[Laporan vulnerability]
REPORT --> GATE{Lolos scan?}
GATE -- Ya --> DEPLOY[Deploy ke cluster]
GATE -- Tidak --> FAIL[Build ditolak]
Operasi Inti dan Peran Masing-Masing #
| Operasi | Perintah | Siapa yang menjalankan |
|---|---|---|
| Build | docker build |
Developer (lokal) atau CI |
| Tag | docker tag |
CI |
| Push | docker push |
CI setelah semua test pass |
| Pull | docker pull |
Deployment target, K8s node |
| Run | docker run |
Local (dev) atau runtime (production via Compose/K8s) |
| Inspect | docker inspect |
Debugging |
| Save/Load | docker save / docker load |
Transfer image antar host tanpa registry |
| Export/Import | docker export / docker import |
Backup filesystem container (bukan image) |
Image Scanning — Gate Wajib di Production Pipeline #
flowchart LR
A[Image di-push] --> B[Trivy scan]
B --> C{Critical CVE?}
C -- Ya --> D[Block deploy]
C -- Tidak --> E[Deploy ke staging]
E --> F[Snyk deep scan]
F --> G[Deploy ke production]
Image scanning menganalisis isi image untuk mencari CVE (Common Vulnerabilities and Exposures) pada package yang terinstal. Tool populer:
- Trivy — open source, cepat, mendukung image, filesystem, dan config scan.
- Snyk — komersial, integrasi GitHub, database CVE yang luas.
- Docker Scout — bawaan Docker Desktop, integrasi dengan Docker Hub.
- AWS ECR Image Scanning — otomatis saat push, terintegrasi dengan Security Hub.
- GCP Container Scanning — otomatis di Artifact Registry.
Trik penting: scan image di setiap layer, bukan hanya image final. Kadang vulnerability ada di base image (yang sudah di-patch di versi baru), dan solusinya cukup bump base image. Tool seperti Trivy otomatis mengecek ini dan menyarankan base image yang lebih aman.
Decision Tree — Memilih Strategi Image #
Tidak ada satu strategi image yang cocok untuk semua situasi. Gunakan decision tree ini sebagai panduan awal.
flowchart TD
A{Bahasa pemrograman<br/>aplikasi?}
A -- Go, Rust, C --> B{Binary butuh<br/>libc dinamis?}
B -- Tidak --> C[scratch atau distroless/static]
B -- Ya --> D[distroless/base]
A -- Java --> E[JRE-only base image<br/>eclipse-temurin:17-jre-alpine]
A -- Node.js --> F[Multi-stage:<br/>build di node:20<br/>runtime di node:20-alpine]
A -- Python --> G[Multi-stage:<br/>build di python:3.12<br/>runtime di python:3.12-slim]
A -- Static site --> H[Build di node:20<br/>serve via nginx:alpine]
C --> I{Tag<br/>strategy?}
D --> I
E --> I
F --> I
G --> I
H --> I
I -- CI/CD --> J[Tag = semantic version<br/>+ immutable digest]
I -- Internal tool --> K[Tag = commit SHA<br/>+ build number]
I -- Production --> L[Tag = semantic version<br/>+ immutable repository]
Ringkasan #
- Image adalah blueprint immutable yang tersusun dari layer-layer filesystem. Container adalah instance runtime-nya, dengan writable layer di atas image. Image tidak pernah berubah setelah di-build; container bisa berubah dan akan hilang saat dihapus.
- Layered architecture adalah pondasi optimasi Docker. Setiap instruksi Dockerfile adalah satu layer. Urutan instruksi menentukan cache hit — letakkan yang jarang berubah di atas, yang sering berubah di bawah.
- Multi-stage build bisa memangkas ukuran image hingga 90% dengan memisahkan build environment (dengan compiler) dari runtime environment (binary akhir). Sangat efektif untuk compiled language (Go, Rust, Java).
- Tag adalah mutable — jangan pakai
latestdi production. Gunakan semantic version (v1.4.2), commit SHA, atau digest SHA256 untuk reproducibility. Aktifkan immutability tag di registry untuk keamanan.- Ukuran image berdampak langsung ke pull time, deploy time, scaling time, dan biaya cloud. Targetkan image sekecil mungkin untuk kebutuhan aplikasi, dengan base image yang sesuai (alpine, slim, distroless, atau scratch).
- Image scanning wajib di pipeline CI/CD. Trivy, Snyk, dan Docker Scout mendeteksi CVE di package yang terinstal. Gate deployment pada hasil scan.
- Image lifecycle: build (lokal/CI) → tag (CI) → push (CI) → pull (deployment target) → run (production). Setiap tahap punya tool dan automasi sendiri.
- Base image bukan cuma soal ukuran — pilih yang paling minimal untuk kebutuhan aplikasimu.
scratchpaling kecil tapi tidak punya apa-apa;distrolessminimal tapi masih punya CA cert dan timezone data;alpinelengkap tapi ringan.