Satu Skrip Deploy: WordPress, Laravel, Next.js
Back to articles

Satu Skrip Deploy: WordPress, Laravel, Next.js

Rancang satu skrip deploy untuk WordPress, Laravel, dan Next.js — template compose per framework, pembuatan database otomatis, sampai teardown yang bersih.

Awalnya ada tiga skrip deploy terpisah: deploy-wordpress.sh, deploy-laravel.sh, deploy-nextjs.sh. Tiga skrip terpisah berarti tiga tempat untuk lupa memperbaiki bug yang sama. Saat saya menemukan bug ALTER USER (password tidak sync), saya harus fix di tiga tempat — dan tentu saya lupa salah satunya. Solusinya: satu skrip deploy untuk WordPress, Laravel, dan Next.js.

Artikel ini membongkar bagaimana skrip itu dirancang: apa yang sama antar framework, apa yang berbeda, dan bagaimana abstraksi yang benar membuat penambahan framework keempat trivial.

TL;DR:

  • Satu skrip deploy untuk WordPress, Laravel, dan Next.js menggantikan tiga skrip terpisah — satu bug fix berlaku ke semua framework
  • Shared logic: pembuatan database, compose generation, nginx rule, teardown. Per-framework: image, env, build step, volume
  • Interface minimal tiga command: deploy <framework> <nama>, down <nama>, list
  • Menambah framework keempat = template baru + satu case di switch, bukan rewrite

Yang berbeda dan yang sama antar framework

Sebelum menyatukan tiga skrip, saya petakan apa yang unique dan apa yang shared. Berikut perbandingan lengkap:

WordPress. Butuh database: ya (MariaDB). Build step: tidak (image official sudah lengkap). Port default: 80 (Apache di dalam image). Volume persist: wp-content/ (uploads, themes, plugins). Env wajib: WORDPRESS_DB_HOST, WORDPRESS_DB_USER, WORDPRESS_DB_PASSWORD, WORDPRESS_DB_NAME. Health check: HTTP 302 (redirect ke install.php untuk fresh) atau HTTP 200.

Laravel. Butuh database: ya (MariaDB atau MySQL). Build step: ya (composer install, php artisan key:generate, php artisan migrate). Port default: 9000 (php-fpm) atau 8000 (php artisan serve). Volume persist: storage/ (logs, cache, uploads). Env wajib: DB_DATABASE, DB_USERNAME, DB_PASSWORD, APP_KEY, APP_URL. Health check: HTTP 200 dari route /up atau /health.

Next.js. Butuh database: tidak (stateless frontend, atau connect ke external API). Build step: ya (npm install, npm run build). Port default: 3000. Volume persist: tidak (stateless). Env wajib: NEXT_PUBLIC_* (harus ada saat build, bukan saat run). Health check: HTTP 200 dari /.

Dari tabel ini, shared logic yang muncul: pembuatan database (untuk WordPress dan Laravel), compose file generation, nginx rule creation, teardown. Yang perlu template per-framework: image, environment variables, build steps, volume mounts.

Desain antarmuka satu skrip deploy untuk WordPress, Laravel, dan Next.js

Skrip deploy punya interface minimal — tiga command:

# Deploy
./scripts/deploy.sh wordpress kopi-surgent
./scripts/deploy.sh laravel portfolio
./scripts/deploy.sh nextjs landing-page

# Teardown
./scripts/deploy.sh down kopi-surgent

# List yang sedang jalan
./scripts/deploy.sh list

Command list sama pentingnya dengan up dan down. Tanpa list, kamu tidak tahu apa yang sedang jalan di server — terutama setelah deploy 5-6 situs. Output list menampilkan nama, framework, status container, dan URL.

Template compose per framework

WordPress

WordPress adalah yang paling sederhana: tidak ada build step, image official sudah lengkap. Detail lengkap pembuatan database dan nginx rule ada di artikel auto-deploy WordPress. Skrip deploy hanya perlu generate compose file dengan env yang benar:

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

Laravel

Laravel lebih kompleks. Tiga keputusan desain: php-fpm vs artisan serve, permission storage/, dan .env generation.

Php-fpm adalah pilihan untuk production karena support concurrency dan di-optimize untuk PHP-FPM process manager. php artisan serve (development server) single-threaded dan tidak cocok untuk traffic. Tapi php-fpm butuh web server di depannya — nginx atau Apache. Untuk keep things sederhana, skrip deploy pakai image yang sudah bundle Apache + PHP:

# /app-stacks/<nama>/docker-compose.yml — Laravel
services:
  <nama>:
    build:
      context: ./app
      dockerfile: Dockerfile
    restart: unless-stopped
    environment:
      DB_DATABASE: db_<nama>
      DB_USERNAME: db_<nama>
      DB_PASSWORD: <DB_PASSWORD>
      APP_KEY: <APP_KEY>
      APP_URL: https://<nama>.jayax.dev
    volumes:
      - ./data/storage:/var/www/html/storage
    deploy:
      resources:
        limits:
          memory: 768M

Permission storage/ adalah masalah klasik Laravel. Container berjalan sebagai www-data, tapi volume mount sering dimiliki root. Skrip deploy menjalankan chown -R www-data:www-data storage/ setelah container start. Tanpa ini, Laravel tidak bisa menulis log dan cache — error 500.

.env generation: skrip generate .env dari template dengan nilai database yang sudah dibuat. APP_KEY di-generate dengan php artisan key:generate. Migrations dijalankan saat pertama kali naik dengan php artisan migrate --force (flag --force karena production environment tidak prompt konfirmasi).

Next.js

Next.js berbeda dari dua lainnya: tidak butuh database, tapi punya quirk build-time. Variabel NEXT_PUBLIC_* harus ada saat build, bukan saat run. Skrip deploy inject env sebelum npm run build:

# /app-stacks/<nama>/docker-compose.yml — Next.js
services:
  <nama>:
    build:
      context: ./app
      dockerfile: Dockerfile
      args:
        NEXT_PUBLIC_API_URL: https://api.example.com
        NEXT_PUBLIC_SITE_URL: https://<nama>.jayax.dev
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M

Build args (NEXT_PUBLIC_*) di-inject saat image build. Setelah build selesai, variabel ini baked into JavaScript bundle — tidak bisa diubah saat runtime tanpa rebuild. Ini berbeda dari WordPress dan Laravel yang membaca env saat runtime.

Untuk Next.js dengan output: standalone (recommended), image akhir berukuran kecil — hanya berisi file yang dibutuhkan. Tanpa standalone, image bisa 1GB+. Pilihan ini juga mempengaruhi health check: standalone serve di port 3000, sedangkan non-standalone butuh next start yang juga di 3000 tapi lebih berat.

Pembuatan database otomatis yang dipakai bersama

WordPress dan Laravel sama-sama butuh database. Skrip deploy punya satu fungsi bash untuk membuat database dan user, dipakai oleh kedua framework:

create_database() {
    local name=$1
    local db_name="db_${name}"
    local db_user="db_${name}"
    local db_pass=$(generate_password)

    docker exec mariadb mysql -u root -p<ROOT_PASSWORD> -e "
        CREATE DATABASE IF NOT EXISTS ${db_name};
        CREATE USER IF NOT EXISTS '${db_user}'@'%' IDENTIFIED BY '${db_pass}';
        ALTER USER '${db_user}'@'%' IDENTIFIED BY '${db_pass}';
        GRANT ALL PRIVILEGES ON ${db_name}.* TO '${db_user}'@'%';
        FLUSH PRIVILEGES;
    "

    echo "${db_pass}" > /app-stacks/${name}/.db_password
}

Konvensi penamaan: db_<nama_situs> untuk database dan user. Password disimpan ke file .db_password di direktori app-stack (tidak dicetak ke stdout). Idempotensi: IF NOT EXISTS + ALTER USER memastikan deploy ulang tidak error.

Next.js tidak memanggil fungsi ini — dia langsung ke compose generation.

Teardown yang benar-benar bersih

Urutan teardown yang benar krusial. Kalau dibalik, ada window dimana traffic masuk ke container yang sedang down:

teardown() {
    local name=$1

    # 1. Hapus rule nginx dan reload
    rm -f /etc/nginx/sites/${name}.conf
    docker exec site-server nginx -s reload

    # 2. Stop container (nginx sudah tidak meneruskan traffic)
    cd /app-stacks/${name}
    docker compose down

    # 3. Hapus volume (data wp-content/storage dihapus)
    docker compose down -v 2>/dev/null || true

    # 4. Drop database dan user
    docker exec mariadb mysql -u root -p<ROOT_PASSWORD> -e "
        DROP DATABASE IF EXISTS db_${name};
        DROP USER IF EXISTS 'db_${name}'@'%';
        FLUSH PRIVILEGES;
    "

    # 5. Hapus direktori app-stack (optional, data sudah hilang)
    cd /app-stacks
    rm -rf ${name}
}

Kenapa urutannya begitu? Kalau container di-stop sebelum nginx rule dihapus, nginx masih meneruskan traffic ke container yang sudah mati — user dapat 502. Kalau database di-drop sebelum container di-stop, container yang masih jalan mencoba connect ke database yang tidak ada — error flood di log.

Peringatan: docker compose down -v menghapus volume. DROP DATABASE menghapus data permanen. Pastikan backup ada sebelum teardown production.

Fix runtime yang saya temukan setelah dipakai

ALTER USER saat user sudah ada

Bug yang sama seperti di artikel WordPress. User database dibuat di deploy pertama, tapi kalau deploy ulang dengan password baru (compose file diubah), password tidak sync. Fix: selalu jalankan ALTER USER setelah CREATE USER IF NOT EXISTS. Ini sudah built-in di fungsi create_database() di atas.

Compose up harus tuntas sebelum nginx reload

Container butuh waktu untuk start — terutama Laravel yang menjalankan composer install dan php artisan migrate saat first boot. Nginx reload sebelum container siap menghasilkan 502. Fix: wait loop yang mencoba HTTP request ke container sebelum reload nginx.

Menambah framework keempat butuh apa saja

Checklist untuk menambah framework ke-empat (misal: Django, Rails, Express):

  1. Template compose. Buat template docker-compose untuk framework baru. Tentukan: image atau build context, port, volume, env.
  2. Fungsi database. Kalau butuh database: panggil create_database(). Kalau tidak: skip.
  3. Build step. Define apakah butuh build sebelum container start. Untuk Next.js: npm run build di Dockerfile. Untuk Laravel: composer install di entrypoint.
  4. Health check. Tentukan cara verifikasi container siap. HTTP route, port check, atau log pattern.
  5. Whitelist bridge. Tambah keyword baru di hermes-bridge.ps1: wsl-deploy <framework-baru> <nama>.
  6. Update deploy.sh. Tambah case baru di switch statement skrip deploy.

Kalau checklist ini terasa berat, abstraksi belum cukup baik. Tanda abstraksi yang sehat: menambah kasus baru adalah copy-paste dengan perubahan kecil, bukan menulis ulang logika.

Yang belum dibahas di artikel ini

Skrip deploy lengkap juga menangani: logging (setiap operasi tercatat dengan timestamp), error handling (exit code + error message yang jelas), dan credential management (password di-generate dan disimpan, tidak di-hardcode). Bagian ini tidak dibahas karena sudah straightforward dan tidak punya trade-off desain yang menarik.

Yang juga tidak dibahas: deployment pipeline untuk static site (Next.js static export) yang menggunakan /sites/ folder langsung tanpa container — itu infrastruktur yang berbeda, dibahas di self-host banyak website di satu server.

Langkah berikutnya

Untuk detail WordPress-specific (konfigurasi MariaDB, bug runtime), baca auto-deploy WordPress dengan Docker dan MariaDB. Untuk memastikan pipeline ini bekerja, baca blueprint uji pipeline deploy otomatis. Konteks lengkap sistem ini ada di AI agent yang deploy website otomatis.

FAQ

Apakah skrip ini bisa deploy framework selain WordPress, Laravel, dan Next.js?

Bisa, tapi butuh penambahan seperti yang dijelaskan di bagian "menambah framework keempat." Skrip dirancang supaya penambahan adalah template baru + satu case baru di switch, bukan rewrite.

Kenapa tidak pakai platform seperti Coolify atau Dokku?

Saya pernah evaluasi. Mereka bagus untuk VPS dengan akses penuh, tapi infrastruktur saya berjalan di WSL2 dengan batasan Docker Desktop dan Windows networking. Skrip custom memberi kontrol yang lebih baik atas urutan operasi dan integrasi dengan bridge queue.

Bagaimana dengan Docker Swarm atau Kubernetes?

Overkill untuk skala ini (2-3 situs di satu mesin). Swarm/Kubernetes menambah overhead resource dan kompleksitas operasional yang tidak sepadan dengan jumlah situs yang saya kelola.

Bisakah deploy dari CI/CD (GitHub Actions)?

Bisa. CI/CD bisa menulis file .cmd ke queue via API atau webhook, dengan credential yang tepat. Tapi untuk saat ini, deploy dilakukan via agent atau manual CLI, yang sudah cukup.

Kesimpulan

Satu skrip untuk tiga framework bekerja karena tiga framework punya overlap yang cukup besar di kebutuhan infrastruktur (compose, database, nginx, tunnel). Yang berbeda — image, env, build step — di-abstract jadi template per framework. Tanda bahwa abstraksi sehat: bug fix satu kali berlaku untuk semua framework, dan menambah framework baru adalah checklist pendek.

Kalau mau memastikan skrip ini bekerja end-to-end, lanjut ke blueprint uji pipeline deploy otomatis.

More Articles