Sertifikat TLS telah terpasang di proxy, perpanjangan berjalan otomatis lewat loop certbot dan nginx -s reload setiap enam jam, dan toko kini melayani pengunjung lewat HTTP/2 di port 443 — data yang melintas jaringan sudah terenkripsi rapi. Namun enkripsi jaringan hanya menutup separuh jalur hidup data: begitu permintaan usai, seluruh kebenaran toko — pesanan, pelanggan, stok, gambar produk — beristirahat di sebuah volume di disk server, dan di sanalah musibah yang lebih sunyi menunggu. Disk yang penuh mendadak bisa membawa turun sistem operasi ikut sekarat, partisi yang sama dengan /var/log bisa menenggelamkan database bersama log yang tak terbatas, dan satu perintah pembersih yang salah waktu mampu memusnahkan volume tanpa sidang. Chapter ini memilih rumah yang benar bagi data produksi: named volume berlabel yang dikelola di luar siklus Compose, volume driver jaringan untuk data yang harus bertahan dari host, dan data-root yang memisahkan seluruh isi Docker ke disk khusus — ditutup dengan pemantauan disk dan kerangka rencana pemulihan bencana.

Tiga Rumah untuk Data Produksi #

Docker menyediakan tiga tempat berbeda untuk data yang harus bertahan, dan ketiganya sudah kita kenal sepanjang Section Penyimpanan Data — yang berubah di produksi hanyalah bobot konsekuensinya. Tabel berikut membandingkan ketiganya dari sudut pandang operator server, bukan lagi sudut pandang developer laptop:

PilihanDi mana data hidupKelebihanRisiko utama
Named volume lokaldi bawah data-root daemon, dikelola penuh oleh Dockercepat, di-copy-up dengan kepemilikan image, tak terlihat tool hostsatu disk dengan sistem operasi — kegagalan disk memusnahkan data
Bind mountdirektori host yang kita pilih sendiriterlihat dan bisa dibaca tool backup host biasajebakan UID/GID antara container dan host; berada di luar kendali daemon; mudah tersapu perintah rm -rf yang salah alamat
Volume driver jaringan (NFS/EFS)penyimpanan luar yang dipasang lewat driver local dengan opsi mountdata bertahan dari kehilangan host; bisa dibagi lintas hostlatensi jaringan pada setiap tulis; server penyimpanan itu sendiri titik kegagalan; semantik fsync database perlu diuji

Ketiga baris itu bukan peringkat melainkan peta: pilihan yang tepat bergantung pada jenis beban kerja, dan kita akan menutup chapter dengan tabel keputusan yang memetakan beban kerja ke rumahnya. Sebagai titik berangkat, stack toko membutuhkan dua volume: toko_db-data untuk cluster PostgreSQL di service db — promosi dari volume db-data yang sejauh ini dikelola Compose — dan toko_uploads, rumah baru untuk gambar produk yang diunggah admin lalu disajikan service frontend dari path /uploads/. Keduanya akan kita kelola sebagai named volume lokal berlabel.

Named Volume Berlabel sebagai Standar #

Sampai chapter ini, volume produksi toko lahir otomatis saat docker compose up pertama kali: kunci db-data pada compose.yaml dari Chapter Konfigurasi Production menghasilkan volume bernama toko-produksi_db-data — prefiks project toko-produksi menempel otomatis — dan Compose memusnahkannya pada docker compose down -v. Di produksi, siklus hidup data tidak boleh sama dengan siklus hidup stack: container boleh lahir mati setiap rilis, data harus hidup lebih lama dari semuanya. Promosinya berjalan tiga langkah: ciptakan volume berlabel secara manual, pindahkan isi cluster sekali lewat pipa tar antar volume — pola yang kita latih pada Chapter Migrasi Data Container — lalu nyatakan kedua volume sebagai external di compose.yaml.

bash
$ docker volume create --label env=prod toko_db-data
$ docker volume create --label env=prod toko_uploads
$ docker compose stop db
$ docker run --rm \
    -v toko-produksi_db-data:/dari:ro \
    -v toko_db-data:/ke \
    alpine:3.20 \
    sh -c 'cd /dari && tar cf - . | (cd /ke && tar xf -)'

Perintah salinan antar volume itu selesai tanpa satu baris output pun — tar hanya bersuara ketika ada yang gagal. Langkah terakhir promosi menunggu di compose.yaml: deklarasi volume berganti bentuk, lalu db menyala menempel rumah barunya:

yaml
services:
  db:
    image: postgres:18-alpine
    ports:
      - "127.0.0.1:5432:5432"
    volumes:
      - db-data:/var/lib/postgresql/data

  frontend:
    image: nginx:1.29-alpine
    volumes:
      - uploads:/usr/share/nginx/html/uploads

volumes:
  db-data:
    external: true
    name: toko_db-data
  uploads:
    external: true
    name: toko_uploads

Mari kita telaah tiga baris kunci pada deklarasi tersebut:

  1. external: true — Compose dilarang menciptakan maupun menghapus volume ini; docker compose down -v, yang biasanya memusnahkan volume milik project, melewati volume eksternal tanpa menyentuhnya. Volume hidup di luar siklus up/down persis seperti yang kita inginkan untuk data produksi.
  2. name: toko_db-data — nama sebenarnya dipatok secara eksplisit tanpa prefiks project; nama lama toko-produksi_db-data sengaja tidak dipertahankan agar volume produksi tidak pernah tertukar dengan volume internal project lain yang kebetulan memakai kunci sama.
  3. Kunci db-data tetap pendek di sisi service — kunci itu hanya pengenal lokal di dalam berkas; db tetap menulis db-data:/var/lib/postgresql/data sementara dunia luar mengenal toko_db-data. Pemisahan nama-pendek-dalam-berkas dan nama-penuh-di-dunia inilah yang membuat stack yang sama bisa dipakai ulang di project lain tanpa tabrakan nama volume.

Satu baris baru pada service db di fragment di atas juga pantas dijelaskan: pemetaan "127.0.0.1:5432:5432" adalah pemetaan debug yang terikat loopback — pola Chapter Port Publishing — yang Budi tanam saat migrasi volume agar psql dari host bisa memeriksa data tanpa membuka satu pintu pun ke jaringan. Kebiasaan itu kita verifikasi ulang lewat pendengar kernel pada Chapter Docker di VPS. Sekarang db dinyalakan ulang dengan deklarasi baru, dan isi cluster dibuktikan utuh:

bash
$ docker compose up -d db && docker compose exec db psql -U toko -d toko -c "SELECT count(*) FROM produk;"

Output yang Dihasilkan:

text
[+] Running 2/2
 ✔ Container toko-produksi-db-1  Recreated  0.4s
 ✔ Container toko-produksi-db-1  Healthy    3.1s
 count
-------
    18
(1 row)

Recreated terjadi karena pemetaan volume db berganti, dan delapan belas baris produk pindah rumah tanpa kehilangan satu baris pun — volume lama toko-produksi_db-data kini boleh dibongkar lewat docker volume rm begitu kita yakin, sedangkan toko_uploads lahir kosong menunggu unggahan pertama admin.

Label yang kita tempelkan saat penciptaan tidak sekadar hiasan metadata — label adalah bukti yang bisa diperiksa dan pagar untuk perintah pembersihan. Bukti itu terbaca langsung dari inspect:

bash
$ docker volume inspect toko_db-data

Output yang Dihasilkan:

json
[
    {
        "CreatedAt": "2026-10-22T09:12:04+07:00",
        "Driver": "local",
        "Labels": {
            "env": "prod"
        },
        "Mountpoint": "/data/docker/volumes/toko_db-data/_data",
        "Name": "toko_db-data",
        "Options": {},
        "Scope": "local"
    }
]

Mari kita telaah dua field pada keluaran tersebut:

  1. Labels berisi "env": "prod" — satu-satunya label yang melekat, karena volume ini lahir dari perintah manual kita, bukan dari Compose (label com.docker.compose.* hanya diberikan kepada volume yang diciptakan Compose); inilah tanda tangan bahwa volume dikelola manusia di luar siklus stack.
  2. Mountpoint berada di bawah /data/docker — path itu bukan bawaan /var/lib/docker; server ini memakai data-root di disk terpisah, keputusan yang kita pasang pada bagian akhir chapter ini.

Ujian sebenarnya dari external: true adalah perintah yang paling ditakuti operator. Budi membuktikannya sekali pada jendela pemeliharaan dini hari, ketika toko sepi: stack dimatikan total dengan -v, lalu dinyalakan ulang.

bash
$ docker compose down -v

Output yang Dihasilkan (dipotong):

text
[+] Running 7/7
 ✔ Container toko-produksi-proxy-1     Removed
 ✔ Container toko-produksi-db-1        Removed
 ...
 ✔ Network toko-produksi_backend-net   Removed
 ✔ Network toko-produksi_frontend-net  Removed

Tidak ada satu baris Volume ... Removed pada keluaran itu — satu-satunya baris yang biasanya memusnahkan data justru absen, karena kedua volume kita berstatus eksternal. Baris berikutnya adalah pembuktian pamungkas:

bash
$ docker volume ls --filter label=env=prod

Output yang Dihasilkan:

text
DRIVER    VOLUME NAME
local     toko_db-data
local     toko_uploads

Kedua volume masih utuh, dan docker compose up -d menuntaskan giliran terakhirnya: db Healthy dalam belasan detik dan seluruh data pesanan kembali terbaca — total pemadaman tercatat 47 detik pada jendela yang memang sengaja dipilih.

Volume Driver Jaringan: Data di Luar Host #

Named volume lokal menyelesaikan masalah kepemilikan dan siklus hidup, tetapi data tetap terkurung di satu disk host. Docker menawarkan jalan keluar yang mengejutkan murahnya: driver local — driver yang sama dengan named volume biasa — dapat memasang penyimpanan jaringan lewat opsi mount yang diteruskan ke kernel. Bentuk lengkapnya untuk NFS:

bash
$ docker volume create --driver local \
    --opt type=nfs \
    --opt device=:/export/db \
    --opt o=addr=192.168.1.30,nfsvers=4 \
    vol-db-nfs

Output yang Dihasilkan:

text
vol-db-nfs

Mari kita telaah tiga pasang opsi tersebut:

  1. --opt type=nfs — jenis filesystem yang akan dipasang kernel; driver local pada dasarnya membungkus panggilan mount, sehingga tipe yang didukung adalah tipe yang didukung kernel host — nfs, tmpfs, bahkan none dengan opsi bind.
  2. --opt device=:/export/db — sumber mount dari sisi server NFS; titik dua di depan adalah sintaks klasik NFS server:path, dan karena alamat server sudah berada di opsi o, device cukup berisi path ekspornya.
  3. --opt o=addr=192.168.1.30,nfsvers=4 — opsi mount tambahan: alamat server NFS di 192.168.1.30 dan versi protokol 4; keduanya diteruskan mentah ke kernel saat volume pertama kali dipakai sebuah container.

Volume hasil perintah ini tampak seperti volume biasa dari sisi container — nama service tetap menulis vol-db-nfs:/var/lib/postgresql/data — bedahnya terlihat pada inspect, di mana field Options kini terisi penuh:

bash
$ docker volume inspect vol-db-nfs --format '{{json .Options}}'

Output yang Dihasilkan:

json
{"device":":/export/db","o":"addr=192.168.1.30,nfsvers=4","type":"nfs"}

Uji cepat membuktikan data benar-benar menyeberang ke server NFS: sebuah container menulis berkas ke volume, lalu berkas yang sama terbaca dari sisi server.

bash
$ docker run --rm -v vol-db-nfs:/data alpine:3.20 sh -c 'echo uji > /data/uji-nfs.txt'
$ ssh [email protected] cat /export/db/uji-nfs.txt

Output yang Dihasilkan:

text
uji

Sekarang bagian yang jujur. Memindahkan volume ke NFS tidak memindahkan kita keluar dari dunia titik kegagalan tunggal — ia hanya memindahkan titik itu dari disk VPS ke server NFS; NAS yang mati kini membawa serta seluruh volume yang bergantung padanya, ditambah latensi jaringan yang menempel pada setiap penulisan. Untuk database transaksional, tambahan satu lapis lagi yang harus diuji: semantik fsync di atas NFS harus dijamin benar oleh server dan konfigurasinya, atau integritas WAL PostgreSQL bisa terancam. Karena itu keputusan toko kita: cluster PostgreSQL tetap di named volume lokal, dan keselamatannya dibangun lewat backup logis malaman — bukan lewat media penyimpanan. NFS masuk hitungan justru untuk berkas unggahan yang harus dibagi lintas host, dan SAN atau volume cloud yang menempel ke satu host dalam blok tebal menjadi masuk akal ketika latensi penuh jaringan tak lagi bisa ditoleransi — wilayah yang kita jelajahi lebih dalam saat stack tumbuh melebihi satu mesin.

Data-root di Disk Terpisah dari Sistem Operasi #

Pilihan ketiga tidak menyangkut volume sama sekali, melainkan rumah besar tempat seluruh isi Docker tinggal: image, container, build cache, dan seluruh volume lokal hidup di bawah data-root — /var/lib/docker secara bawaan, di partisi yang sama dengan sistem operasi. Konsekuensinya berlapis: partisi / yang penuh oleh layer image membuat sshd gagal menulis log dan operator kehilangan akses tepat saat dibutuhkan, ukuran konsumsi Docker tercampur dengan noise sistem, dan rebuild OS mesti hati-hati agar /var/lib/docker tidak ikut disapu. Server produksi toko punya disk data kedua /dev/vdb1; kita pasang sebagai /data lewat fstab agar bertahan lintas boot:

bash
$ sudo mkfs.ext4 /dev/vdb1
$ sudo mkdir -p /data
$ echo '/dev/vdb1 /data ext4 defaults,nofail 0 2' | sudo tee -a /etc/fstab
$ sudo mount -a
$ df -h / /data

Output yang Dihasilkan:

text
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1         20G  6.2G   13G  31% /
/dev/vdb1        100G  4.2G   91G   5% /data

Satu baris pada fstab menuntaskan pemasangan; opsi nofail menjaga server tetap boot meski disk data lepas. Selanjutnya daemon.json — berkas yang kita kenal dari Chapter Konfigurasi Docker Engine — mengarahkan Docker ke rumah barunya:

json
{
  "data-root": "/data/docker"
}
bash
$ sudo systemctl restart docker
$ docker info

Output yang Dihasilkan (dipotong):

text
Server:
 Images: 5
 Server Version: 29.9.0
 Storage Driver: overlayfs
  driver-type: containerd
...
 Docker Root Dir: /data/docker
...

Baris Docker Root Dir: /data/docker adalah bukti pindahan berhasil, dan seluruh Mountpoint pada docker volume inspect kini berprefiks /data/docker/volumes/. Pada server yang sudah lama berjalan, pindahan data-root menuntut satu langkah pengaman:

Tiga keuntungan pemisahan ini terasa sejak hari pertama. Pertama, disk data yang penuh tidak lagi membunuh sistem operasi — sshd, jurnal sistem, dan ufw tetap hidup sehingga operator bisa masuk dan mendiagnosis persis pada saat darurat. Kedua, konsumsi Docker menjadi terukur dengan satu perintah: df -h /data kini adalah tagihan Docker dan hanya tagihan Docker. Ketiga, sistem operasi bisa dibangun ulang dari nol tanpa menyentuh data — cukup lepas partisi / lalu pasang ulang, dan fstab yang sama mengembalikan /data lengkap dengan seluruh volume produksi.

Label sebagai Pagar saat Membersihkan #

Label env=prod yang kita tempelkan pada kedua volume kini menunaikan tugas keduanya: menjadi pagar pada saat operator membersihkan mesin. Penyaring pertama memangkas daftar volume menjadi hanya milik produksi:

bash
$ docker volume ls --filter label=env=prod

Output yang Dihasilkan:

text
DRIVER    VOLUME NAME
local     toko_db-data
local     toko_uploads

Penyaring yang lebih penting justru bentuk negasinya, karena perintah pembersih volume menghapus semua volume yang tidak dipakai container — dan volume produksi yang sedang dipakai db aman, tetapi volume produksi yang sesaat tidak terpasang (misalnya saat stack sedang down di jendela pemeliharaan) berada tepat di jalur tembak.

Memantau Disk dan Menyiapkan Musibah #

Rumah yang benar tetap perlu diawasi isinya. Satu perintah merangkum seluruh konsumsi disk milik daemon dalam empat baris:

bash
$ docker system df

Output yang Dihasilkan:

text
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          5         5         872MB     0B (0%)
Containers      5         5         36.7MB    0B (0%)
Local Volumes   2         2         412MB     0B (0%)
Build Cache     0         0         0B        0B

Mari kita telaah empat baris tersebut dari sudut pandang produksi:

  1. Images 5/5 aktif — lima image stack toko semuanya terpakai; RECLAIMABLE 0B berarti tidak ada image menganggur, wajar pada server yang hanya menarik image yang dideklarasikan compose.yaml.
  2. Containers 36.7MB — ukuran ini adalah writable layer ditambah berkas log yang masih tersimpan; angka yang terkunci kecilnya adalah buah rotasi log yang kita pasang pada chapter berikutnya.
  3. Local Volumes 412MB — cluster PostgreSQL ditambah arsip unggahan; satu-satunya baris yang tumbuh seiring usia toko, dan oleh karena itu baris yang paling perlu diawasi.
  4. Build Cache 0 — server produksi tidak pernah membangun image; seluruh image datang dari registry bertag, keputusan yang kita kunci pada Chapter Konfigurasi Production.

Angka-angka itu melengkapi df -h /data: yang pertama menjelaskan isi, yang kedua mengukur tempat. Aturan praktis toko menetapkan ambang waspada 80% — pelebihan ambang memicu notifikasi yang mekanismenya kita bangun lengkap dengan skrip dan cron pada Chapter Docker di VPS, sementara pembahasan penuh pemantauan resource CPU dan memori berada di Chapter Monitoring Resource. Pemantauan adalah separuh kesiapan; separuh lainnya adalah pengakuan jujur bahwa musibah tetap mungkin, dan untuk itu empat kerangka disaster recovery plan kita pancangkan sekarang:

  1. Definisikan musibah yang ditanggung — disk mati, host mati total, data terkorupsi, dan human error seperti docker volume rm yang salah sasaran; keempatnya menuntut jawaban berbeda, dan rencana yang hanya menjawab satu adalah rencana setengah jadi.
  2. Salinan mengikuti pola 3-3-1 — tiga salinan di dua media berbeda dengan satu di luar lokasi; di dunia Docker, wujudnya adalah volume hidup plus arsip tar plus dump SQL, kerangka yang kita bangun penuh pada Chapter Backup dan Recovery.
  3. Jalur pemulihan tertulis dan teruji — runbook bernomor yang dilatih berkala, bukan dokumen yang hanya dibaca saat panik; pola helper container untuk arsip volume berasal dari Chapter Backup dan Restore Volume, dan pola dump logis dari Chapter Backup Database.
  4. Angka RTO dan RPO disepakati sebelum musibah — berapa lama boleh mati dan berapa banyak data yang boleh hilang adalah keputusan bisnis, bukan hasil hitungan panik di tengah insiden.

Seluruh pilihan chapter ini kini bisa dirangkum menjadi satu tabel keputusan — pemetaan beban kerja ke rumah yang tepat:

Beban kerjaRumah yang tepatAlasan singkat
Database transaksional satu hostnamed volume lokal + dump logis terjadwallatensi disk lokal; keselamatan dibangun dari backup, bukan dari media
Berkas unggahan penggunanamed volume lokal; NFS bila harus dibagi lintas hostberkas statis aman diarsipkan dengan tar sambil layanan hidup
Konfigurasi statisconfigs Compose atau bind mount :robukan data — cukup dibaca, tidak perlu bertahan
Data sementara (sesi, berkas antara)tmpfs mounthilang bersama container memang perilaku yang diinginkan

Data toko kini punya rumah yang benar: volume berlabel yang tak tersentuh siklus stack, data-root di disk terpisah dari sistem operasi, dan kerangka pemulihan yang menunggu dilengkapi. Namun ada satu penghuni disk yang tumbuh diam-diam lebih rajin dari data mana pun dan baru terasa saat semuanya terlambat — log. Driver bawaan json-file menulis tanpa batas kecuali kita mengaturnya, dan pengaturan itu, beserta log terstruktur dan korelasi antar service, adalah panggung Chapter Logging Production.

Referensi #