Belajar Docker - Image Internals: Layer, Union File System & Image Storage
Episode 23 of 28

Belajar Docker - Image Internals: Layer, Union File System & Image Storage

Membongkar isi image hingga ke lapisan paling bawah: image sebagai tumpukan layer read-only di atas Union File System, bagaimana Dockerfile menjadi layer, storage driver overlay2 dengan Copy-on-Write, serta perintah lanjutan save/load, export/import, dan mengapa docker commit adalah anti-pattern.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Pendahuluan

Setelah di episode 22 sebelumnya kita merakit arsitektur production-grade yang utuh, kali ini kita menarik lapisan paling bawah dari semuanya. Selama 22 episode, kalian membangun image dengan Dockerfile, menulis RUN, COPY, dan CMD, lalu memanggil docker build dan docker push seolah semuanya magis. Tetapi pernahkah kalian bertanya: apa sebenarnya isi sebuah image? Jawaban singkatnya — tumpukan layer read-only di atas sebuah Union File System — ternyata menjelaskan hampir semua perilaku Docker yang tampak aneh.

Kenapa mengubah urutan perintah di Dockerfile mengubah kecepatan build secara drastis? Kenapa menulis file ke dalam kontainer "memperbesar" kontainer itu tapi tidak mengubah image? Kenapa menyimpan database di volume lebih baik daripada di layer tulis kontainer? Kenapa image yang diturunkan dari base yang sama tidak menggandakan ukuran di host? Semua pertanyaan ini punya satu jawaban bersama: struktur layer. Episode ini membedah jawaban itu, menyentuh storage driver overlay2, dan menutup dengan perintah-perintah lanjutan untuk memindahkan image antar host — beserta satu anti-pattern klasik yang wajib kalian tahu.

Pembahasan Utama

Anatomi Image: Tumpukan Layer Read-Only

Sebuah image bukanlah satu blok file yang utuh. Image adalah tumpukan layer, dan setiap layer adalah satu snapshot dari filesystem yang merepresentasikan hasil dari satu instruksi Dockerfile. Yang membuat ini berfungsi adalah Union File System: kemampuan kernel Linux untuk menggabungkan beberapa direktori (layer) menjadi satu pohon direktori tampilan, seolah-olah menyatukan kertas kalkir yang saling bertumpuk.

Anatomi image berlapis
+--------------------------------------+
| Layer 4: CMD ["node", "server.js"]   |  <- layer teratas, paling sering berubah
+--------------------------------------+
| Layer 3: COPY . /app                 |
+--------------------------------------+
| Layer 2: RUN npm ci --omit=dev       |
+--------------------------------------+
| Layer 1: ENV NODE_ENV=production     |
+--------------------------------------+
| Layer 0: FROM node:22-alpine         |  <- layer dasar (base image)
+--------------------------------------+

Yang terlihat oleh aplikasi di dalam kontainer adalah pandangan gabungan dari semua layer — ia tidak tahu bahwa file server.js sebenarnya berasal dari layer 3, atau bahwa node binary berasal dari layer 0. Bagi proses di dalam kontainer, itu satu filesystem yang utuh. Tapi bagi Docker, setiap layer tetap entitas terpisah yang bisa dibagikan dan di-cache.

Ini menghasilkan dua fakta yang menentukan. Pertama, layer bersifat immutable: begitu sebuah layer dibuat, isinya tidak pernah berubah; memperbarui sebuah file berarti membuat layer baru di atasnya. Kedua, layer dibagikan antar image: jika dua image sama-sama menggunakan node:22-alpine sebagai base, kedua image itu memakai layer yang sama persis di disk — itulah mengapa seratus image dari base yang sama tidak membuat disk meledak.

Bagaimana Dockerfile Menjadi Layer

Setiap instruksi yang mengubah filesystem menghasilkan satu layer baru: RUN, COPY, ADD, dan ENV/WORKDIR (yang mencatat metadata). Instruksi non-filesystem seperti CMD, ENTRYPOINT, dan EXPOSE hanya menulis metadata dan tidak menambah ukuran disk — perhatikan di output history nanti, ukurannya 0B. Urutan instruksi menentukan jumlah perubahan antar layer, yang menjadi kunci strategi caching yang kita pelajari di episode 6: layer yang tidak berubah di-reuse, layer yang berubah — dan semua layer di atasnya — dibangun ulang.

Dockerfile yang menghasilkan 4 layer
FROM node:22-alpine          # layer A (base)
WORKDIR /app                 # layer B (metadata: working dir)
COPY package*.json ./        # layer C (file dependency)
RUN npm ci --omit=dev        # layer D (node_modules)
COPY . .                     # layer E (source code)
CMD ["node", "server.js"]    # metadata, tanpa layer baru

Inilah mengapa urutan COPY package*.json sebelum npm ci sebelum COPY . . adalah pola emas: ketika kalian hanya mengubah kode sumber, layer A–D tetap valid dari cache, dan hanya layer E yang dibangun ulang. Sebaliknya, jika COPY . . ditulis sebelum npm ci, perubahan satu baris kode memaksa npm ci (proses paling lambat) ikut jalan ulang setiap saat.

Mengintip Layer: docker image history & docker inspect

Cara paling langsung melihat struktur layer adalah docker image history. Setiap barisnya adalah satu layer, diurutkan dari yang paling baru:

Melihat layer image
docker image history my-app:1.0
docker image history --no-trunc my-app:1.0
Contoh output (disederhanakan)
IMAGE          CREATED       CREATED BY                                    SIZE
6f2e9b0d4c1a   5 min ago     CMD ["node" "server.js"]                      0B
5c1a8f2e6d3b   5 min ago     COPY . /app                                   14.2MB
9b7e4d2a8c1f   6 min ago     RUN /bin/sh -c npm ci --omit=dev              92MB
3f6a1e9c7b4d   6 min ago     COPY package*.json ./                         2.1MB
d0b4a8c2e6f1   8 min ago     WORKDIR /app                                  0B
<missing>       2 weeks ago   /bin/sh -c #(nop) ENV NODE_ENV=production     0B

Perhatikan dua hal. Pertama, ukuran 0B pada CMD — instruksi metadata memang tidak memakan ruang. Kedua, pada layer base muncul <missing> di kolom IMAGE: layer tersebut tidak ada di image ini secara lokal (base image ditarik terpisah dan berbagi layer dengan image lain), tetapi hash-nya tetap tercatat. Baris terakhir inilah yang membuktikan bahwa image kalian hanyalah "lapisan tipis" di atas node:22-alpine.

Untuk melihat hash layer secara eksplisit, docker inspect mengungkap array RootFS.Layers — daftar digest SHA-256 yang membentuk image:

Digest layer lewat inspect
docker inspect --format '{{json .RootFS}}' my-app:1.0
Contoh output
{"Type":"layers","Layers":[
  "sha256:1f4e6f2a...",
  "sha256:2b8a9c3d...",
  "sha256:3c5d7e9f..."
]}

Setiap digest ini unik dan konten-deterministik: image dengan kumpulan layer yang sama pasti memiliki RootFS yang sama, terlepas dari dari mana ia ditarik. Inilah dasar dari immutable digest yang kita bahas di episode 12.

Storage Driver: overlay2 & Copy-on-Write

Sekarang kita masuk ke bagian yang benar-benar "mesin" di balik image. Docker Linux modern menggunakan overlay2 sebagai storage driver default. overlay2 bekerja di /var/lib/docker/overlay2/ dengan tiga direktori penting per layer:

  • lowerdir — tumpukan layer read-only dari image (semua kecuali yang teratas).
  • upperdir — layer tulis kontainer; tempat semua perubahan yang dilakukan proses di dalam kontainer.
  • merged — hasil penggabungan lowerdir dan upperdir, yang terlihat oleh proses sebagai satu filesystem.
Struktur overlay2 di host
ls -la /var/lib/docker/overlay2/<layer-hash>/

Konsep yang menyatukan semuanya adalah Copy-on-Write (CoW). Ketika sebuah proses di dalam kontainer menulis file:

  1. Jika file itu berasal dari layer bawah (lowerdir) dan belum pernah diubah, overlay2 menyalin file tersebut ke upperdir, lalu menulis perubahannya di sana. Layer asli tetap utuh.
  2. Semua perubahan berikutnya ke file yang sama langsung ditulis ke upperdir, tanpa menyalin ulang.
  3. Jika kontainer dihapus, upperdir dibuang; lowerdir (image) tetap utuh dan dipakai bersama kontainer lain.

Inilah mengapa dua kontainer dari image yang sama bisa "menulis" secara independen tanpa saling merusak, dan mengapa menulis ke layer kontainer tidak pernah mengubah image. Namun CoW punya sisi gelap yang harus dipahami: write amplification. Saat sebuah file di lowerdir diubah, seluruh bloknya disalin ke atas terlebih dahulu. Bayangkan database yang menulis ribuan kali per detik ke file data berukuran gigabyte di layer tulis — setiap tulis kecil bisa memicu penyalinan blok yang besar, dan upperdir membengkak tanpa batas karena tidak pernah di-squash. Inilah jawaban definitif atas pertanyaan episode 8: database wajib memakai volume, bukan layer tulis — volume dilewati overlay2 sepenuhnya dan ditulis langsung ke disk host.

Tip

Cek storage driver kalian dengan docker info | grep -i "storage driver". Jika outputnya fuse-overlayfs, itu menandakan kalian menjalankan rootless Docker — di mana mount overlay2 butuh hak istimewa kernel sehingga digantikan FUSE. Konsepnya tetap sama (lowerdir/upperdir/merged), hanya lapisan implementasinya yang berbeda.

overlay2 vs fuse-overlayfs vs Volume

Aspekoverlay2 (rootful)fuse-overlayfs (rootless)Volume
Akses fileNative kernelUser-space FUSENative kernel
Performa tulisBaik (CoW)Lebih lambat (overhead FUSE)Terbaik (tanpa CoW)
PersistensiSelama container adaSelama container adaIndependen dari container
Cocok untukImage & runtimeImage & runtime rootlessData besar & database
Berbagi dataAntar container sehostAntar container sehostAntar container, bisa antar host (driver)

Kesimpulan praktisnya: gunakan overlay2 (default) untuk image dan runtime, terima pengorbanan rootless bila kebutuhan keamanan menuntut, dan selalu letakkan data besar yang sering ditulis ke volume. Menulis data transaksional ke layer tulis bukan hanya masalah performa, tetapi juga masalah durabilitas: layer tulis hilang bersama kontainer.

Memindahkan Image Tanpa Registry: save & load

Kadang kalian perlu memindahkan image antar host tanpa registry — misalnya server production yang terisolasi dari internet, atau transfer manual saat jaringan terputus. docker save mengemas seluruh layer image menjadi satu arsip tar (dengan semua metadata dan history), dan docker load membongkarnya kembali:

Save & load image (offline transfer)
# Kemas image menjadi satu file tar
docker save -o my-app-1.0.tar ghcr.io/arman/my-app:1.0.0
 
# Salin ke host tujuan (tanpa registry)
scp my-app-1.0.tar user@server:/tmp/
 
# Bongkar di host tujuan
docker load -i my-app-1.0.tar
 
# Verifikasi — image sekarang ada, dengan semua layer yang sama
docker images ghcr.io/arman/my-app

Karena formatnya arsip tar, kalian juga bisa mengompresnya untuk mempercepat transfer: docker save my-app:1.0 | gzip > my-app.tar.gz. Yang membedakan save/load dari sekadar menyalin file: arsipnya memuat struktur layer utuh, sehingga image yang dimuat di host tujuan identik dengan sumbernya — termasuk history dan digest. Ini jalur yang aman dan dapat diaudit untuk transfer image offline.

export/import: File System, Bukan Image

Di sebelah save/load ada pasangan docker export dan docker import — dan keduanya sering tertukar. docker export mengekspor filesystem dari sebuah container (termasuk layer tulisnya!) menjadi satu tarball, tanpa metadata image. docker import membongkarnya kembali menjadi image baru yang hanya terdiri dari satu layer, tanpa history:

Export/import filesystem (bukan image)
# Ekspor filesystem container (termasuk data yang diubah runtime)
docker export my-container > rootfs.tar
 
# Import sebagai image satu-layer (history & metadata image hilang)
docker import rootfs.tar my-app:imported

Contoh yang paling gamblang membedakannya: docker save ubuntu > ubuntu.tar menghasilkan arsip yang bisa di-load kembali menjadi image ubuntu dengan history utuh. Sedangkan docker export dari kontainer ubuntu menghasilkan tarball filesystem mentah — hasil import-nya adalah image baru tanpa base, tanpa layer history, dan ukuran satu layer gabungan. Kapan export berguna? Untuk memindahkan data dari container (misalnya snapshot filesystem sebelum dibuang), bukan untuk mendistribusikan aplikasi.

docker commit: Kenapa Ini Anti-Pattern

Terakhir, mitos yang paling menggiurkan pemula: docker commit — mengubah sebuah container yang sedang berjalan menjadi image baru. Sekilas ini kelihatan seperti solusi instan: "jalankan perintah manual di dalam kontainer sampai berfungsi, lalu bekukan jadi image".

docker commit (anti-pattern)
docker commit my-container my-app:manual

Mengapa praktik ini berbahaya dan tidak direkomendasikan:

  1. State ephemeral ikut terbekukan. Container yang berjalan sering punya file sementara, cache, log, atau data runtime di layer tulisnya. Semuanya masuk ke image. Hasilnya image yang tidak reproducible: tidak ada yang tahu kenapa dan apa yang ada di dalamnya.
  2. History hilang. Image hasil commit tidak punya jejak Dockerfile. Kalian tidak bisa mengaudit, memperbarui dependency, atau membangun ulang dengan cara yang terkontrol.
  3. Tidak ada kontrol versi. Tidak seperti Dockerfile yang bisa di-review di git, image commit adalah kotak hitam yang hanya tersimpan di satu mesin. Rekan kerja tidak bisa melakukan code review.

Ini persis situasi "laptop yang sudah di-setting manual selama bertahun-tahun": bisa dipakai, tapi tidak bisa ditiru, tidak bisa diperbaiki, dan pasti akan mengkhianati kalian saat mesinnya mati. Dockerfile adalah sumber kebenaran — setiap image produksi harus lahir dari Dockerfile yang ter-versi. docker commit baru dapat diterima untuk forensik sesaat (membekukan state container yang bermasalah untuk dianalisis), bukan untuk membangun artefak distribusi.

Caution

Jangan pernah memakai docker commit untuk menyimpan database atau data aplikasi. Data harus hidup di volume — bukan di layer image. Image yang di-commit dari container dengan data di layer tulis akan membawa data itu sebagai satu layer raksasa, sulit dibackup, sulit di-upgrade, dan rentan kehilangan konsistensi.

Pitfalls Seputar Layer & Storage

  1. Layer tunggal yang raksasa. Satu RUN yang menyalin build artifact besar (misalnya node_modules ber-gigabyte) menghasilkan satu layer besar yang harus ditransfer dan disimpan utuh setiap kali layer itu berubah. Pisahkan apa yang jarang berubah (dependency) dari yang sering berubah (source) dengan urutan COPY yang benar — pelajaran yang kembali lagi dari episode 6 dan 23.
  2. Terlalu banyak layer. Meski layer-layer kecil itu efisien berkat CoW, ratusan layer menambah overhead saat image dipull/di-load dan saat kontainer dijalankan (setiap layer menambah mount point). Imbangi dengan menggabungkan RUN yang terkait (misalnya apt-get update + install + clean) — dalam batas wajar, bukan menjejalkan semuanya ke satu baris.
  3. Menulis file besar ke layer tulis. Ingat write amplification: database, cache besar, dan file log di dalam layer tulis membuat upperdir membengkak, memperlambat tulis, dan hilang saat kontainer di-recreate. Selalu arahkan data ke volume.
  4. Mengabaikan --squash. Beberapa orang menyarankan squash (menggabungkan semua layer jadi satu) untuk memperkecil image. Ini jarang dipakai karena menghancurkan caching dan berbagi layer — trade-off yang biasanya tidak sepadan dengan manfaat ukuran yang tipis. Urutan Dockerfile yang benar tetap solusi utama.

Penutup

Pada episode 23 ini kita telah membedah isi image hingga lapisan terdalam: image adalah tumpukan layer read-only di atas Union File System, tiap instruksi Dockerfile membentuk satu layer, dan docker image history/docker inspect membuktikannya lewat digest. Kita memahami storage driver overlay2 dengan lowerdir/upperdir/merged, mengapa Copy-on-Write membuat image berbagi layer secara efisien sekaligus menjadi sumber write amplification untuk database (karena itu volume wajib untuk data besar), membandingkannya dengan fuse-overlayfs rootless, dan menguasai docker image save/load untuk transfer offline serta docker export/import yang menangani filesystem, bukan image. Terakhir, kita menutup dengan alasan kuat mengapa docker commit adalah anti-pattern yang mengorbankan reproducibility demi kemudahan sesaat.

Inti yang harus kalian bawa:

  • Image = layer read-only bertumpuk; layer immutable dan dibagikan antar image.
  • Urutan Dockerfile menentukan efisiensi cache: dependency dahulu, source terakhir.
  • CoW efisien untuk layer image, tapi berbahaya untuk data besar — pakai volume.
  • docker image save/load = pindah image utuh; export/import = pindah filesystem.
  • docker commit anti-pattern; Dockerfile adalah sumber kebenaran.

Di episode 24 selanjutnya kita berpindah dari isi image ke mesin yang menjalankannya: Docker Daemon Configuration & Host Maintenance — mengatur daemon.json (log driver, data-root, live-restore), membersihkan host dengan docker system prune, memantau disk /var/lib/docker, dan memakai Docker Context untuk mengelola banyak host dari satu CLI. Sampai jumpa di episode 24!