Proyek-proyek sebelumnya selalu membuka dengan berkas sumber aplikasi, lalu Dockerfile yang mengemasnya. Kali ini urutannya terbalik dan lebih pendek: tidak ada aplikasi yang kita tulis dan tidak ada resep yang kita bangun — hanya satu berkas compose.yaml berisi 43 baris yang merangkai dua image resmi menjadi satu stack utuh. Seluruh keputusan arsitektur hidup di berkas itu: image versi berapa, variabel apa yang disuntikkan, data apa yang dipersist-kan, dan service mana yang boleh menyala lebih dulu. Kita bedah berkas itu baris demi baris, mulai dari sisi WordPress, karena sisi itulah yang terlihat pertama kali oleh pengunjung situs.

compose.yaml untuk Dua Service #

Buat berkas compose.yaml di akar folder proyek dengan isi berikut — persis seperti ini, karena kita akan membedahnya sepotong-sepotong pada bagian-bagian selanjutnya:

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:

Bentuk besarnya akrab dari Struktur compose.yaml: satu blok services berisi dua service (db dan wordpress), satu blok volumes yang mendeklarasikan dua volume bernama, dan tidak ada satu kunci build pun — Compose cukup menarik kedua image dari registry. Mari kita telaah enam kunci pada service wordpress:

  1. image: wordpress:6.8-apache — image resmi WordPress versi 6.8, dipin pada tag eksplisit alih-alih latest — kebiasaan yang kita tanam sejak Memahami Image Tag. Suffix -apache memilih varian yang menyatukan Apache dan mod_php dalam satu image; itulah varian paling cocok untuk proyek tanpa Dockerfile, karena satu container sudah melayani HTTP tanpa perlu service web terpisah seperti pada varian -fpm.
  2. environment dengan empat WORDPRESS_DB_* — alamat, user, sandi, dan nama database yang dipakai WordPress. Empat variabel ini adalah jembatan resmi ke wp-config.php; mekanisme persisnya kita bedah pada bagian berikutnya. Perhatikan bahwa WORDPRESS_DB_PASSWORD diambil dari interpolasi ${MYSQL_PASSWORD:?…} — variabel yang sama dengan sisi db, sehingga kedua service tidak mungkin salah nyambung.
  3. volumes: wp-content — satu-satunya path WordPress yang dipersist-kan: /var/www/html/wp-content, rumah tema, plugin, dan unggahan. Berkas inti WordPress (wp-admin, wp-includes, seluruh core) sengaja tidak dipersist-kan karena ia milik image — wujud pemisahan program dan data yang kita bahas pada Konsep Persistence.
  4. ports: "8090:80" — menerbitkan port host 8090 menuju port 80 container. Angka 8090 dipilih karena 8080 sering sudah ditempati stack latihan lain di mesin yang sama. Tanda kutipnya bukan hiasan — peringatannya kita pasang tepat di bawah ini.
  5. depends_on dengan condition: service_healthy — WordPress tidak menyala begitu db dinyalakan, melainkan menunggu hingga db lulus healthcheck. Inilah gerbang kesiapan yang kita kenal dari depends_on dan Startup Order: urutan start yang sebenarnya, bukan sekadar urutan eksekusi.
  6. restart: unless-stopped — kebijakan yang sama dengan proyek sebelumnya: container bangkit otomatis saat daemon atau host menyala ulang, tetapi patuh berhenti bila kita yang menghentikannya secara sengaja — nuansa yang kita bedah pada Restart Policy.

Satu keputusan yang kelihatan seperti kekurangan justru disengaja: service db tidak memiliki kunci ports sama sekali. MySQL hanya hidup di dalam network project yang dibuat Compose, dan satu-satunya jalur menujunya adalah nama service — WordPress menghubungi db:3306 lewat DNS embedded yang bekerja pada user-defined network, sebagaimana kita buktikan pada Container DNS. Dari luar host, port 3306 tidak diterbitkan apa pun: database tidak bisa disambung oleh internet atau bahkan oleh proses host sembarangan. Mengurangi permukaan yang terbuka sejak dari desain adalah bentuk paling murah dari Prinsip Keamanan Container.

Jembatan WORDPRESS_DB_* menuju wp-config.php #

Instalasi WordPress klasik dimulai dengan mengedit wp-config.php secara manual — mengisi DB_HOST, DB_USER, DB_PASSWORD, DB_NAME dengan tangan. Image resmi WordPress memotong ritual itu lewat entrypoint-nya. Setiap kali container menyala, skrip entrypoint melakukan dua pekerjaan berurutan: menyalin isi /usr/src/wordpress ke /var/www/html (isi wp-content yang sudah ada dipertahankan, tidak ditimpa), lalu — bila wp-config.php belum ada — menyalin template wp-config-docker.php menjadi wp-config.php. Template itu tidak memuat sandi apa pun secara tetap; ia membaca keempat variabel WORDPRESS_DB_* dari environment container lewat helper getenv_docker() — pasangan variabel dan nilai cadangannya — lalu memetakannya ke konstanta yang dibaca WordPress:

Variabel environmentKonstanta di wp-config.phpNilai pada stack kita
WORDPRESS_DB_HOSTDB_HOSTdb:3306
WORDPRESS_DB_USERDB_USERwordpress
WORDPRESS_DB_PASSWORDDB_PASSWORDnilai MYSQL_PASSWORD dari .env
WORDPRESS_DB_NAMEDB_NAMEwordpress

Akibat pemetaan ini terasa begitu kita membuka wizard: halaman pengisian koneksi database yang biasa menjadi langkah pertama instalasi tidak pernah muncul, karena wp-config.php sudah ada dan sudah terisi. Konfigurasi database berpindah dari ritual manual di dalam container menjadi deklarasi di compose.yaml — persis filosofi yang kita bangun sejak Pengantar Docker Compose.

Kredensial dalam .env.example #

Sekarang bagian yang belum kita miliki: nilai dua sandi. Berkas compose.yaml tidak memuatnya — ia hanya meminta variabel MYSQL_ROOT_PASSWORD dan MYSQL_PASSWORD dengan gerbang :?, dan penyedia nilainya adalah berkas .env di root proyek. Template-nya disimpan sebagai .env.example:

ini
# Salin berkas ini menjadi .env lalu ganti kedua nilainya:
#   cp .env.example .env
# Berkas .env berisi kredensial dan tidak boleh di-commit (lihat .gitignore).

# Password akun root MySQL.
MYSQL_ROOT_PASSWORD=ganti-root-password-ini

# Password user aplikasi "wordpress" yang dibuat otomatis oleh
# entrypoint MySQL saat volume db-data masih kosong.
MYSQL_PASSWORD=ganti-password-wordpress-ini

Hanya dua variabel yang diminta — bukan empat — karena MYSQL_DATABASE dan MYSQL_USER tidaklah rahasia: nilainya wordpress tercetak polos di compose.yaml. Sandi adalah satu-satunya bagian yang berpindah ke .env, sesuai prinsip satu berkas per rahasia yang kita rinci di Environment dan env File. Gerbang pada nilainya dibangun oleh pola ${MYSQL_PASSWORD:?pesan} yang kita bedah pada Variable Interpolation: tanda :? berarti “bila variabel kosong atau tidak ada, gagalkan perintah dan cetak pesan ini”. Gerbang itu bisa kita saksikan menembak kapan saja setelah .env tersalin — cukup sembunyikan sementara berkasnya lalu minta Compose merender konfigurasi:

bash
$ mv .env .env.simpan
$ docker compose config

Output yang Dihasilkan:

text
required variable MYSQL_ROOT_PASSWORD is missing a value: MYSQL_ROOT_PASSWORD wajib diisi lewat .env
bash
$ mv .env.simpan .env

Pesan galatnya memuat dua hal yang kita tulis sendiri: nama variabel dan kalimat petunjuknya. Gerbang kembar pada MYSQL_PASSWORD menembak dengan pesan yang serupa bila salah satu saja kosong. Gagal di sini berarti gagal sebelum satu container pun tersentuh — jauh lebih baik daripada stack yang menyala dengan sandi kosong lalu mati lima menit kemudian dengan galat koneksi yang menjauh dari akar masalah. Karena berisi kredensial, .env tidak boleh masuk repositori — tugas yang dipegang .gitignore:

text
# Berkas .env berisi kredensial — jangan pernah di-commit.
.env

Menyalakan Stack #

Ritual penyalaan cuma dua langkah: salin template menjadi .env, ganti kedua nilainya dengan sandi pilihan kita, lalu nyalakan stack. Pastikan tidak ada stack lain yang sedang menempati port 8090 di host — bila sisa latihan masih menyala, bongkar dulu dengan docker compose down dari foldernya:

bash
$ cp .env.example .env
$ docker compose up -d

Output yang Dihasilkan:

text
[+] Pulling 2/2
 ✔ db Pulled                                           21.6s
 ✔ wordpress Pulled                                    13.1s
[+] Running 3/3
 ✔ Network wordpress-dan-mysql_default        Created                         0.2s
 ✔ Container wordpress-dan-mysql-db-1         Healthy                         11.4s
 ✔ Container wordpress-dan-mysql-wordpress-1  Started                         1.9s

Mari kita telaah enam baris kemajuan di atas:

  1. [+] Pulling 2/2 — kedua image resmi ditarik pada pemakaian pertama; keduanya berukuran ratusan megabyte, dengan db paling lama karena image MySQL membawa server lengkap di atas basisnya. Di mesin yang sudah pernah menarik salah satunya — misalnya dari Chapter Menjalankan MySQL — Compose hanya menarik yang hilang, sesuai kebijakan missing bawaan yang kita kenal dari Image, Build, dan Pull.
  2. Network … Created — network wordpress-dan-mysql_default tercipta otomatis dengan nama folder sebagai prefiks; di sinilah nama db menjadi alamat DNS yang sah.
  3. db … Healthy dalam 11,4 detik — angka paling penting: volume kosong memaksa entrypoint MySQL melakukan inisialisasi penuh (tabel sistem, database wordpress, user aplikasi), dan Compose menunggu hingga probe mysqladmin ping lulus sebelum melanjutkan.
  4. wordpress … Started setelahnya — baris ini hanya tercipta karena gerbang service_healthy terbuka; tanpa healthcheck, WordPress akan menyala bersamaan dengan db dan kehilangan beberapa detik pertamanya untuk galat koneksi.

Status akhirnya kita baca dalam satu pandangan lewat docker compose ps:

bash
$ docker compose ps

Output yang Dihasilkan:

text
NAME                            IMAGE                COMMAND                  SERVICE     CREATED         STATUS                    PORTS
wordpress-dan-mysql-db-1        mysql:8.4            "docker-entrypoint.s…"   db          3 minutes ago   Up 3 minutes (healthy)    3306/tcp
wordpress-dan-mysql-wordpress-1 wordpress:6.8-apache "docker-entrypoint.s…"   wordpress   3 minutes ago   Up 3 minutes              0.0.0.0:8090->80/tcp

Dua baris ini menegaskan dua keputusan desain kita. Pertama, (healthy) pada db — probe masih berdenyut setiap 3 detik dan gerbangnya tetap terjaga. Kedua, kolom PORTS: WordPress menerbitkan 0.0.0.0:8090->80/tcp, sedangkan db hanya menampilkan 3306/tcp — port tersebut terekspose oleh image tetapi tidak dipublikasikan; tidak ada pemetaan ke host, sehingga satu-satunya pintu masuk ke MySQL adalah lewat network internal.

Wizard Instalasi dalam Empat Langkah #

Buka browser ke localhost:8090 dan wizard instalasi menyambut dalam empat langkah pendek:

  1. Pilih bahasa — daftar bahasa WordPress lengkap hadir; pilih Bahasa Indonesia lalu klik Lanjut.
  2. Identitas situs — isi judul situs Toko Kopi Kita, username admin budi, password kuat (WordPress menawarkan generatornya), dan alamat email semisal [email protected].
  3. Klik Install WordPress — beberapa detik kemudian muncul halaman sukses WordPress telah sukses terinstal.
  4. Klik Log In — masuk dengan akun budi dan dashboard wp-admin terbuka lengkap.

Langkah yang tidak muncul adalah halaman koneksi database — biasanya wizard klasik meminta nama database, user, sandi, dan host di layar pertama. Di stack kita layar itu dilewati total karena wp-config.php sudah ditulis entrypoint dari keempat WORDPRESS_DB_*; database sudah terisi koneksi yang benar sejak container pertama menyala. Dalam waktu kurang dari satu menit setelah docker compose up -d, situs Toko Kopi Kita sudah hidup penuh dengan akun admin miliknya.

Stack yang barusan kita lahirkan ini masih menyimpan pertanyaan yang belum terjawab: apa sebenarnya pekerjaan masing-masing variabel MYSQL_*, mengapa healthcheck memerlukan dua tanda dolar, dan mengapa akun aplikasi berbeda dari root. Bagian 2: Mengonfigurasi Database menjawab ketiganya — lengkap dengan pembuktian least privilege langsung dari dalam container database.

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: