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:
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/mysqlEmpat variabel ini mengerjakan tiga pekerjaan pada momen yang sama: saat inisialisasi pertama volume db-data. Mari kita telaah satu per satu:
MYSQL_ROOT_PASSWORD— kata sandi akunroot, 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.envlewat gerbang:?— galatrequired variable … wajib diisi lewat .envyang kita saksikan di Bagian 1 lahir dari baris ini.MYSQL_DATABASE: wordpress— nama database awal yang diciptakan saat inisialisasi. Satu baris ini menggantikan ritual manualCREATE DATABASE wordpress;setelah instalasi.MYSQL_USER: wordpress— akun aplikasi yang diciptakan sekaligus dengan database awal. Entrypoint tidak memberinya seluruh kuasa server, melainkan hak penuh hanya atas databasewordpress.MYSQL_PASSWORD— pasangan sandi untuk akun aplikasi itu, juga dari.env. Perhatikan bahwa variabel yang sama dipakai olehWORDPRESS_DB_PASSWORDdi servicewordpress— 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 wordpress | Sisi service db | Nilai yang harus cocok |
|---|---|---|
WORDPRESS_DB_HOST | nama service + port | db:3306 |
WORDPRESS_DB_NAME | MYSQL_DATABASE | wordpress |
WORDPRESS_DB_USER | MYSQL_USER | wordpress |
WORDPRESS_DB_PASSWORD | MYSQL_PASSWORD | nilai dari .env |
Healthcheck dengan Dolar Terpelihara #
Sepenggal kedua dari service db — blok yang paling padat materi di seluruh compose.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: 30sEnam kunci di dalamnya bekerja sebagai satu sistem, dan dua di antaranya menyimpan jebakan $ yang layak dijelaskan tuntas:
testdengan bentukCMD-SHELL— probe dijalankan lewat/bin/sh -cdi dalam container, sehingga sintaks shell hidup: penggabungan argumen, tanda kutip, dan terutama ekspansi variabel. Bandingkan dengan bentukCMDmurni: argumen diterima apa adanya tanpa pernah disentuh shell, sehingga teks$MYSQL_ROOT_PASSWORDakan dikirim sebagai sandi literal — huruf demi huruf — dan probe gagal selamanya. Pilihan antara keduanya kita bahas pada HEALTHCHECK dan Healthcheck dan Service Readiness.$$di depanMYSQL_ROOT_PASSWORD— Compose menginterpolasi seluruh tekscompose.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 berisiMYSQL_ROOT_PASSWORDhasil penyuntikan Compose dari.env. Dengan begitu sandi tidak pernah menjadi bagian dari tekscompose.yaml, tidak pernah tampil di renderdocker compose config, dan tetap sampai kemysqladmin.interval: 3s— probe berdenyut setiap 3 detik, sepuluh kali lebih rapat daripada bawaan 30 detik. Probe cepat dibutuhkan karena gerbangservice_healthymenunggu putusannya: interval pendek memotong waktu tunggu WordPress.timeout: 3s— tiap probe hanya diberi 3 detik;mysqladminyang menggantung dianggap gagal dan dihitung sebagai kegagalan berikutnya.retries: 20— statusunhealthybaru melekat setelah 20 kegagalan beruntun; dikali interval 3 detik, sistem ini memberi server sekitar satu menit masa pemulihan sebelum dinyatakan sakit.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, databasewordpress, 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:
$ docker compose exec db mysql -uroot -pOutput yang Dihasilkan:
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
ByeBanner 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):
$ docker compose exec db mysql -uwordpress -pOutput yang Dihasilkan:
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
ByePerbedaan pandangan kedua akun ini adalah bukti least privilege yang bekerja:
- Root melihat lima database, aplikasi hanya dua — akun
wordpresshanya mengenalwordpressmiliknya ditambahinformation_schemayang memang terbaca semua user lewat hak metadata. USE mysqlditolak denganERROR 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.- Dua belas tabel
wp_terlihat di databasewordpress— 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 #
- MySQL — Docker Hub
— variabel
MYSQL_*, perilaku inisialisasi, dan pola healthcheckmysqladmin ping. - Services — Compose file reference
— kunci
healthchecklengkap denganinterval,timeout,retries, danstart_period.
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: