Bagian 2 menutup dengan dua belas tabel wp_ yang terlihat hidup di mata akun aplikasi — tetapi melihat data tidak sama dengan mempercayainya. Janji volume mudah diucapkan: data bertahan melewati kematian container. Membuktikannya menuntut ritual yang jujur: menanam jejak yang kita kenali di kedua dunia data, menghancurkan container beserta network-nya, menyala ulang stack, lalu memeriksa apakah jejak itu masih berdiri di tempat yang sama. Bagian ini menjalankan ritual itu, menambahkan pemeriksaan dari sisi host lewat docker volume, dan menutupnya dengan pertanyaan desain yang jarang diajukan: mengapa dua volume terpisah, bukan satu volume besar untuk seluruh situs?
Tiga Deklarasi, Dua Volume #
Deklarasi volume tersebar di tiga tempat pada compose.yaml. Di sisi db:
volumes:
- db-data:/var/lib/mysqlDi sisi wordpress:
volumes:
- wp-content:/var/www/html/wp-contentDan di blok teratas yang mendaftarkan keduanya sebagai volume bernama:
volumes:
db-data:
wp-content:Sintaksnya adalah pola nama:folder yang kita kenal dari Volume: sumber tanpa garis miring di awal berarti volume bernama, bukan bind mount — Docker mengelola lokasinya di disk, dan entri di blok volumes teratas yang membuatnya resmi milik project. Perbandingan kedua volume terangkum berikut ini:
| Aspek | wp-content | db-data |
|---|---|---|
| Isi | tema, plugin, unggahan pengguna | tabel, post, opsi, akun |
| Bentuk data | berkas biasa yang bisa dibaca ls | berkas internal MySQL yang tak boleh disentuh langsung |
| Path di container | /var/www/html/wp-content | /var/lib/mysql |
| Tanpa volume | tema dan unggahan hilang saat container dibuang | seluruh isi situs kembali kosong |
| Alat backup | tar | mysqldump |
Dua baris terakhir tabel itu adalah alasan seluruh Bagian 4 ditulis: bentuk data yang berbeda menuntut alat cadangan yang berbeda. Sebelum sampai ke sana, kita tanam dulu buktinya.
Menanam Bukti di Kedua Dunia #
Masuk ke dashboard sebagai budi, buka menu Posts → Add New, dan buat tulisan berjudul Lima Kopi Favorit Pelanggan — dua paragraf cerita pelanggan reguler kedai sudah cukup. Sebelum menekan Publish, unggah sebuah gambar melalui panel Featured image: pilih berkas menu-kopi.jpg dari komputer. WordPress menyimpannya ke folder wp-content/uploads/2026/10/ — konvensi tahun/bulan yang otomatis mengikuti tanggal unggahan — dan sekaligus melahirkan beberapa versi ukuran yang lebih kecil. Tekan Publish, lalu periksa jejak berkasnya dari dalam container:
$ docker compose exec wordpress ls -l /var/www/html/wp-content/uploads/2026/10Output yang Dihasilkan:
total 164
-rw-r--r-- 1 www-data www-data 83421 Oct 13 09:12 menu-kopi.jpg
-rw-r--r-- 1 www-data www-data 12648 Oct 13 09:12 menu-kopi-150x150.jpg
-rw-r--r-- 1 www-data www-data 23976 Oct 13 09:12 menu-kopi-300x300.jpg
-rw-r--r-- 1 www-data www-data 41397 Oct 13 09:12 menu-kopi-768x768.jpgEmpat berkas itu milik user www-data — user yang dipakai Apache di dalam container. Sekarang jejak di dunia berkas sudah ada; tanam jejak pembandingnya di dunia database dengan membaca tabel wp_posts yang kita temui di Bagian 2:
$ docker compose exec db mysql -uwordpress -p wordpress -e \
"SELECT ID, post_title, post_date FROM wp_posts WHERE post_type = 'post' ORDER BY ID;"Output yang Dihasilkan:
Enter password:
+----+-----------------------------+---------------------+
| ID | post_title | post_date |
+----+-----------------------------+---------------------+
| 1 | Hello world! | 2026-10-13 08:55:40 |
| 5 | Lima Kopi Favorit Pelanggan | 2026-10-13 09:18:07 |
+----+-----------------------------+---------------------+Dua baris ini menyimpan cerita yang layak dibaca pelan. Baris ID 1 adalah tulisan bawaan Hello world! dari instalasi. ID 5 adalah tulisan kita — nomor itu bukan kebetulan: ID 2 dan 3 dipakai halaman contoh bawaan WordPress, sedangkan ID 4 adalah baris attachment untuk menu-kopi.jpg. Unggahan gambar memang tercatat sebagai baris di wp_posts — judul, metadata, dan ukurannya — sementara byte gambar itu sendiri hidup di wp-content/uploads/. Satu unggahan dengan demikian meninggalkan jejak di dua dunia sekaligus, dan itulah yang membuat pemeriksaan berikutnya tuntas.
Menguji Ketahanan dengan down dan up #
Container kini kita buang — bukan hanya distop, tetapi dibongkar bersama network-nya:
$ docker compose downOutput yang Dihasilkan:
[+] Running 3/3
✔ Container wordpress-dan-mysql-wordpress-1 Removed 1.1s
✔ Container wordpress-dan-mysql-db-1 Removed 2.2s
✔ Network wordpress-dan-mysql_default Removed 0.3sTiga baris itu perlu dibaca dengan teliti: dua container dan satu network dihapus — dan tidak ada satu pun baris Volume … Removed. Perilaku inilah janji down yang kita catat pada Menjalankan dan Mengelola Stack: volume bernama bukan milik container, sehingga pembongkaran container tidak menyentuhnya. Nyalakan ulang stack:
$ docker compose up -dOutput yang Dihasilkan:
[+] Running 3/3
✔ Network wordpress-dan-mysql_default Created 0.2s
✔ Container wordpress-dan-mysql-db-1 Healthy 2.1s
✔ Container wordpress-dan-mysql-wordpress-1 Started 1.8sPerhatikan angkanya: db lulus healthcheck dalam 2,1 detik — bandingkan dengan 11,4 detik pada penyalaan pertama di Bagian 1. Volume yang sudah berisi membuat inisialisasi dilewati seluruhnya; server MySQL cukup memuat data yang ada. Inilah bukti tidak langsung bahwa volume memang berisi. Dan bukti utamanya: jalankan ulang dua perintah pemeriksaan di atas — keluarannya identik byte demi byte. Tabel wp_posts masih memuat ID 1 dan ID 5 dengan post_date yang sama persis, dan ls pada folder uploads/2026/10 masih menampilkan empat berkas menu-kopi dengan ukuran serta waktu yang tidak bergeser. Tulisan dan gambarnya pun masih terbaca utuh di situs — container baru, data lama.
Memeriksa Volume dari Luar #
Docker CLI mengenal dua volume ini tanpa perlu membuka container mana pun. Filter berdasarkan nama project menjaga daftar tetap bersih dari volume stack latihan lain di mesin yang sama:
$ docker volume ls --filter name=wordpress-dan-mysqlOutput yang Dihasilkan:
DRIVER VOLUME NAME
local wordpress-dan-mysql_db-data
local wordpress-dan-mysql_wp-contentPrefiks wordpress-dan-mysql pada kedua nama adalah nama project Compose — diambil dari nama folder, karena compose.yaml kita sengaja tidak memakai kunci name: (komentar tiga baris pertamanya menyatakan hal itu secara eksplisit). Detail kepemilikannya terbuka penuh lewat inspect:
$ docker volume inspect wordpress-dan-mysql_db-dataOutput yang Dihasilkan:
[
{
"CreatedAt": "2026-10-13T08:52:11Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "wordpress-dan-mysql",
"com.docker.compose.project.config_files": "compose.yaml",
"com.docker.compose.project.working_dir": "/home/budi/wordpress-dan-mysql",
"com.docker.compose.volume": "db-data"
},
"Mountpoint": "/var/lib/docker/volumes/wordpress-dan-mysql_db-data/_data",
"Name": "wordpress-dan-mysql_db-data",
"Options": null,
"Scope": "local"
}
]Tiga kunci JSON di atas menjelaskan siapa pemilik sebenarnya dari data kita:
com.docker.compose.project— label yang mengikat volume ke projectwordpress-dan-mysql; inilah yang dibacadocker compose down -vuntuk memutuskan volume mana yang ikut dibongkar, dan yang dipakaidocker composeuntuk menemukan kembali datanya saat stack dinyalakan ulang di folder yang sama.Mountpoint— lokasi fisik di host:/var/lib/docker/volumes/wordpress-dan-mysql_db-data/_data, dikelola Docker di wilayahnya sendiri, jauh dari folder proyek. Perbedaan wilayah inilah pembeda volume bernama dari bind mount yang menunjuk folder milik kita — trade-off yang kita rangkum di Memilih Jenis Mount.CreatedAt— volume lahir sekali padauppertama dan tidak pernah tercipta ulang olehdownbiasa, cocok dengan masa tenggang inisialisasi yang sudah kita ukur dua kali.
Kenapa Dipisah, Bukan Digabung #
Satu volume besar untuk seluruh situs akan lebih hemat satu baris konfigurasi — tetapi tiga alasan membuat pemisahan db-data dan wp-content membayar dirinya sendiri:
- Alat backup yang berbeda — isi
db-datahanya bermakna bagi MySQL dan harus dicadangkan lewatmysqldump, sedangkanwp-contentadalah berkas biasa yang paling alami diarsipkantar. Volume terpisah membuat tiap dunia punya ritual cadangannya sendiri — harian untuk database yang berubah setiap komentar, mingguan untuk berkas yang berubah setiap unggahan — persis ritme yang kita bangun di Bagian 4. - Laju tumbuh yang berbeda — tabel post tumbuh beberapa kilobyte per tulisan, sementara satu sesi unggahan foto bisa menambah belasan megabyte ke
wp-content. Dengan volume terpisah, pemantauan pemakaian disk bisa membedakan “database yang bengkak” dari “unggahan yang menumpuk” — dua diagnosis dengan dua obat yang berbeda. - Siklus perubahan yang berbeda — image WordPress bisa naik versi tanpa menyentuh isi database; migrasi atau uji coba server MySQL baru bisa berjalan tanpa memindahkan ratusan megabyte unggahan. Dua volume berarti dua keputusan upgrade yang tidak saling menahan.
Data kini terbukti tahan bongkar-pasang container — tetapi persistensi bukanlah backup: volume tetap satu disk di satu mesin, dan down -v tetap satu perintah yang menghapus segalanya. Bagian 4: Backup dan Restore mengeluarkan data dari mesin ini: dua resep cadangan dari README.md, otomasi cron, dan disaster drill yang menguji semuanya sampai tuntas.
Referensi #
- Volumes — Docker Docs — mekanisme volume bernama, mountpoint, dan siklus hidupnya di host.
- docker volume — CLI reference
— rujukan
docker volume lsdandocker volume inspect.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
compose.yaml
Unduh berkas# WordPress + MySQL: dua service jadi, dua volume, tanpa Dockerfile.
# Nama proyek Compose otomatis diambil dari nama folder karena berkas
# ini sengaja tidak memakai kunci `name:`.
services:
db:
image: mysql:8.4
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD wajib diisi lewat .env}"
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: "${MYSQL_PASSWORD:?MYSQL_PASSWORD wajib diisi lewat .env}"
volumes:
- db-data:/var/lib/mysql
healthcheck:
# $$ membuat dolar lolos dari interpolasi Compose; shell di dalam
# container yang mengekspansi $MYSQL_ROOT_PASSWORD saat test berjalan.
test: ["CMD-SHELL", "mysqladmin ping -h localhost -p$$MYSQL_ROOT_PASSWORD"]
interval: 3s
timeout: 3s
retries: 20
start_period: 30s
wordpress:
image: wordpress:6.8-apache
restart: unless-stopped
depends_on:
db:
condition: service_healthy
ports:
- "8090:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: "${MYSQL_PASSWORD:?MYSQL_PASSWORD wajib diisi lewat .env}"
WORDPRESS_DB_NAME: wordpress
volumes:
- wp-content:/var/www/html/wp-content
volumes:
db-data:
wp-content: