Container dirancang sekali pakai: docker rm menghapus container beserta seluruh tulisannya, dan itu justru fitur — deployment berikutnya mulai dari keadaan bersih. Masalahnya, database yang datanya ikut lenyap bukan fitur melainkan bencana. Chapter ini membedah cara kerja penyimpanan container — layer image yang read-only, writable layer, dan copy-on-write — lalu tiga jalan menyelamatkan data keluar dari container: volume, bind mount, dan tmpfs. Karena docker tidak terpasang di lingkungan latihan, mekanismenya kita simulasikan di filesystem lokal, lengkap dengan fixture JSON dan latihan backup yang sesungguhnya dijalankan.

Layer Image dan Copy-on-Write #

Image adalah tumpukan layer read-only — satu layer per instruksi Dockerfile, dan layer-layer itu dipakai bersama oleh semua container dari image yang sama. Saat container berjalan, satu writable layer tipis ditumpuk di puncaknya. Proses di dalam container melihat gabungan seluruh tumpukan sebagai satu filesystem utuh; ketika proses menulis ke file milik layer bawah, file itu disalin dulu ke writable layer (copy-up), lalu ditulis di sana. Layer image tidak pernah tersentuh. Simulasinya di filesystem biasa:

bash
mkdir -p image/app writable
printf 'versi=1.0\n' > image/app/app.conf
printf 'kunci=r4h451a\n' > image/app/secret.key

echo '== layer image (read-only) =='
cat image/app/app.conf

echo '== proses menulis app.conf: disalin dulu ke writable layer =='
cp image/app/app.conf writable/app.conf
printf 'versi=1.1\n' > writable/app.conf
cat writable/app.conf

echo '== tampilan gabungan yang dilihat proses di dalam container =='
for f in image/app/*; do
  nama=$(basename "$f")
  if [ -f "writable/$nama" ]; then cat "writable/$nama"; else cat "$f"; fi
done

Output yang Dihasilkan:

text
== layer image (read-only) ==
versi=1.0
== proses menulis app.conf: disalin dulu ke writable layer ==
versi=1.1
== tampilan gabungan yang dilihat proses di dalam container ==
versi=1.1
kunci=r4h451a

Mari kita bedah bloknya:

  1. Dua direktori = dua layer: image/app berisi file bawaan image — konfigurasi versi 1.0 dan satu secret key.
  2. cp adalah momen copy-up: file disalin ke writable layer sebelum diubah — persis yang dilakukan kernel saat proses menulis file dari layer bawah.
  3. cat pertama: layer image terbukti tak tersentuh — isinya masih versi=1.0 setelah container menulis versi=1.1.
  4. Loop for f in image/app/*: mensimulasikan tampilan gabungan — file yang ada di writable layer menang, file yang tidak ada jatuh ke layer image.

Konsekuensinya lugas: docker rm membuang writable layer, dan semua perubahan sejak container menyala ikut lenyap — config, log, data. Image tetap bersih dan siap dipakai container baru. Data yang harus bertahan (isi database, unggahan user) wajib pindah keluar dari tumpukan itu.

Tiga Jalan Keluar untuk Data #

Tiga mekanisme mount membawa data keluar dari container, masing-masing dengan wilayah kerjanya:

MekanismeLokasi sebenarnyaDikelolaBertahan setelah container dihapusKasus klasik
volume/var/lib/docker/volumes/...engine containeryadata database
bind mountpath host mana punkita sendiriya (ini file host)konfigurasi, source code
tmpfsmemorikerneltidak — memang begitusecret, state sementara

Sintaksnya satu keluarga: -v data-db:/var/lib/postgresql/data untuk volume, -v /home/budi/conf:/etc/nginx/conf.d:ro untuk bind mount, --tmpfs /run untuk memori. Akhiran :ro memasang baca-saja — container bisa membaca tapi tidak menulis; :rw adalah kebalikannya.

Volume dari Dalam #

Volume adalah wadah milik engine container: dibuat dengan docker volume create data-db, dipasang ke container, dan isinya hidup di /var/lib/docker/volumes/ di host. Satu volume bisa dipakai bergantian oleh banyak container, dan datanya tidak tersentuh siklus hidup container mana pun. Berkas volume.json di bundle materi adalah hasil docker volume inspect pada volume buatan compose:

bash
jq -r '.[0].Name, .[0].Mountpoint' volume.json
echo '== asal-usulnya (label compose) =='
jq -r '.[0].Labels["com.docker.compose.project"]' volume.json

Output yang Dihasilkan:

text
toko_data-db
/var/lib/docker/volumes/toko_data-db/_data
== asal-usulnya (label compose) ==
toko

Tiga field itu bercerita: nama toko_data-db adalah gabungan nama proyek compose (toko) dan nama volume di compose file (data-db) — persis pola prefix yang kita lihat di Chapter Docker Compose ; Mountpoint adalah lokasi sebenarnya di host tempat data menetap; dan label menyatakan volume itu dibuat otomatis oleh compose proyek toko, bukan manual.

Bind Mount: Dua Path Satu Data #

Bind mount menempelkan path host apa adanya ke path di dalam container — tanpa perantara, tanpa salinan. Editor di host menyunting file yang persis sama dengan yang dibaca proses di dalam container; begitulah hot reload bekerja. Efek “dua path menunjuk satu data” bisa dirasakan lokal lewat analogi symlink:

bash
mkdir -p host-data aplikasi
printf 'port=8080\n' > host-data/app.conf

ln -s ../host-data/app.conf aplikasi/app.conf
echo '== dibaca lewat path "container" =='
cat aplikasi/app.conf

echo '== host mengubah isi file =='
printf 'port=9090\n' > host-data/app.conf
cat aplikasi/app.conf

Output yang Dihasilkan:

text
== dibaca lewat path "container" ==
port=8080
== host mengubah isi file ==
port=9090

Bedah singkatnya: ln -s membuat jendela kedua menuju file yang sama, jadi penulisan lewat host-data/app.conf langsung terlihat dari aplikasi/app.conf tanpa menyalin ulang apa pun. Batas analoginya perlu jujur diakui — symlink adalah redirect path yang terlihat lewat readlink, sedangkan bind mount sungguhan adalah mount entry di mount namespace kernel yang tidak kelihatan dari dalam container. Efek praktisnya sama: satu data, dua jendela. Satu perbedaan yang sering menggigit: file bind mount dimiliki oleh UID di host, jadi container yang berjalan sebagai user berbeda bisa saja tidak punya izin menulisnya — topik yang menyambung langsung ke chapter berikutnya.

Backup dan Restore #

Volume berumur panjang, tetapi tetap terikat pada satu mesin. Backup memindahkan isinya ke berkas yang bisa disalin ke mana saja, dan polanya adalah tar: arsipkan isi volume, simpan hasilnya di host. Latihan berikut menjalankan pola itu sungguhan di folder lokal:

bash
mkdir -p data-lama backup
printf 'pelanggan=budi\n' > data-lama/pelanggan.csv
printf 'pesanan=1024\n' > data-lama/pesanan.csv

tar -C data-lama -czf backup/data-db.tar.gz .
echo '== isi arsip =='
tar -tzf backup/data-db.tar.gz | sort

mkdir -p data-baru
tar -C data-baru -xzf backup/data-db.tar.gz
diff -r data-lama data-baru && echo 'restore identik dengan aslinya'

Output yang Dihasilkan:

text
== isi arsip ==
./
./pelanggan.csv
./pesanan.csv
restore identik dengan aslinya

Empat langkahnya:

  1. tar -C data-lama -czf ... .: pindah direktori lebih dulu (-C) supaya path di dalam arsip bersih — ./pelanggan.csv, bukan data-lama/pelanggan.csv; . berarti seluruh isi volume.
  2. tar -tzf: melisting isi arsip terkompresi — inventaris sebelum menyimpannya.
  3. tar -C data-baru -xzf: ekstraksi ke lokasi baru — restore.
  4. diff -r: membandingkan dua pohon direktori; sunyi diikuti pesan sukses berarti restore menghasilkan salinan identik.

Di mesin berdocker, pola yang sama berjalan lewat container sementara yang memasang volume lalu mengarsipkannya: docker run --rm -v toko_data-db:/data -v "$PWD:/backup" debian:12-slim tar -C /data -czf /backup/data-db.tar.gz . — volume dipasang, diarsipkan, container pergi.

Data kini bertahan melewati hidup-mati container. Satu dimensi terakhir sebelum menutup section: container yang selama ini kita jalankan — seberapa aman sebenarnya ia berjalan? Chapter Container Security membedah user non-root, penanganan secret, dan scanning.

Referensi lanjut: dokumentasi storage Docker untuk perilaku volume dan bind mount secara lengkap.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

volume.json

Unduh berkas
json
[
  {
    "Name": "toko_data-db",
    "Driver": "local",
    "Mountpoint": "/var/lib/docker/volumes/toko_data-db/_data",
    "Labels": {
      "com.docker.compose.project": "toko",
      "com.docker.compose.volume": "data-db"
    },
    "Scope": "local"
  }
]