Auto-Deploy WordPress Docker + MariaDB Otomatis
Back to articles

Auto-Deploy WordPress Docker + MariaDB Otomatis

Deploy WordPress satu perintah: Docker Compose plus MariaDB, database dan user dibuat otomatis, rule nginx langsung terpasang di subdomain. Plus fix runtime.

Deploy WordPress manual itu 7 langkah yang selalu sama: buat database, buat user, grant privilege, tulis docker-compose, jalankan container, pasang nginx reverse proxy, reload nginx. Langkah yang paling sering bikin gagal adalah membuat database dan user — karena ada 4 SQL statement yang harus tepat, dan WordPress tidak akan bilang "database tidak ada" dengan jelas. Yang kamu lihat adalah layar putih atau error 500 yang tidak menjelaskan apa-apa.

Artikel ini membongkar bagaimana saya auto-deploy WordPress dengan Docker dan MariaDB sampai satu perintah: database dan user dibuat otomatis, nginx rule langsung terpasang, situs hidup di subdomain. Ini bagian dari sistem AI agent yang deploy website otomatis yang lebih besar.

TL;DR:

  • Satu perintah ./scripts/deploy.sh wordpress <nama> membuat database, user, container, dan rule nginx otomatis
  • MariaDB shared instance (bukan per container) menghemat RAM — trade-off: semua situs bergantung satu instance
  • Dua bug runtime yang makan waktu debugging: ALTER USER untuk sync password, dan wait loop sebelum nginx reload (hindari 502)
  • WordPress install wizard tetap manual setelah container hidup

Prasyarat

Versi software yang saya pakai, diambil dari mesin nyata:

$ docker --version
Docker version 29.6.2

$ docker compose version
Docker Compose version v2.x

$ nginx -v
nginx version: nginx/1.x (di dalam container site-server)

$ mysqld --version
MariaDB SQL server (di dalam container mariadb)

Yang harus sudah jalan sebelum deploy: Docker Desktop, container MariaDB (shared instance), container nginx site-server, dan Cloudflare Tunnel. Semua ini bagian dari infrastruktur self-hosting yang lebih luas.

Bentuk akhir yang kita tuju

Satu perintah, output yang diharapkan, struktur direktori hasil:

$ ./scripts/deploy.sh wordpress kopi-surgent

# Output yang diharapkan:
Creating db_kopi-surgent...
User db_kopi-surgent created
Starting container...
SITE 'kopi-surgent' TERDEPLOY

Struktur direktori setelah deploy sukses:

/app-stacks/kopi-surgent/
├── docker-compose.yml    # compose file khusus situs ini
├── data/
│   └── wp-content/       # volume persistent WordPress
└── app/                  # kosong untuk WordPress (image sudah lengkap)

Database db_kopi-surgent dibuat di MariaDB shared. Container kopi-surgent jalan di network Docker internal. Nginx proxy rule mengarahkan kopi-surgent.jayax.dev ke port container.

Template docker-compose untuk WordPress

Setiap situs WordPress dapat compose file sendiri di /app-stacks/<nama>/docker-compose.yml. Image WordPress official dipakai, dengan koneksi ke MariaDB shared:

# /app-stacks/kopi-surgent/docker-compose.yml
services:
  kopi-surgent:
    image: wordpress:latest
    container_name: kopi-surgent
    restart: unless-stopped
    environment:
      WORDPRESS_DB_HOST: mariadb:3306
      WORDPRESS_DB_USER: db_kopi-surgent
      WORDPRESS_DB_PASSWORD: <DB_PASSWORD>
      WORDPRESS_DB_NAME: db_kopi-surgent
    volumes:
      - ./data/wp-content:/var/www/html/wp-content
    deploy:
      resources:
        limits:
          memory: 512M
    networks:
      - default

Kenapa MariaDB di luar compose per-situs (shared instance)? Karena setiap container MariaDB baru makan 200-400MB RAM baseline. Dengan shared instance, satu proses MariaDB melayani semua database — db_kopi-surgent, db_blog, db_portfolio — di port yang sama. Konsekuensinya: semua situs bergantung pada satu instance MariaDB. Kalau MariaDB down, semua situs down. Tapi untuk skala homelab dengan 2-3 situs aktif, trade-off ini masuk akal.

Network Docker default digunakan supaya container situs bisa resolve hostname mariadb ke IP container MariaDB. Tidak perlu expose port MariaDB ke host.

Membuat database dan user secara otomatis

Bagian yang paling rentan error saat deploy manual. Skrip deploy menjalankan 4 SQL statement ini melalui docker exec ke container MariaDB:

-- Konvensi penamaan: db_<nama_situs>
-- Idempoten: IF NOT EXISTS mencegah error kalau deploy nama yang sama dua kali

CREATE DATABASE IF NOT EXISTS db_kopi-surgent;
CREATE USER IF NOT EXISTS 'db_kopi-surgent'@'%' IDENTIFIED BY '<DB_PASSWORD>';
GRANT ALL PRIVILEGES ON db_kopi-surgent.* TO 'db_kopi-surgent'@'%';
FLUSH PRIVILEGES;

Konvensi penamaan db_<nama> penting karena: (1) nama database selalu match nama container, jadi troubleshooting mudah; (2) user database sama dengan nama database, jadi tidak perlu lookup tambahan; (3) saat teardown, satu nama cukup untuk identifikasi semua resource.

Kenapa IF NOT EXISTS krusial? Bayangkan deploy kopi-surgent pertama kali — berhasil. Lalu sesuatu terjadi, perlu re-deploy. Tanpa IF NOT EXISTS, CREATE DATABASE akan error "database already exists", dan deploy berhenti di tengah. Dengan IF NOT EXISTS, deploy lanjut — database yang ada dipakai ulang, user di-update (lihat bagian ALTER USER di bawah).

Memasang rule nginx reverse proxy

Setelah container hidup, nginx butuh tahu ke mana harus meneruskan traffic untuk subdomain baru. Skrip deploy menulis server block ke nginx config:

# Server block per situs, di-include oleh nginx site-server
server {
    listen 80;
    server_name kopi-surgent.jayax.dev;

    location / {
        proxy_pass http://kopi-surgent:80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WordPress butuh upload file besar
        client_max_body_size 64M;
    }
}

Perintah reload nginx:

docker exec site-server nginx -s reload

Yang penting: jangan hardcode port container. Docker compose bisa assign port dinamis. Proxy pass menggunakan nama container (http://kopi-surgent:80) di network Docker internal, bukan port yang di-publish ke host. Ini kenapa semua container harus berada di network Docker yang sama dengan nginx site-server.

Dua bug runtime yang menghabiskan waktu saya

ALTER USER: user sudah ada dengan password beda

Gejala: WordPress balas "Error establishing a database connection" padahal database dan user sudah dibuat.

Penyebab: User db_kopi-surgent sudah ada dari deploy sebelumnya, tapi dengan password yang berbeda dari yang tertulis di compose file. WordPress mencoba connect dengan password baru, MariaDB menolak.

Error asli yang muncul di log MariaDB:

[Warning] Access denied for user 'db_kopi-surgent'@'x.x.x.x' (using password: YES)

Fix: Skrip deploy selalu menjalankan ALTER USER setelah CREATE USER IF NOT EXISTS:

-- Selalu dijalankan, bahkan kalau user sudah ada
ALTER USER 'db_kopi-surgent'@'%' IDENTIFIED BY '<DB_PASSWORD>';
FLUSH PRIVILEGES;

Dengan ALTER USER, password user selalu di-update ke nilai terbaru yang tertulis di compose file. Tidak peduli user baru dibuat atau sudah ada dari deploy lama.

Urutan operasi: nginx reload sebelum container siap

Gejala: Browser dapat 502 Bad Gateway di kopi-surgent.jayax.dev padahal deploy "berhasil".

Penyebab: nginx di-reload sebelum container WordPress selesai start. Container WordPress butuh 10-30 detik untuk initialization pertama (extract WordPress files, setup config). Saat nginx mulai meneruskan traffic, container belum ready — nginx dapat connection refused, balas 502.

Fix: Skrip deploy menunggu container siap sebelum reload nginx:

# Tunggu container WordPress sampai health check lewat
echo "Menunggu container siap..."
for i in $(seq 1 30); do
    HTTP_CODE=$(docker exec site-server curl -s -o /dev/null -w "%{http_code}" \
        http://kopi-surgent:80/ 2>/dev/null || echo "000")
    if [ "$HTTP_CODE" != "000" ] && [ "$HTTP_CODE" != "502" ]; then
        echo "Container siap (HTTP $HTTP_CODE)"
        break
    fi
    sleep 2
done

# Baru reload nginx setelah container confirmed ready
docker exec site-server nginx -s reload

Loop ini mencoba HTTP request ke container setiap 2 detik, maksimum 60 detik. Begitu container balas dengan kode selain 000 (connection refused) dan 502 (bad gateway), nginx di-reload.

Verifikasi hasil deploy

Setelah deploy selesai, verifikasi dengan tiga langkah:

# 1. Cek container jalan
docker ps --filter name=kopi-surgent --format "{{.Names}} {{.Status}}"
# Expected: kopi-surgent Up X minutes

# 2. Cek HTTP response (local, lewat nginx)
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: kopi-surgent.jayax.dev" http://site-server/
# Expected: 302 (redirect ke wp-admin/install.php untuk fresh install)

# 3. Cek publik (lewat Cloudflare Tunnel)
curl -s -o /dev/null -w "%{http_code}\n" https://kopi-surgent.jayax.dev
# Expected: 302 atau 200

WordPress fresh install akan 302-redirect ke wp-admin/install.php. Itu normal — user harus menjalankan install wizard (pilih bahasa, set judul, buat admin user). Setelah wizard selesai, homepage balas 200.

Membongkar kembali (teardown)

Teardown yang benar: hapus rule nginx dulu, reload, baru stop container. Kalau dibalik, traffic masih masuk ke container yang sedang down — user dapat 502 selama window teardown.

# 1. Hapus nginx rule, reload
rm /etc/nginx/sites/kopi-surgent.conf
docker exec site-server nginx -s reload

# 2. Stop container, hapus container
cd /app-stacks/kopi-surgent && docker compose down

# 3. Drop database dan user (DESTRUKTIF!)
docker exec mariadb mysql -u root -p<ROOT_PASSWORD> -e \
    "DROP DATABASE IF EXISTS db_kopi-surgent; \
     DROP USER IF EXISTS 'db_kopi-surgent'@'%'; \
     FLUSH PRIVILEGES;"

# 4. Hapus direktori app-stack
rm -rf /app-stacks/kopi-surgent

Peringatan: DROP DATABASE dan docker compose down -v menghapus data permanen. Pastikan tidak ada data yang perlu disimpan sebelum teardown.

Skrip deploy versi bridge menyediakan ini lewat wsl-deploy down <nama>. Container dihentikan, rule nginx dihapus, database di-drop. Folder /app-stacks/<nama>/ tetap ada (data tidak hilang) sampai dihapus manual.

Tabel troubleshooting

  • Gejala: Error establishing a database connection — Penyebab: password user tidak match compose. Fix: re-deploy (ALTER USER akan sync password).
  • Gejala: 502 Bad Gateway — Penyebab: container belum siap saat nginx reload. Fix: tunggu, atau re-deploy (skrip sudah punya wait loop).
  • Gejala: 404 Not Found di subdomain — Penyebab: rule nginx tidak terpasang atau DNS belum propagate. Fix: cek docker exec site-server nginx -T | grep <nama>, cek Cloudflare DNS.
  • Gejala: WordPress redirect loop — Penyebab: X-Forwarded-Proto header tidak diteruskan nginx. Fix: pastikan proxy_set_header ada di server block.
  • Gejala: Upload file gagal (413 Request Entity Too Large) — Penyebab: client_max_body_size terlalu kecil. Fix: set client_max_body_size 64M di nginx server block.
  • Gejala: Container restart terus-menerus — Penyebab: memory limit 512M kurang untuk WordPress dengan plugin berat. Fix: tambah limit di compose file.

Langkah berikutnya

Artikel ini fokus pada WordPress. Skrip deploy yang sama bisa menangani Laravel dan Next.js — lihat satu skrip deploy untuk tiga framework. Kalau mau memahami bagaimana AI agent memanggil skrip ini secara otomatis, baca AI agent yang deploy website otomatis. Untuk infrastruktur self-hosting lengkapnya, lihat self-host banyak website di satu server.

FAQ

Apakah database dan user benar-benar dibuat otomatis tanpa intervensi manual?

Ya. Skrip deploy menjalankan CREATE DATABASE, CREATE USER, GRANT, dan ALTER USER secara otomatis via docker exec ke container MariaDB. Password di-auto-generate dan disimpan. Satu-satunya langkah manual adalah WordPress install wizard setelah container hidup.

Kenapa pakai satu instance MariaDB shared, bukan MariaDB per container?

Setiap container MariaDB baru makan 200-400MB RAM. Host punya ~11GB untuk WSL2. Dengan 2-3 situs aktif, shared instance jauh lebih efisien. Trade-off: semua situs bergantung pada satu instance MariaDB.

Bagaimana kalau nama situs sudah dipakai?

Skrip menggunakan IF NOT EXISTS di SQL, jadi deploy nama yang sama akan update (ALTER USER sync password), bukan error. Container lama dihentikan dan diganti. Data di wp-content/ volume tetap ada.

Bisakah deploy WordPress tanpa Cloudflare Tunnel?

Cloudflare Tunnel adalah jalur publik. Tanpa tunnel, situs hanya accessible dari local network. Bisa diganti dengan port forwarding + DDNS, tapi itu mengekspos IP publik rumah. Detail trade-off di artikel self-hosting.

Kesimpulan

Auto-deploy WordPress dengan Docker dan MariaDB menyederhanakan 7 langkah manual menjadi satu perintah. Bagian tersulit bukan Docker — tapi sinkronisasi database, container, dan nginx dalam urutan yang benar. Dua bug runtime (ALTER USER dan urutan nginx reload) menghabiskan waktu debugging terbanyak, dan keduanya adalah masalah urutan/sinkronisasi, bukan masalah teknis yang rumit.

Kalau sistem ini menarik, lanjut ke satu skrip deploy untuk WordPress, Laravel, dan Next.js — di sana saya membongkar bagaimana skrip yang sama menangani tiga framework berbeda.

More Articles