Pada tahap development, target utama biasanya adalah membuat aplikasi Laravel berjalan stabil. Namun setelah aplikasi live di VPS, fokus berpindah ke hal-hal yang lebih operasional: keamanan, observability, backup, proses deploy yang konsisten, dan recovery saat terjadi masalah. Inilah area yang sering diabaikan, padahal justru paling menentukan apakah aplikasi bisa bertahan dalam jangka panjang.

Artikel ini menjadi penutup seri dengan fokus pada praktik yang realistis untuk deployment Laravel berbasis Docker di VPS. Kita akan membahas hardening container dan VPS, pengelolaan secret, pembatasan akses jaringan, backup database, monitoring log dan resource, healthcheck, restart policy, alert dasar, hingga pipeline CI/CD sederhana menggunakan GitHub Actions untuk build, push image, dan deploy ke VPS via SSH. Di akhir, kita lengkapi dengan strategi rollback, checklist pasca-deploy, dan troubleshooting umum.

1. Hardening dasar container dan VPS

Jalankan proses dengan user non-root

Salah satu kesalahan paling umum adalah menjalankan container sebagai root. Memang lebih mudah karena hampir semua permission langsung bekerja, tetapi risikonya juga lebih tinggi. Jika ada celah pada aplikasi atau dependency, proses yang berjalan sebagai root akan memberi ruang gerak lebih besar bagi attacker.

Untuk image PHP-FPM Laravel, buat user khusus dan gunakan user tersebut untuk menjalankan aplikasi:

FROM php:8.2-fpm-alpine

RUN addgroup -g 1000 app && adduser -G app -g app -s /bin/sh -D app
WORKDIR /var/www/html

COPY . /var/www/html
RUN chown -R app:app /var/www/html

USER app
CMD ["php-fpm"]

Pada praktiknya, beberapa langkah instalasi dependency tetap membutuhkan root saat build image. Itu tidak masalah, asalkan proses runtime dijalankan sebagai user non-root. Jika menggunakan Nginx sebagai reverse proxy, pastikan permission file tidak memaksa Anda kembali ke root hanya karena direktori storage dan bootstrap/cache tidak benar.

Gunakan permission yang aman

Pada Laravel, direktori yang biasanya butuh write access hanyalah storage dan bootstrap/cache. Hindari memberikan permission terlalu longgar seperti chmod -R 777. Ini memang sering “menyelesaikan masalah”, tetapi juga membuka risiko yang tidak perlu.

Gunakan pola yang lebih aman:

chown -R app:app storage bootstrap/cache
chmod -R 775 storage bootstrap/cache

Jika container PHP dan web server berjalan dengan user yang berbeda, selaraskan group ownership atau gunakan strategi ACL sesuai kebutuhan. Prinsipnya sederhana: berikan hak minimum yang benar-benar diperlukan.

Batasi port yang diekspos

Jangan publish semua port container ke internet. Pada arsitektur Laravel Docker di VPS, biasanya hanya 80 dan 443 yang perlu diakses publik melalui reverse proxy. Port internal seperti PHP-FPM, MySQL, Redis, atau worker queue sebaiknya hanya tersedia di jaringan Docker internal.

Contoh yang aman di docker-compose.yml:

services:
  nginx:
    ports:
      - "80:80"
      - "443:443"

  app:
    expose:
      - "9000"

  mysql:
    expose:
      - "3306"

  redis:
    expose:
      - "6379"

Perbedaan pentingnya: ports mem-publish ke host, sedangkan expose hanya mendokumentasikan port internal untuk komunikasi antarkontainer. Dengan begitu, database tidak ikut terbuka ke publik tanpa sengaja.

Hardening VPS: firewall dan akses SSH

Di level VPS, aktifkan firewall seperti UFW dan buka hanya port yang benar-benar diperlukan:

ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
ufw status

Untuk SSH, praktik minimum yang disarankan:

  • Gunakan SSH key, bukan password.
  • Nonaktifkan login password jika sudah siap.
  • Pertimbangkan mengganti port default hanya sebagai pengurangan noise, bukan sebagai kontrol keamanan utama.
  • Batasi user yang boleh login SSH.

Selain itu, rutin pasang pembaruan sistem operasi, terutama patch keamanan. Banyak insiden justru terjadi bukan karena celah zero-day, tetapi karena server lama tidak pernah di-update.

Secret management: jangan simpan rahasia di image

Kesalahan lain yang sering terjadi adalah menyalin file .env ke dalam image Docker. Ini membuat rahasia seperti APP_KEY, password database, token mail, atau credential third-party menempel permanen pada image. Jika image bocor atau tersimpan di registry publik, semua secret ikut terekspos.

Gunakan environment variable saat runtime, atau mount file env dari server. Untuk VPS sederhana, pendekatan yang umum adalah menyimpan file .env di host dan memasangnya saat deploy. Pastikan file tersebut tidak ikut ke repository.

Catatan: Untuk skala kecil hingga menengah di VPS pribadi, file .env di server masih cukup masuk akal. Jika kebutuhan keamanan lebih tinggi, pertimbangkan secret manager terpusat seperti Vault, AWS SSM, atau Doppler.

Rutin update base image dan dependency

Image Docker bukan artefak yang boleh dibiarkan selamanya. Base image PHP, Nginx, Node, dan dependency sistem perlu diperbarui karena bisa mengandung CVE. Kebiasaan yang baik adalah melakukan rebuild image secara berkala, lalu deploy setelah diuji.

Trade-off-nya jelas: semakin sering update, semakin besar kemungkinan ada perubahan perilaku dependency. Karena itu, update perlu masuk ke pipeline yang bisa diuji dan memiliki rollback.

2. Backup database dan data penting

Backup bukan hanya soal membuat dump database, tetapi juga memastikan backup tersebut bisa direstore. Banyak tim baru sadar backup-nya rusak atau tidak lengkap saat insiden sudah terjadi.

Data apa saja yang perlu dibackup?

  • Database utama MySQL atau MariaDB.
  • File upload jika disimpan lokal di VPS.
  • File environment penting seperti .env.
  • Konfigurasi reverse proxy dan sertifikat jika tidak mudah diregenerasi.

Contoh backup database dengan mysqldump:

mysqldump -h 127.0.0.1 -u laravel_user -p'strongpassword' laravel_db \
  | gzip > /backup/laravel_db_$(date +%F_%H-%M-%S).sql.gz

Lebih baik lagi, jalankan backup terjadwal dengan cron dan kirim hasilnya ke object storage atau server lain. Jangan simpan backup hanya di VPS yang sama, karena jika disk VPS rusak atau server terhapus, backup ikut hilang.

Uji proses restore

Minimal, sesekali lakukan simulasi restore ke database terpisah:

gunzip < laravel_db_2026-03-25_01-00-00.sql.gz | mysql -u laravel_user -p laravel_db_restore

Yang perlu divalidasi setelah restore:

  • Tabel dan data utama lengkap.
  • Encoding/charset sesuai.
  • Aplikasi bisa terkoneksi.
  • Versi schema konsisten dengan migrasi aktif.

3. Monitoring log, resource, healthcheck, dan alert dasar

Kelola log container dengan disiplin

Docker memudahkan akses log lewat docker logs, tetapi jika tidak dikendalikan, ukuran log bisa membengkak dan memenuhi disk. Atur log rotation di Docker daemon atau pada konfigurasi logging.

Contoh pada Docker Compose:

services:
  app:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"

Untuk Laravel, tentukan apakah log aplikasi disimpan ke stdout/stderr atau ke file. Di lingkungan container, mendorong log ke stdout biasanya lebih praktis karena lebih mudah dikumpulkan oleh Docker atau sistem log aggregator.

Pantau resource container

Perintah sederhana seperti docker stats sering cukup untuk diagnosis awal. Anda bisa melihat lonjakan CPU, memory, dan I/O pada container tertentu. Untuk kebutuhan yang lebih rapi, gunakan kombinasi Prometheus + Grafana, atau minimal Netdata untuk observability cepat di VPS.

Hal yang perlu dipantau paling tidak:

  • Penggunaan CPU dan memory per container.
  • Kapasitas disk VPS, terutama volume log dan database.
  • Status restart container.
  • Latency dan error rate aplikasi.

Tambahkan healthcheck

Healthcheck membantu Docker mengetahui apakah container benar-benar sehat, bukan sekadar “masih hidup”. Misalnya, proses PHP-FPM bisa tetap berjalan, tetapi aplikasi tidak responsif karena dependency lain bermasalah.

Contoh healthcheck sederhana:

services:
  app:
    healthcheck:
      test: ["CMD", "php", "artisan", "about"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 20s

Untuk Nginx, Anda bisa menambahkan endpoint health seperti /up atau route khusus yang memeriksa koneksi database/Redis secara ringan. Jangan membuat health endpoint terlalu berat karena akan dipanggil berkala.

Gunakan restart policy yang masuk akal

Jika proses mati sesaat karena error sementara, Docker bisa mencoba menjalankannya kembali:

services:
  app:
    restart: unless-stopped
  queue:
    restart: unless-stopped

Namun restart policy bukan solusi akar masalah. Jika container terus crash lalu restart tanpa henti, Anda harus melihat log dan penyebab utamanya. Loop restart justru bisa menyamarkan masalah jika tidak dipantau.

Alert dasar

Untuk VPS sederhana, alert tidak harus rumit. Yang penting ada sinyal saat layanan utama down. Beberapa opsi praktis:

  • Uptime monitoring eksternal untuk memeriksa endpoint HTTPS.
  • Notifikasi saat disk hampir penuh.
  • Notifikasi saat container restart berulang.
  • Notifikasi saat job queue gagal menumpuk.

Bahkan kombinasi health endpoint + uptime checker publik + notifikasi email/Telegram sudah jauh lebih baik daripada tidak ada alert sama sekali.

4. Pipeline CI/CD sederhana dengan GitHub Actions

Tujuan CI/CD di konteks ini bukan membangun sistem yang sangat kompleks, tetapi mengurangi deploy manual yang rawan salah langkah. Pola sederhananya:

  1. Push code ke branch tertentu.
  2. GitHub Actions build image Docker.
  3. Image di-push ke registry.
  4. Workflow SSH ke VPS.
  5. VPS pull image terbaru dan restart service terkait.

Contoh workflow ringkas:

name: Deploy Laravel

on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and Push Image
        run: |
          IMAGE=ghcr.io/${{ github.repository }}/laravel-app:${{ github.sha }}
          docker build -t $IMAGE .
          docker push $IMAGE
          echo "IMAGE=$IMAGE" >> $GITHUB_ENV

      - name: Deploy to VPS
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          script: |
            cd /opt/laravel-app
            export IMAGE=${{ env.IMAGE }}
            sed -i "s|^APP_IMAGE=.*|APP_IMAGE=$IMAGE|" .env.deploy
            docker compose --env-file .env.deploy pull app
            docker compose --env-file .env.deploy up -d app queue
            docker compose exec -T app php artisan migrate --force
            docker compose exec -T app php artisan config:cache
            docker compose exec -T app php artisan route:cache

Pada server, docker-compose.yml bisa membaca nilai image dari file env deploy:

services:
  app:
    image: ${APP_IMAGE}
  queue:
    image: ${APP_IMAGE}

Keuntungan pendekatan ini adalah setiap deploy memakai tag image yang jelas, biasanya berdasarkan commit SHA. Ini sangat membantu untuk audit dan rollback.

Catatan penting untuk deploy migrasi

Perintah php artisan migrate --force harus dijalankan dengan hati-hati. Migration yang destruktif atau tidak backward-compatible bisa menyebabkan downtime atau rollback menjadi sulit. Jika memungkinkan:

  • Gunakan migration yang kompatibel dengan versi lama dan baru selama masa transisi.
  • Hindari menghapus kolom yang masih dipakai kode lama pada deploy yang sama.
  • Untuk perubahan besar, pecah menjadi beberapa release.

5. Strategi rollback yang realistis

Rollback yang baik tidak bergantung pada “ingat command terakhir”. Karena image diberi tag commit SHA, rollback bisa dilakukan dengan mengubah APP_IMAGE ke tag sebelumnya lalu menjalankan ulang service:

cd /opt/laravel-app
sed -i 's|^APP_IMAGE=.*|APP_IMAGE=ghcr.io/org/repo/laravel-app:PREVIOUS_SHA|' .env.deploy
docker compose --env-file .env.deploy up -d app queue

Namun rollback aplikasi tidak selalu cukup jika migration database sudah mengubah schema secara permanen. Karena itu, strategi rollback harus mempertimbangkan dua sisi:

  • Rollback code/image ke versi sebelumnya.
  • Rollback data/schema hanya jika benar-benar aman dan sudah diuji.

Praktik yang paling aman adalah merancang migration agar kompatibel selama beberapa release, sehingga rollback code tetap memungkinkan tanpa harus langsung rollback database.

6. Checklist pasca-deploy

Setelah deploy sukses, jangan langsung anggap semuanya aman. Lakukan verifikasi cepat:

  1. Cek status container: docker compose ps
  2. Cek log aplikasi dan web server: docker compose logs --tail=100 app nginx
  3. Pastikan endpoint utama mengembalikan status 200.
  4. Login ke aplikasi dan uji alur penting.
  5. Pastikan queue worker aktif.
  6. Verifikasi migrasi database berhasil.
  7. Cek SSL dan redirect HTTP ke HTTPS.
  8. Pastikan job terjadwal dan cron/scheduler tetap berjalan.

7. Troubleshooting umum setelah aplikasi live

Container crash berulang

Gejala: container status-nya restart terus atau exit. Langkah diagnosis:

  • Lihat log: docker compose logs app
  • Periksa error konfigurasi env.
  • Pastikan dependency seperti DB dan Redis bisa dijangkau.
  • Cek apakah permission storage dan bootstrap/cache benar.
  • Periksa batas memory VPS; proses PHP bisa dibunuh oleh OOM killer.

Migrasi gagal saat deploy

Penyebab umum: kredensial database salah, migration konflik, tabel sudah ada, atau lock pada database. Jika migration gagal di tengah deploy:

  • Jangan panik restart semua container tanpa membaca log.
  • Tentukan apakah aplikasi lama masih kompatibel dengan schema saat ini.
  • Jika perlu rollback image, lakukan segera.
  • Perbaiki migration lalu rilis ulang dengan prosedur yang lebih aman.

SSL bermasalah

Masalah SSL biasanya muncul karena sertifikat kedaluwarsa, konfigurasi reverse proxy salah, atau challenge ACME gagal. Periksa:

  • Apakah DNS sudah mengarah benar ke VPS.
  • Apakah port 80/443 terbuka.
  • Apakah volume sertifikat termount dengan benar.
  • Apakah reverse proxy mengarah ke upstream yang benar.

Jika Laravel berada di balik reverse proxy, pastikan konfigurasi trusted proxy benar agar URL HTTPS tidak berubah menjadi HTTP di aplikasi.

Queue macet atau job tidak diproses

Pada Laravel, queue sering menjadi komponen pertama yang terlupakan setelah deploy. Pastikan worker berjalan sebagai service/container terpisah dan memiliki restart policy. Cek hal berikut:

  • Koneksi Redis/database queue benar.
  • Worker container aktif.
  • Tidak ada job gagal menumpuk tanpa dipantau.
  • Timeout dan memory worker sesuai beban kerja.

Gunakan perintah seperti php artisan queue:failed dan lihat log worker untuk mengetahui job mana yang macet atau gagal berulang.

Penutup

Menjalankan Laravel di Docker pada VPS bukan hanya soal membuat aplikasi bisa diakses melalui domain. Setelah live, tantangan sesungguhnya adalah menjaga sistem tetap aman, dapat dipantau, mudah di-deploy ulang, dan cepat dipulihkan saat terjadi gangguan.

Mulailah dari hal-hal yang paling berdampak: jalankan container sebagai non-root, batasi port, kelola secret dengan benar, buat backup yang bisa direstore, pasang healthcheck dan restart policy, serta gunakan pipeline CI/CD sederhana yang menghasilkan image bertag jelas. Setelah fondasi ini stabil, Anda bisa melangkah ke monitoring yang lebih matang, deployment tanpa downtime, dan otomasi operasional yang lebih lanjut.

Jika tiga artikel sebelumnya fokus pada menyiapkan aplikasi sampai berjalan di server, maka tahap ini memastikan aplikasi tersebut layak dipelihara dalam lingkungan produksi nyata.