Situs Toko Kopi Kita sudah terinstal, tetapi di Bagian 1 kita sengaja melaju dan meninggalkan sisi db sebagai kotak yang hanya kita sapa sekilas. Kotak itu menyimpan tiga keputusan yang paling sering salah dipahami pemakai image MySQL: apa sebenarnya yang dikerjakan empat variabel MYSQL_*, mengapa healthcheck harus menulis dua tanda dolar, dan mengapa aplikasi tidak sekalian memakai akun root. Bagian ini membuka ketiganya — pertama lewat bedah konfigurasi, lalu lewat pembuktian langsung dari dalam container: kita masuk sebagai root, masuk sebagai akun aplikasi, dan menyaksikan sendiri betapa berbedanya pandangan dunia kedua akun itu.

Empat Variabel dengan Tiga Pekerjaan #

Sepenggal deklarasi service db — bagian yang kita lewati cepat di Bagian 1 — berbunyi:

yaml
  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

Empat variabel ini mengerjakan tiga pekerjaan pada momen yang sama: saat inisialisasi pertama volume db-data. Mari kita telaah satu per satu:

  1. MYSQL_ROOT_PASSWORD — kata sandi akun root, pengelola penuh server. Inilah satu-satunya variabel yang benar-benar wajib; entrypoint MySQL menolak bekerja tanpa salah satu dari tiga opsi sandi root yang kita kenal di Menjalankan MySQL. Nilainya datang dari .env lewat gerbang :? — galat required variable … wajib diisi lewat .env yang kita saksikan di Bagian 1 lahir dari baris ini.
  2. MYSQL_DATABASE: wordpress — nama database awal yang diciptakan saat inisialisasi. Satu baris ini menggantikan ritual manual CREATE DATABASE wordpress; setelah instalasi.
  3. MYSQL_USER: wordpress — akun aplikasi yang diciptakan sekaligus dengan database awal. Entrypoint tidak memberinya seluruh kuasa server, melainkan hak penuh hanya atas database wordpress.
  4. MYSQL_PASSWORD — pasangan sandi untuk akun aplikasi itu, juga dari .env. Perhatikan bahwa variabel yang sama dipakai oleh WORDPRESS_DB_PASSWORD di service wordpress — satu sumber kebenaran untuk sandi yang harus cocok di kedua sisi.

Kata kunci dari keempat baris itu adalah saat inisialisasi: variabel-variabel ini bukan setelan yang bisa diubah kapan saja. Kesesuaian lengkapnya dengan sisi WordPress terangkum dalam tabel berikut:

Sisi service wordpressSisi service dbNilai yang harus cocok
WORDPRESS_DB_HOSTnama service + portdb:3306
WORDPRESS_DB_NAMEMYSQL_DATABASEwordpress
WORDPRESS_DB_USERMYSQL_USERwordpress
WORDPRESS_DB_PASSWORDMYSQL_PASSWORDnilai dari .env

Healthcheck dengan Dolar Terpelihara #

Sepenggal kedua dari service db — blok yang paling padat materi di seluruh compose.yaml:

yaml
    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

Enam kunci di dalamnya bekerja sebagai satu sistem, dan dua di antaranya menyimpan jebakan $ yang layak dijelaskan tuntas:

  1. test dengan bentuk CMD-SHELL — probe dijalankan lewat /bin/sh -c di dalam container, sehingga sintaks shell hidup: penggabungan argumen, tanda kutip, dan terutama ekspansi variabel. Bandingkan dengan bentuk CMD murni: argumen diterima apa adanya tanpa pernah disentuh shell, sehingga teks $MYSQL_ROOT_PASSWORD akan dikirim sebagai sandi literal — huruf demi huruf — dan probe gagal selamanya. Pilihan antara keduanya kita bahas pada HEALTHCHECK dan Healthcheck dan Service Readiness.
  2. $$ di depan MYSQL_ROOT_PASSWORD — Compose menginterpolasi seluruh teks compose.yaml, dan $$ adalah satu-satunya cara menulis satu tanda dolar literal: setelah interpolasi, perintah yang sampai ke container berbunyi -p$MYSQL_ROOT_PASSWORD. Ekspansinya baru terjadi saat test berjalan, dikerjakan shell container yang mewarisi environment service — dan environment itulah yang berisi MYSQL_ROOT_PASSWORD hasil penyuntikan Compose dari .env. Dengan begitu sandi tidak pernah menjadi bagian dari teks compose.yaml, tidak pernah tampil di render docker compose config, dan tetap sampai ke mysqladmin.
  3. interval: 3s — probe berdenyut setiap 3 detik, sepuluh kali lebih rapat daripada bawaan 30 detik. Probe cepat dibutuhkan karena gerbang service_healthy menunggu putusannya: interval pendek memotong waktu tunggu WordPress.
  4. timeout: 3s — tiap probe hanya diberi 3 detik; mysqladmin yang menggantung dianggap gagal dan dihitung sebagai kegagalan berikutnya.
  5. retries: 20 — status unhealthy baru melekat setelah 20 kegagalan beruntun; dikali interval 3 detik, sistem ini memberi server sekitar satu menit masa pemulihan sebelum dinyatakan sakit.
  6. start_period: 30s — masa tenggang pertama: kegagalan pada 30 detik awal tidak dihitung sebagai retry. Inilah kunci bagi MySQL, karena inisialisasi volume kosong memang pekerjaan berat — menciptakan tabel sistem, database wordpress, dan user aplikasi memakan waktu belasan detik sebelum server sanggup menjawab ping pertama.

Least Privilege yang Terbukti #

Mengapa repot-repot membuat akun wordpress kalau akun root sudah ada? Karena prinsip least privilege: aplikasi hanya diberi tepat apa yang dibutuhkannya, sehingga celah pada aplikasi tidak otomatis menjadi celah pada seluruh server. Klaim ini bisa kita buktikan, bukan sekadar percaya. Masuk dulu sebagai root — sandinya nilai MYSQL_ROOT_PASSWORD di .env kita:

bash
$ docker compose exec db mysql -uroot -p

Output yang Dihasilkan:

text
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 14
Server version: 8.4.11 MySQL Community Server - GPL

Copyright (c) 2000, 2026, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| wordpress          |
+--------------------+
5 rows in set (0.00 sec)

mysql> quit
Bye

Banner Server version: 8.4.11 mengonfirmasi seri LTS yang kita pilih lewat tag mysql:8.4. Sebagai root, lima database terlihat: empat milik sistem (mysql, information_schema, performance_schema, sys) dan satu milik aplikasi kita. Sekarang ganti kursi — keluar dan masuk kembali sebagai akun aplikasi (sandi MYSQL_PASSWORD di .env):

bash
$ docker compose exec db mysql -uwordpress -p

Output yang Dihasilkan:

text
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 16
Server version: 8.4.11 MySQL Community Server - GPL
...
mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| wordpress          |
+--------------------+
2 rows in set (0.00 sec)

mysql> USE mysql;
ERROR 1044 (42000): Access denied for user 'wordpress'@'%' to database 'mysql'
mysql> USE wordpress;
Database changed
mysql> SHOW TABLES;
+---------------------------+
| Tables_in_wordpress       |
+---------------------------+
| wp_commentmeta            |
| wp_comments               |
| wp_links                  |
| wp_options                |
| wp_postmeta               |
| wp_posts                  |
| wp_term_relationships     |
| wp_term_taxonomy          |
| wp_termmeta               |
| wp_terms                  |
| wp_usermeta               |
| wp_users                  |
+---------------------------+
12 rows in set (0.01 sec)

mysql> quit
Bye

Perbedaan pandangan kedua akun ini adalah bukti least privilege yang bekerja:

  1. Root melihat lima database, aplikasi hanya dua — akun wordpress hanya mengenal wordpress miliknya ditambah information_schema yang memang terbaca semua user lewat hak metadata.
  2. USE mysql ditolak dengan ERROR 1044 — database sistem, rumah tabel grant dan akun, tertutup untuk akun aplikasi. Kalau kelak celah keamanan ditemukan pada WordPress, penyerang yang membajak koneksi ini tetap terkurung di satu database.
  3. Dua belas tabel wp_ terlihat di database wordpress — sidik jari instalasi WordPress standar: post, comment, option, user, dan keluarga taksonomi. Tabel-tabel inilah yang akan jadi objek pembuktian persistensi di Bagian 3 dan objek backup di Bagian 4.

Data aplikasi kini hidup dalam database milik akun yang terbatas kuasanya — tetapi data itu sendiri masih menyimpan satu kerentanan: ia hanya ada di dalam volume di satu disk. Apakah volume db-data dan wp-content benar-benar menepati janji persistensinya bila container dibuang, dan mengapa keduanya sengaja dipisah? Bagian 3: Volume dan Persistence mengujinya dengan bukti yang kita tanam sendiri.

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: