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:

yaml
    volumes:
      - db-data:/var/lib/mysql

Di sisi wordpress:

yaml
    volumes:
      - wp-content:/var/www/html/wp-content

Dan di blok teratas yang mendaftarkan keduanya sebagai volume bernama:

yaml
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:

Aspekwp-contentdb-data
Isitema, plugin, unggahan penggunatabel, post, opsi, akun
Bentuk databerkas biasa yang bisa dibaca lsberkas internal MySQL yang tak boleh disentuh langsung
Path di container/var/www/html/wp-content/var/lib/mysql
Tanpa volumetema dan unggahan hilang saat container dibuangseluruh isi situs kembali kosong
Alat backuptarmysqldump

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:

bash
$ docker compose exec wordpress ls -l /var/www/html/wp-content/uploads/2026/10

Output yang Dihasilkan:

text
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.jpg

Empat 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:

bash
$ 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:

text
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:

bash
$ docker compose down

Output yang Dihasilkan:

text
[+] 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.3s

Tiga 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:

bash
$ docker compose up -d

Output yang Dihasilkan:

text
[+] 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.8s

Perhatikan 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:

bash
$ docker volume ls --filter name=wordpress-dan-mysql

Output yang Dihasilkan:

text
DRIVER    VOLUME NAME
local     wordpress-dan-mysql_db-data
local     wordpress-dan-mysql_wp-content

Prefiks 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:

bash
$ docker volume inspect wordpress-dan-mysql_db-data

Output yang Dihasilkan:

json
[
    {
        "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:

  1. com.docker.compose.project — label yang mengikat volume ke project wordpress-dan-mysql; inilah yang dibaca docker compose down -v untuk memutuskan volume mana yang ikut dibongkar, dan yang dipakai docker compose untuk menemukan kembali datanya saat stack dinyalakan ulang di folder yang sama.
  2. 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.
  3. CreatedAt — volume lahir sekali pada up pertama dan tidak pernah tercipta ulang oleh down biasa, 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:

  1. Alat backup yang berbeda — isi db-data hanya bermakna bagi MySQL dan harus dicadangkan lewat mysqldump, sedangkan wp-content adalah berkas biasa yang paling alami diarsipkan tar. 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.
  2. 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.
  3. 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 #

Berkas Sumber

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

compose.yaml

Unduh berkas
yaml
# 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: