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:

  • FROM membuat layer dari image yang sudah ada (biasanya base image). Layer ini sering kali berukuran besar (ubuntu:22.04 sekitar 77 MB).
  • RUN, COPY, ADD membuat layer baru dari hasil eksekusi perintah.
  • CMD, ENTRYPOINT, ENV, EXPOSE, LABEL adalah 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.mediaType dan digest — 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:

  1. Stage 1 menggunakan image besar (Go SDK 300+ MB) untuk meng-compile kode.
  2. Stage 2 dimulai dari image minimal (distroless, 20 MB).
  3. COPY --from=builder menyalin hanya binary akhir dari stage 1 ke stage 2.
  4. 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. scratch misalnya tidak punya apa-apa — tidak ada shell, tidak ada libc, tidak ada sertifikat CA. Jika aplikasimu butuh getaddrinfo() (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: IMMUTABLE per 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:

  1. Pilih base image kecilalpine, slim, distroless, atau scratch sesuai kebutuhan.
  2. Gabungkan RUN commands — setiap RUN adalah satu layer, dan layer kosong (untuk cache) tetap menambah ukuran image.
  3. Bersihkan cache package managerapt-get clean && rm -rf /var/lib/apt/lists/* atau apk --no-cache.
  4. Gunakan .dockerignore — exclude node_modules, .git, tests, docs, *.md dari build context.
  5. Multi-stage build — pisahkan build environment dari runtime.
  6. Hapus dependency development — install hanya production deps (npm ci --only=production, pip install --no-dev).
  7. Strip binarygo build -ldflags="-s -w" untuk Go, --strip-all untuk 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 latest di 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. scratch paling kecil tapi tidak punya apa-apa; distroless minimal tapi masih punya CA cert dan timezone data; alpine lengkap tapi ringan.

← Sebelumnya: Engine & Daemon   Berikutnya: Container →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact