Persistensi sudah terbukti di Bagian 3 — container boleh dibongkar berkali-kali, datanya tetap setia. Tetapi volume bukan backup: db-data dan wp-content tetap tinggal di satu disk pada satu mesin, dan peringatan down -v dari Bagian 3 mengingatkan betapa dekatnya kata “hapus seluruhnya” dengan jari kita. Bagian penutup proyek ini mengeluarkan data dari mesin: dua dunia data dicadangkan dengan dua alat yang berbeda, ritualnya diotomasi lewat cron, dan hasilnya tidak dipercaya begitu saja — semuanya diuji oleh disaster drill yang sengaja menghancurkan situs penuh, lalu membangkitkannya kembali dari dua berkas arsip. Seluruh perintah pada bagian ini dikutip verbatim dari README.md proyek, sehingga yang kita bangun di chapter dan yang tersimpan di repositori adalah satu resep yang sama.

Backup Database #

Perintah berikut dijalankan dari root folder proyek, dengan stack dalam keadaan menyala:

bash
$ source .env && docker compose exec -T db sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" wordpress' | gzip > backup-wordpress-$(date +\%F).sql.gz

Output yang Dihasilkan:

text
mysqldump: [Warning] Using a password on the command line interface can be insecure.

Sebelum bedah perintahnya, satu kalimat di awal baris perlu dijelaskan dulu karena paling sering membingungkan: source .env. Compose membaca .env secara otomatis — tetapi hanya untuk urusan interpolasinya sendiri, mengganti ${…} pada compose.yaml ketika berkas itu dibaca. Shell yang mengetik perintah ini tidak pernah dilayani pembacaan tersebut: variabel MYSQL_ROOT_PASSWORD tidak tiba-tiba hadir di sesi shell kita hanya karena berkasnya ada. source memuat pasangan kunci=nilai ke shell saat ini dengan dua faedah: gerbang fail fast — bila .env hilang, source gagal dan && menghentikan pipeline sebelum menyala — dan variabel yang siap dipakai perintah host lain pada sesi yang sama. Ekspansi sandinya sendiri justru terjadi di dalam container: argumen sh -c dibungkus tanda kutip tunggal sehingga shell host mengirimnya apa adanya, dan shell di dalam container db-lah yang mengekspansi $MYSQL_ROOT_PASSWORD dari environment service yang disuntik Compose. Kutip tunggal itu bukan gaya — kutip ganda di sisi host justru memindahkan ekspansi ke shell host dan membuat sandi tercetak pada daftar proses.

Mari kita telaah empat bagian inti perintah ini:

  1. docker compose exec -T db — menunjuk service bernama db, yang Compose terjemahkan menjadi container wordpress-dan-mysql-db-1 mengikuti pola nama <project>-<service>-<index> — pola yang membuat perintah tetap benar kapan pun container-nya dicipta ulang. Flag -T mematikan alokasi pseudo-TTY; tanpanya, keluaran yang dipipakan terkontaminasi terjemahan terminal — jebakan yang kita bedah pada Backup Database.
  2. sh -c 'exec mysqldump …' — exec menggantikan shell dengan mysqldump itu sendiri, sehingga proses backup menerima sinyal penghentian langsung tanpa tersangkut di induk shell; -p"$MYSQL_ROOT_PASSWORD" diekspansi shell container; argumen terakhir wordpress adalah database yang dibaca.
  3. Pipe | di host — mysqldump menulis SQL ke stdout di dalam container, Compose menyalurkan stream itu ke stdout host, dan gzip di host memampatkannya. Tidak ada berkas SQL perantara yang pernah ditulis ke disk: data mengalir dari tabel ke arsip dalam satu pipa.
  4. > backup-wordpress-$(date +\%F).sql.gz — nama arsip bertanggal (backup-wordpress-2026-10-13.sql.gz), sehingga backup besok tidak menimpa backup hari ini.

Satu-satunya keluaran perintah adalah peringatan tadi. Pesan mysqldump: [Warning] Using a password on the command line interface can be insecure. ditulis ke stderr, bukan stdout: redirect > hanya menangkap stdout, sehingga peringatan tetap tampil di terminal sebagai pengingat sementara arsip SQL tetap bersih darinya. Isi peringatannya pun jujur — bentuk -p<password> memang bisa terbaca proses lain di mesin yang sama — tetapi di dalam container satu-tujuan ini satu-satunya penghuni adalah MySQL dan mysqldump, sehingga risikonya nyaris nol. Kita menerima peringatan itu sebagai harga pola resmi yang didokumentasikan Docker Hub.

Backup wp-content #

Dunia kedua dicadangkan dengan alat yang sesuai bentuknya — berkas biasa diarsipkan tar, dijalankan dari dalam container wordpress:

bash
$ docker compose exec -T wordpress tar -czf - -C /var/www/html wp-content > backup-wp-content.tar.gz

Perintah satu baris ini padat helper tar yang jarang kita tulis sendiri:

  1. -czf - — buat arsip (c), mampatkan dengan gzip (z), tulis ke berkas bernama - (f -) — dan - pada konteks ini berarti stdout. Arsip tidak pernah ditulis ke disk di dalam container; ia mengalir keluar lewat stream exec, sama seperti SQL tadi.
  2. -C /var/www/html — pindah ke direktori itu dulu sebelum mengarsipkan, sehingga nama berkas di dalam arsip berawalan wp-content/, bukan /var/www/html/wp-content/. Pilihan kecil ini yang membuat restore-nya kelak cuma butuh satu -C yang sama.
  3. wp-content — direktori yang diarsipkan: tema, plugin, dan seluruh unggahan termasuk uploads/2026/10/ peninggalan tulisan Bagian 3.
  4. > backup-wp-content.tar.gz — redirect di host menangkap stream arsip menjadi satu berkas.

Kedua arsip kini berdiri bersebelahan di folder proyek, dan ukurannya mengesankan betapa berbedanya dua dunia yang kita cadangkan:

bash
$ ls -lh backup-wordpress-2026-10-13.sql.gz backup-wp-content.tar.gz

Output yang Dihasilkan:

text
-rw-r--r-- 1 budi budi  72K Oct 13 10:04 backup-wordpress-2026-10-13.sql.gz
-rw-r--r-- 1 budi budi 2.9M Oct 13 10:05 backup-wp-content.tar.gz

Seluruh isi situs — dua belas tabel, dua tulisan, satu akun admin, tema bawaan, empat varian gambar — terwakili oleh 72K data terstruktur dan 2,9M berkas. Perbandingan inilah argumen paling ringkas untuk dua alat yang berbeda: memampatkan dump SQL memang memakan rasio besar, sementara JPEG yang sudah terkompresi hampir tidak menyusut lagi di dalam tar gzip.

Otomasi: cron dan Rotasi Arsip #

Backup yang mengandalkan ingatan hanya sekuat ingatan itu. Dua baris crontab berikut — dikutip verbatim dari README.md — mengubah ritual menjadi jadwal: database tiap hari pukul 02:30, berkas setiap hari Minggu pukul 03:30:

text
30 2 * * * cd /home/budi/wordpress-dan-mysql && source .env && docker compose exec -T db sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" wordpress' | gzip > backup-wordpress-$(date +\%F).sql.gz
30 3 * * 0 cd /home/budi/wordpress-dan-mysql && docker compose exec -T wordpress tar -czf - -C /var/www/html wp-content > backup-wp-content-$(date +\%F).tar.gz

Empat detail kecil pada dua baris itu menyimpan jebakan klasik cron:

  1. cd /home/budi/wordpress-dan-mysql — cron memulai sesi tanpa memedulikan folder proyek; tanpa cd, .env tidak ditemukan source dan compose.yaml tidak ditemukan Compose.
  2. \% pada date +\%F — cron menafsirkan karakter % sebagai penanda akhir baris perintah; sisa baris setelahnya dianggap masukan stdin, bukan perintah. Tanpa escape, $(date +%F) terpotong dan nama berkas kacau — jebakan yang kita bedah tuntas di Backup Database.
  3. . ./.env sebagai ejaan POSIX — cron menjalankan barisnya dengan /bin/sh, dan pada Debian atau Ubuntu shell itu adalah dash yang tidak mengenal source sebagai builtin. Bila crontab Anda menolaknya, ganti segmen itu menjadi . ./.env — perintah titik adalah ejaan POSIX untuk memuat berkas environment, maknanya sama persis.
  4. Dua jadwal terpisah — jeda satu jam antara backup harian dan mingguan memberi dump besar waktu selesai sebelum arsip berikutnya lahir; workload tidak pernah berebut jam yang sama.

Rotasi arsip menutup otomasi ini — berkas cadangan tidak boleh menumpuk tanpa batas, dan lima belas hari adalah jendela retensi yang kita pilih:

bash
$ find . -name "backup-*" -mtime +14 -delete

Satu baris find memeriksa waktu modifikasi (-mtime +14 berarti lebih tua dari 14 hari penuh) dan menghapus yang lolos (-delete), tanpa satu pun pemanggilan rm per berkas. Dijalankan berkala — masuk ke crontab pun sah — baris ini menjaga folder proyek tetap memuat dua minggu sejarah situs, tidak lebih.

Restore Database #

Arah data kini dibalik: arsip di host harus mengalir masuk ke container. Perintahnya, verbatim dari README.md:

bash
$ source .env && gunzip -c backup-wordpress-2026-10-13.sql.gz | docker compose exec -T db sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" wordpress'

Mari kita telaah alur baliknya:

  1. gunzip -c — membongkar arsip dan menulis hasil dekompresinya ke stdout; -c penting karena tanpanya gunzip mengganti berkas arsip itu sendiri dengan versi tak terkompresi di disk.
  2. docker compose exec -T db sh -c 'exec mysql …' — stream dari pipe masuk ke stdin container; -T menegaskan tidak ada pseudo-TTY yang menerjemahkan byte di tengah jalan — semangat yang sama dengan docker exec -i tanpa -t yang kita andalkan pada Restore Database: data SQL harus mengalir murni, satu byte pun tidak boleh ditafsir ulang terminal.
  3. mysql -uroot -p"$MYSQL_ROOT_PASSWORD" wordpress — klien di sisi container membaca perintah SQL dari stdin dan mengeksekusinya pada database wordpress; root dipakai karena restore perlu membangun ulang seluruh tabel.

Kita tidak perlu memilih antara “tabel sudah ada” dan “database harus dikosongkan dulu”, karena dump mysqldump membawa pembersihnya sendiri. Jumlahnya persis seperti yang kita harapkan:

bash
$ gunzip -c backup-wordpress-2026-10-13.sql.gz | grep -c "DROP TABLE IF EXISTS"

Output yang Dihasilkan:

text
12

Di depan setiap CREATE TABLE pada dump tertulis DROP TABLE IF EXISTS — dua belas kali, satu untuk tiap tabel wp_ yang kita hitung di Bagian 2. Akibatnya restore selalu menimpa dengan rapi: tabel lama dibongkar dulu, lalu dibangun ulang dari dump, tanpa perlu mengosongkan database secara manual dan tanpa sisa baris lama yang tercecer di antara baris baru.

Restore wp-content #

Dunia berkas dipulihkan dengan satu baris yang memadatkan tiga langkah — kosongkan, ekstrak, lalu rapikan lewat restart:

bash
$ docker compose exec -T wordpress sh -c 'rm -rf /var/www/html/wp-content/* && tar -xzf - -C /var/www/html' < backup-wp-content.tar.gz
$ docker compose restart wordpress

Tiga keputusan pada baris pertama dan satu pada baris kedua:

  1. rm -rf /var/www/html/wp-content/* dulu — arsip menimpa berkas yang namanya sama, tetapi tidak menghapus berkas ekstra yang tidak ada di dalamnya. Tanpa pengosongan dulu, unggahan yang pernah dihapus dari situs sebelum backup akan menetap kembali seperti hantu; mengosongkan membuat hasil restore persis identik dengan isi arsip.
  2. tar -xzf - — bentuk kebalikan dari backupnya: baca arsip gzip dari stdin (-), ekstrak (-x). Redirect < di host mengalirkan berkas arsip masuk ke stdin container — pasangan dari -C /var/www/html yang menempatkan hasilnya tepat di tempat yang sama dengan asalnya.
  3. docker compose restart wordpress — container dihentikan lalu dinyalakan lagi, sehingga entrypoint WordPress berjalan sekali lagi dan menata kepemilikan berkas menjadi rapi milik www-data — ritual permission yang kita kenal dari Permission dan Ownership Volume.

Disaster Drill: Menghapus Segalanya dengan Sadar #

Sekarang ujian yang sesungguhnya. Backup yang belum pernah diuji restore-nya bukan backup — dan mengujinya menuntut keberanian menghapus data hidup. Kita jalankan down -v, perintah yang di Bagian 3 kita pasangi peringatan factory reset:

bash
$ docker compose down -v

Output yang Dihasilkan:

text
[+] Running 5/5
 ✔ 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
 ✔ Volume wordpress-dan-mysql_db-data         Removed                         0.4s
 ✔ Volume wordpress-dan-mysql_wp-content      Removed                         0.2s

Lima baris — dua container, satu network, dan dua Volume … Removed: dua belas tabel, dua tulisan, empat varian gambar, dan seluruh identitas situs barusan lenyap dari disk. Nyalakan ulang stack dengan volume kosong:

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                         12.6s
 ✔ Container wordpress-dan-mysql-wordpress-1  Started                         1.9s

Dua volume kosong tercipta kembali di belakang layar, dan db memakan 12,6 detik untuk healthy — volume kosong berarti inisialisasi MySQL diulang dari nol, persis seperti penyalaan pertama di Bagian 1; .env yang masih ada membuat gerbang :? lolos dan database wordpress beserta akunnya tercipta ulang. Bila browser dibuka sekarang, wizard instalasi akan menyambut lagi — situs menganggap dirinya baru lahir. Rangkaian restore berikut mengubah nasibnya:

bash
$ source .env && gunzip -c backup-wordpress-2026-10-13.sql.gz | docker compose exec -T db sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" wordpress'
$ docker compose exec -T wordpress sh -c 'rm -rf /var/www/html/wp-content/* && tar -xzf - -C /var/www/html' < backup-wp-content.tar.gz
$ docker compose restart wordpress

Dan pengadilan akhirnya adalah dua pemeriksaan yang sama dari Bagian 3:

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 |
+----+-----------------------------+---------------------+
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

Kedua keluaran itu sama persis dengan sebelum bencana — termasuk detik post_date dan menit modifikasi berkas, karena dump menyimpan nilai kolom apa adanya dan tar mempertahankan waktu modifikasi berkas. Pemeriksaan terakhir tidak berupa perintah: buka kembali halaman masuk di localhost:8090, isi username budi dengan sandinya — dashboard terbuka, karena baris budi di tabel wp_users ikut pulang bersama dump. Situs Toko Kopi Kita telah bangkit dari dua berkas di folder proyek.

Penutup Proyek #

Empat bagian berlalu dan proyek tanpa Dockerfile pertama kita menuntaskan lima janji:

  1. Stack dua service dari image resmi — wordpress:6.8-apache dan mysql:8.4 dirangkai 43 baris compose.yaml tanpa satu baris RUN pun, dengan urutan penyalaan yang dijaga gerbang service_healthy.
  2. Kredensial yang terkelola — .env.example menjadi template, :? menjadi gerbang fail-fast, .gitignore menjadi pagar, dan sandi tidak pernah menyentuh repositori maupun teks konfigurasi.
  3. Mekanisme yang terbukti, bukan dipercaya — WORDPRESS_DB_* kita telusuri sampai wp-config.php, $$ dijelaskan sampai ke siapa yang mengekspansi, dan least privilege dibuktikan dengan ERROR 1044.
  4. Persistensi yang lulus ujian — dua volume terpisah ditanam bukti, diuji siklus down–up, diperiksa dari luar lewat docker volume inspect.
  5. Backup yang layak dipercaya — dua alat untuk dua dunia data, otomasi cron dengan rotasi, dan disaster drill yang menghapus segalanya sebelum membangkitkannya kembali.

Perjalanan Section Proyek tinggal satu tiket lagi. Kelak di Proyek: Reverse Proxy Multi-Service, penerbitan port berubah menjadi urusan satu pihak: nginx berdiri sebagai satu pintu di depan dua service Python, keduanya berbicara lewat custom network Compose — dan satu-satunya port host yang diterbitkan adalah milik si proxy, yang di mesin latihan tetap berdiri di 8080. Semua kebiasaan yang kita tanam di sini — image terpin, service tanpa port yang tidak perlu, healthcheck sebelum kepercayaan — naik kelas mengikuti jumlah service yang harus dijaga.

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: