Menghadapi masalah kontainer stateless: kenapa data hilang saat kontainer di-recreate, memahami tiga jenis storage mount (bind mounts, named volumes, dan tmpfs), mengelola volume lewat CLI, pola backup-restore dengan tar, dan praktik penyimpanan data persisten untuk database produksi.

Setelah di episode 7 sebelumnya kita mengoptimalkan image dengan multi-stage builds — memangkas ukuran image dari 312 MB menjadi 14.8 MB dan memilih base image runtime yang tepat (Alpine, Distroless, atau scratch) — pada episode kali ini kita menghadapi masalah yang sama sekali berbeda sifatnya: data. Sejauh ini kita memperlakukan kontainer sebagai kotak hitam yang bisa dihapus dan dibuat ulang sesuka hati. Di episode 4 kita bahkan membahas docker rm -f dan docker system prune dengan santai. Tapi ada pertanyaan yang belum terjawab: apa yang terjadi pada data di dalam kontainer ketika kontainer itu dihapus?
Jawabannya singkat dan menyakitkan: semua hilang. Data yang ditulis aplikasi ke dalam writable layer kontainer bersifat ephemeral — ia melekat pada umur kontainer itu sendiri. Bayangkan menulis laporan penting di laptop yang harus dimatikan total setiap malam, dan semua file-nya hangus setiap kali laptop dimatikan. Tidak masuk akal, bukan? Sayangnya inilah perilaku default kontainer, dan banyak pemula baru tersadar setelah database PostgreSQL mereka lenyap begitu saja karena satu perintah docker compose down yang salah.
Masalah ini bukan hanya soal "kehilangan data". Di dunia kerja, ini menentukan arsitektur penyimpanan: di mana database menyimpan datanya, bagaimana konfigurasi aplikasi dibagikan ke kontainer, bagaimana data sensitif ditangani, dan bagaimana tim membackup semuanya. Di episode ini kita akan membedah tiga jenis storage mount — bind mounts, named volumes, dan tmpfs — kapan memakai yang mana, cara mengelola volume, pola backup-restore, dan contoh nyata Postgres persisten.
Untuk memahami mengapa data hilang, kita kembali ke anatomi image di episode 5. Image adalah tumpukan layer read-only. Saat kontainer dibuat dari image, Docker menambahkan satu lapisan tipis di paling atas: container layer (writable layer). Semua yang aplikasi tulis — file upload, log, isi database, cache — ditulis ke lapisan tipis ini.
+------------------------------+
| Container Layer (writable) | ← data yang ditulis aplikasi
+------------------------------+
| Layer 4 (read-only) | ┐
| Layer 3 (read-only) | │ image
| Layer 2 (read-only) | │
| Layer 1 (read-only) | ┘
+------------------------------+Masalahnya: lapisan writable ini melekat pada siklus hidup kontainer. Ketika kalian menjalankan docker rm (atau docker compose down tanpa flag -v dengan data), lapisan itu beserta seluruh isinya dihapus. Bahkan docker run --rm yang kita pakai di episode 3 untuk kontainer sementara menghapus semuanya saat kontainer berhenti. Data yang ditulis tadi lenyap — bukan karena bug, tapi karena desain: kontainer dirancang stateless agar bisa di-scaled, diganti, dan dibuang kapan pun.
Solusinya adalah memindahkan data keluar dari container layer ke tempat yang hidup lebih lama dari kontainer: storage mount. Docker menyediakan tiga jenis mount, masing-masing dengan tujuan berbeda. Ketiganya diatur dengan opsi -v atau --mount saat docker run, dan semuanya bisa dikombinasikan dalam satu kontainer.
Bind mount menautkan sebuah direktori/file di mesin host langsung ke path di dalam kontainer. Tidak ada perantara — apa yang ada di /home/devnull/project di host, akan terlihat di /app di dalam kontainer, dan perubahan di salah satu sisi langsung terlihat di sisi lain.
docker run -d -p 3000:3000 -v /home/devnull/project:/app my-app:1.0
docker run -d -p 3000:3000 --mount type=bind,source=/home/devnull/project,target=/app my-app:1.0Kegunaan utama bind mount ada dua. Pertama, development mode: kalian bisa mengedit kode di editor favorit di host, dan perubahan itu langsung terlihat di kontainer tanpa perlu rebuild image — alat seperti nodemon atau vite akan melakukan hot reload. Inilah workflow development modern yang akan kita dalami di episode 21. Kedua, file konfigurasi: meletakkan nginx.conf, sertifikat TLS, atau config file ke kontainer tanpa membakarnya ke image — misalnya -v /etc/hosts:/etc/hosts:ro. Flag :ro membuat mount read-only dari sisi kontainer, melindungi file konfigurasi host dari penulisan aplikasi.
Bind mount juga satu-satunya mount yang memakai path host yang benar-benar kalian tentukan — tidak dikelola Docker. Konsekuensinya: kalian harus tahu persis path-nya, dan ia tidak portabel antar host (path /home/devnull berbeda di server CI). Karena itu, bind mount lebih cocok untuk development dan konfigurasi lokal daripada untuk data produksi yang kritis.
Named volume adalah penyimpanan yang dikelola sepenuhnya oleh Docker. Kalian memberi nama — db-data, misalnya — dan Docker menyimpan datanya di lokasi khusus di host: /var/lib/docker/volumes/db-data/_data. Kalian tidak perlu tahu path fisiknya; cukup rujuk namanya.
docker volume create db-data
docker run -d --mount type=volume,src=db-data,dst=/var/lib/postgresql/data postgres:16-alpineMengapa named volume ideal untuk data produksi (terutama database)? Karena ia terpisah dari siklus hidup kontainer: hapus kontainer, volume tetap ada; buat kontainer baru dengan volume yang sama, datanya utuh. Volume juga bisa dibagikan antar kontainer, dan — yang paling penting — bisa di-backup, di-restore, dan dipindahkan antar host dengan mudah. Saat kalian menjalankan docker compose down, volume tidak terhapus kecuali dengan -v; saat docker system prune --volumes dijalankan pun hanya volume unused yang dibersihkan.
Perbedaan mendasar dengan bind mount bisa diringkas:
| Aspek | Bind Mount | Named Volume |
|---|---|---|
| Dikelola oleh | Kalian (path host eksplisit) | Docker (/var/lib/docker/volumes/) |
| Portabilitas | Rendah (terikat path host) | Tinggi (cukup nama) |
| Backup/restore | Lewat tool host langsung | Pola kontainer + tar |
| Cocok untuk | Development, konfigurasi | Database, data produksi |
tmpfs mount menyimpan data di memory (RAM) host, bukan di disk. Data ditulis ke memori dan hilang permanen saat kontainer berhenti — ini disengaja: tujuan tmpfs justru menyimpan data sementara yang tidak boleh selamat dari reboot.
docker run -d --mount type=tmpfs,dst=/tmp --tmpfs /tmp:size=256m my-app:1.0Kegunaan tmpfs: (1) data sensitif — file yang isinya secret/kunci ditulis ke RAM, tidak pernah menyentuh disk (dan tidak ikut terbawa jika image di-extract); (2) data sementara berperforma tinggi — cache, session, temp files yang tidak perlu persisten dan butuh kecepatan RAM. Contoh nyata: menaruh folder /tmp dan /run sebagai tmpfs sehingga aplikasi tidak menulis sampah ke container layer.
Peringatan penting: tmpfs mengonsumsi RAM. Dataset besar (misalnya index database) akan menghabiskan memory dan bisa membuat host out of memory. Gunakan tmpfs hanya untuk data kecil yang memang harus cepat dan sementara. Untuk cache besar yang boleh hilang tapi tidak boleh menghabiskan RAM, pertimbangkan volume biasa.
Docker menyediakan perintah-perintah khusus untuk mengelola volume:
docker volume create db-data
docker volume ls
docker volume inspect db-data
docker volume rm db-data
docker volume pruneDRIVER VOLUME NAME
local db-data
local e9a3c1d8f2b4docker volume inspect db-data menampilkan metadata detail dalam JSON — termasuk lokasi fisik (Mountpoint) dan opsi mount. Perhatikan bahwa docker volume create itu opsional: jika kalian docker run -v db-data:/path tanpa membuatnya lebih dulu, Docker otomatis membuat volume bernama db-data. Namun membuatnya eksplisit lebih jelas dan memungkinkan konfigurasi driver khusus. docker volume prune menghapus semua volume yang tidak sedang dipakai kontainer mana pun — hati-hati: di mesin produksi, volume "unused" bisa jadi backup yang sengaja dibiarkan.
Ada dua cara menulis mount di Docker: sintaks singkat -v/--volume (kompatibel dari Docker lama) dan sintaks panjang --mount (lebih eksplisit, direkomendasikan untuk hal-hal yang bukan volume sederhana).
# Bind mount
-v /host/path:/container/path
--mount type=bind,source=/host/path,target=/container/path
# Named volume
-v my-vol:/app/data
--mount type=volume,src=my-vol,dst=/app/data
# tmpfs
--tmpfs /tmp:size=256m
--mount type=tmpfs,dst=/tmp,tmpfs-size=256mSintaks -v sangat ringkas dan masih paling banyak dipakai di tutorial lama, tapi ia punya kelemahan: untuk bind mount, source path yang tidak diawali / bisa disalahartikan sebagai named volume (misteri klasik "kenapa data saya masuk ke volume random?"). Sintaks --mount memaksa kalian menyebutkan type= secara eksplisit, sehingga lebih jelas dan tidak ambigu. Untuk kode yang masuk ke dalam file Compose (episode 10), kalian akan memakai bentuk key-value yang mirip --mount. Rekomendasi modern: gunakan --mount untuk opsi yang kompleks (read-only, tmpfs size, volume driver), dan -v hanya untuk bind mount/named volume sederhana yang sudah terbukti.
Tip
Tiga flag :ro (read-only), :rw (read-write, default), dan :z/:Z (konteks SELinux) sering mengecoh. -v /host/path:/container/path:ro membuat kontainer tidak bisa menulis ke mount tersebut — pola yang sangat disarankan untuk file konfigurasi dan image code yang tidak boleh dimodifikasi kontainer.
Inilah pola yang membedakan engineer yang "mengerti volume" dari yang sekadar "memakai volume": kemampuan membackup dan merestorasi. Karena named volume dikelola Docker, cara termudah untuk backup adalah memakai kontainer sementara yang me-mount volume tersebut beserta folder backup, lalu mengarsipkannya dengan tar. Perhatikan pola berikut:
docker run --rm \
-v db-data:/data \
-v /home/devnull/backups:/backup \
alpine tar czf /backup/db-data-2026-08-02.tar.gz -C /data .Mari bedah: --rm menjamin kontainer sementara dihapus setelah selesai. Mount pertama (-v db-data:/data) memasang volume yang ingin di-backup ke /data. Mount kedua (-v /home/devnull/backups:/backup) memasang folder host tempat hasil arsip disimpan. Perintah alpine tar czf /backup/db-data-2026-08-02.tar.gz -C /data . mengompres seluruh isi /data menjadi satu file .tar.gz. Hasilnya: arsip lengkap volume ada di host, siap diunduh atau dipindahkan.
Restore adalah kebalikannya — ekstrak arsip ke dalam volume:
docker run --rm \
-v db-data:/data \
-v /home/devnull/backups:/backup \
alpine sh -c "tar xzf /backup/db-data-2026-08-02.tar.gz -C /data"Untuk arsip yang dibuat dari direktori (-C /data .), pastikan restore memakai -C /data juga agar struktur direktori tetap utuh. Pola kontainer-sementara-ini sangat fleksibel: kalian bisa mengganti alpine dengan image yang punya tool lain, dan bisa dijadwalkan via cron di host. Untuk produksi serius, kombinasikan dengan dump logis (misalnya pg_dump untuk Postgres) — arsip file mentah bagus untuk disaster recovery, dump logis bagus untuk migrasi antar versi.
Mari terapkan semuanya pada skenario paling umum di produksi: database PostgreSQL. Perhatikan bahwa image postgres mendokumentasikan lokasi datanya: /var/lib/postgresql/data. Jika kita menaruh named volume di path itu, data database hidup di volume — bukan di container layer.
docker volume create pgdata
docker run -d --name postgres-db \
-e POSTGRES_USER=app -e POSTGRES_PASSWORD=rahasia -e POSTGRES_DB=myapp \
-p 5432:5432 \
-v pgdata:/var/lib/postgresql/data \
postgres:16-alpineSekarang mari kita buktikan persistensinya: buat data, hancurkan kontainer, buat ulang — data harus tetap ada.
docker exec -it postgres-db psql -U app -d myapp -c "CREATE TABLE notes (id serial PRIMARY KEY, title text);"
docker rm -f postgres-db
docker run -d --name postgres-db \
-e POSTGRES_USER=app -e POSTGRES_PASSWORD=rahasia -e POSTGRES_DB=myapp \
-p 5432:5432 \
-v pgdata:/var/lib/postgresql/data \
postgres:16-alpine
docker exec -it postgres-db psql -U app -d myapp -c "\dt" List of relations
Schema | Name | Type | Owner
--------+-------+-------+-------
public | notes | table | appKontainer pertama dihapus dengan docker rm -f — dan tabel notes yang dibuat tetap ada, karena datanya tersimpan di volume pgdata, bukan di kontainer. Ini persis perilaku yang diharapkan dari database produksi: kontainer bisa mati, di-update, atau dipindah ke host lain (dengan volume yang di-copy), data tetap utuh.
Masalah permission bind mount. Kontainer berjalan sebagai user tertentu (ingat USER node di episode 6), tapi folder host yang di-bind mount dimiliki oleh user host (misalnya UID 1000). Kontainer non-root sering gagal menulis ke folder tersebut — ini error klasik "Permission denied" di development. Solusinya: samakan UID/GID user kontainer dengan user host, atau set permission folder host dengan benar. Inilah salah satu alasan bind mount tidak ideal untuk data produksi yang butuh kontrol permission ketat.
Memakai bind mount/volume untuk code di produksi. Di development, bind mount untuk source code itu bagus (hot reload). Di produksi, jangan. Kode harus tertanam di image (via multi-stage build episode 7), bukan di-mount dari host — kalau tidak, kode tidak versioned, tidak reproducible, dan host menjadi titik kegagalan tunggal. Image adalah satu-satunya artefak yang bisa di-deploy.
tmpfs untuk data besar. Menaruh dataset besar di tmpfs akan menggerus RAM host dan bisa memicu OOM killer. tmpfs hanya untuk data kecil yang sementara dan sensitif.
Mengabaikan backup. Volume menyelamatkan data dari siklus hidup kontainer, tetapi volume tidak menyelamatkan dari disk rusak, host hilang, atau docker volume prune yang salah ketik. Volume itu sendiri bisa hilang bersama host-nya. Backup rutin (pola tar atau dump) adalah keharusan, bukan opsional.
Important
Ada perbedaan besar antara docker compose down (menghentikan kontainer dan network, volume tetap ada) dan docker compose down -v (menghapus volume juga). Di mesin development yang penuh data uji, -v terdengar tidak berbahaya — sampai seseorang menjalankannya di lingkungan yang menyerupai produksi dan kehilangan semua data. Selalu pikir dua kali sebelum menambahkan -v.
Pada episode 8 ini kalian telah memahami akar masalah kontainer stateless — writable layer yang melekat pada umur kontainer — dan tiga solusi storage mount Docker: bind mounts (folder host langsung, untuk development dan konfigurasi), named volumes (dikelola Docker di /var/lib/docker/volumes/, untuk database dan produksi), dan tmpfs mounts (data sementara di RAM). Kalian juga belajar mengelola volume dengan docker volume create/ls/inspect/rm/prune, membandingkan sintaks -v vs --mount, mempraktikkan pola backup-restore dengan kontainer sementara + tar, dan membuktikan persistensi PostgreSQL dengan named volume yang selamat dari docker rm -f.
Inti yang harus kalian bawa pulang: data kritis tidak boleh hidup di dalam kontainer — ia harus hidup di volume, dan volume itu sendiri harus di-backup. Kontainer adalah warga negara yang bisa diganti kapan pun; volume adalah tanah tempat data kalian berakar.
Mulai episode 9, satu pertanyaan lagi yang belum terjawab: bagaimana kontainer saling berbicara? Selama ini kita menghubungkan aplikasi ke port host secara manual (-p 3000:3000). Di episode 9 selanjutnya kita akan menyelami container networking — driver bridge, host, none, macvlan, dan overlay; mengapa default bridge tidak punya DNS internal; serta bagaimana membuat aplikasi dan database berbicara lewat nama kontainer di custom network. Sampai jumpa di episode 9!