Dari 500 Error ke Stabil: Checklist Troubleshooting Laravel di VPS

 


Error 500 di VPS punya satu sifat yang menjengkelkan: pesannya hampir tidak memberi tahu apa-apa. Di layar hanya tertulis "Server Error", sementara penyebabnya bisa bersembunyi di lima lapisan berbeda — kode aplikasi, konfigurasi Nginx, PHP-FPM, izin file, atau worker queue yang sudah lama tidak restart. Artikel ini memberi Anda urutan diagnosis yang bisa diikuti dari atas ke bawah, tanpa menebak-nebak, supaya tiap gejala mengarah ke satu perintah cek yang jelas.

Satu hal perlu ditegaskan di awal: kronologi di bawah adalah ilustrasi yang disusun untuk menjelaskan alur diagnosis. Ini bukan laporan kejadian yang pernah dialami siapa pun, dan bukan klaim bahwa langkah-langkahnya sudah dijalankan di server tertentu. Anggap saja sebagai simulasi di atas kertas: sebuah aplikasi Laravel yang kemarin masih normal, pagi ini menampilkan 500 setelah deploy semalam, dan kita menelusuri penyebabnya lapis demi lapis.

Ilustrasi: satu pagi, satu halaman 500

Bayangkan skenarionya seperti ini. Semalam ada deploy: beberapa file diubah, satu migrasi ditambahkan, aset front-end dibangun ulang. Pagi ini halaman utama menampilkan 500. Di mesin lokal, kode yang sama berjalan normal. Perbedaan antara lokal dan VPS inilah yang justru memberi petunjuk: kalau kode yang sama lolos di satu tempat dan gagal di tempat lain, penyebabnya kemungkinan besar ada di lingkungan server, bukan di logika aplikasi semata. Diagnosis dimulai dari sana.

Aturan pertama sebelum menyentuh apa pun: jangan langsung menjalankan perintah perbaikan. Baca dulu. Perintah perbaikan yang dijalankan tanpa bukti — membersihkan semua cache, mengubah izin jadi 777, me-restart semua service sekaligus — sering menutupi jejak penyebab asli, sehingga error yang sama kembali minggu depan tanpa ada yang paham kenapa. Urutan di bawah dirancang supaya bukti terkumpul sebelum ada yang diubah.

Langkah 1: Pastikan Anda melihat error yang benar

Di produksi, APP_DEBUG harus false. Konsekuensinya, Laravel tidak menampilkan detail error ke browser — dan itu memang seharusnya begitu, demi keamanan. Detailnya ada di log, bukan di layar. Periksa dulu nilai yang sedang aktif:

cd /var/www/nama-aplikasi/current
php artisan config:show app.debug

Perintah ini membaca konfigurasi yang sedang dipakai aplikasi, bukan sekadar isi file .env. Kalau keluarannya true di server produksi, ubah di /var/www/nama-aplikasi/shared/.env menjadi APP_DEBUG=false, lalu bangun ulang cache:

php artisan config:cache

Hasil yang diharapkan: halaman error kembali generik, dan detailnya hanya bisa dibaca lewat log server. Kalau Anda justru mengaktifkan APP_DEBUG=true untuk "mempermudah debugging", jangan lupa mengembalikannya — satu halaman error dengan debug aktif bisa menampilkan potongan kredensial basis data.

Langkah 2: Baca log Laravel

Log aplikasi adalah titik awal diagnosis untuk hampir semua error 500. Lokasinya:

tail -n 100 /var/www/nama-aplikasi/shared/storage/logs/laravel.log

Kalau aplikasi Anda memakai struktur tanpa direktori shared, jalurnya adalah storage/logs/laravel.log di dalam direktori aplikasi. Yang Anda cari bukan sekadar baris terakhir, melainkan blok error lengkap: stempel waktu, pesan exception, dan jejak file yang terlibat. Dalam ilustrasi kita, katakanlah log menampilkan pesan tentang tabel yang tidak ditemukan setelah migrasi semalam. Itu langsung mengarahkan diagnosis ke langkah migrasi, bukan ke Nginx.

Perintah berikut membantu saat log sudah panjang dan Anda hanya butuh error terbaru:

grep -n "ERROR\|Exception" /var/www/nama-aplikasi/shared/storage/logs/laravel.log | tail -n 20

Kesalahan umum: log kosong padahal halaman menampilkan 500. Log kosong hampir selalu berarti Laravel tidak bisa menulis ke file log itu — yang dengan sendirinya adalah petunjuk ke langkah izin file di bawah. Jangan menyimpulkan "tidak ada error" dari log yang kosong; periksa dulu apakah file-nya bisa ditulis.

Langkah 3: Baca log Nginx dan PHP-FPM

Kalau log Laravel tidak menunjukkan apa-apa, naik satu lapis ke web server. Log error Nginx:

sudo tail -n 100 /var/log/nginx/error.log

Pesan seperti connect() to unix:/run/php/php8.3-fpm.sock failed berarti Nginx tidak bisa berbicara dengan PHP-FPM — bisa karena service-nya berhenti, bisa karena jalur socket di konfigurasi Nginx tidak cocok dengan yang dipakai PHP-FPM. Cocokkan dengan konfigurasi pool FPM Anda di /etc/php/8.3/fpm/pool.d/www.conf, pada baris listen.

Untuk status service-nya sendiri:

sudo systemctl status php8.3-fpm
sudo systemctl status nginx

Ganti 8.3 dengan versi PHP yang terpasang di server Anda. Keluaran yang sehat menunjukkan active (running). Kalau FPM berhenti, lognya bisa dibaca lewat:

sudo journalctl -u php8.3-fpm --since "1 hour ago" --no-pager

Perintah ini menampilkan satu jam terakhir log service tersebut. Dalam ilustrasi kita, katakanlah Nginx dan FPM sama-sama berjalan — maka lapis web server bisa dicoret, dan diagnosis bergerak ke izin file.

Langkah 4: Periksa izin storage dan bootstrap/cache

Ini penyebab 500 paling klasik di Laravel, dan paling mudah dicek. Laravel harus bisa menulis ke storage/ dan bootstrap/cache/. Periksa kepemilikan dan izinnya:

ls -ld /var/www/nama-aplikasi/shared/storage /var/www/nama-aplikasi/current/bootstrap/cache

Hasil yang diharapkan: kedua direktori dimiliki user deploy dengan grup www-data, atau sepenuhnya oleh www-data, dengan izin yang memungkinkan proses PHP menulis. Perintah perbaikan yang benar:

sudo chown -R deploy:www-data /var/www/nama-aplikasi/shared/storage /var/www/nama-aplikasi/current/bootstrap/cache
sudo chmod -R 775 /var/www/nama-aplikasi/shared/storage /var/www/nama-aplikasi/current/bootstrap/cache

Jangan tergoda memakai chmod -R 777. Izin 777 memang membuat error hilang seketika, tapi ia membuka direktori itu untuk ditulis siapa pun di server — termasuk proses yang tidak seharusnya. Gejala yang tampak sembuh dengan 777 biasanya kembali dalam bentuk yang lebih buruk: file log atau cache yang tiba-tiba dimiliki user lain dan tidak bisa ditimpa lagi.

Satu pemeriksaan tambahan yang sering terlewat: symlink public/storage. Kalau file yang diunggah mendadak 404 setelah deploy, padahal halaman lain normal, cek tautannya:

ls -la /var/www/nama-aplikasi/current/public/storage

Keluaran yang sehat menunjukkan symlink ke storage/app/public di dalam rilis (yang pada gilirannya mengarah ke direktori shared). Kalau symlink itu hilang — misalnya karena deploy baru meng-clone ulang direktori public — buat ulang dengan:

cd /var/www/nama-aplikasi/current
php artisan storage:link

Langkah 5: Periksa migrasi dan status basis data

Kembali ke ilustrasi kita: log Laravel menyebut tabel yang tidak ditemukan. Langkah berikutnya adalah membandingkan migrasi yang tercatat dengan file migrasi yang ada:

cd /var/www/nama-aplikasi/current
php artisan migrate:status

Perintah ini menampilkan daftar migrasi beserta statusnya: Ran untuk yang sudah dieksekusi, Pending untuk yang belum. Kalau migrasi semalam berstatus Pending, jalankan dengan flag produksi:

php artisan migrate --force

Flag --force diperlukan karena Laravel menolak menjalankan migrasi di lingkungan produksi tanpa konfirmasi. Hasil yang diharapkan: baris Migrated: untuk migrasi tersebut, dan halaman kembali normal.

Kalau migrasi berstatus Ran tapi tabel tetap tidak ada, kemungkinan migrasi pernah gagal di tengah jalan dan tercatat sebagian. Di titik ini, jangan menjalankan perintah perusak seperti migrate:fresh di produksi — perintah itu menghapus dan membangun ulang seluruh skema. Sebagai gantinya, periksa isi tabel migrations di basis data dan bandingkan dengan file migrasi, lalu perbaiki dengan satu migrasi baru yang menambahkan apa yang kurang. Prinsipnya: di produksi, skema hanya bergerak maju lewat migrasi baru, tidak pernah dihapus dan dibangun ulang.

Langkah 6: Curigai cache yang usang

Cache adalah penyebab error yang paling licik, karena gejalanya tidak konsisten: sebagian halaman normal, sebagian menampilkan data lama, dan sesekali muncul error yang tidak masuk akal. Setelah deploy, tiga cache ini harus dibangun ulang dari rilis yang baru:

cd /var/www/nama-aplikasi/current
php artisan optimize:clear
php artisan optimize

Perintah pertama menghapus cache konfigurasi, route, view, dan event. Perintah kedua membangunnya lagi. Perlu dicatat: optimize:clear juga memanggil cache:clear, sehingga data di cache aplikasi default ikut terhapus — jadwalkan langkah ini di luar jam sibuk kalau cache aplikasi Anda menampung data yang mahal untuk dibangun ulang.

Dua jebakan cache yang patut dikenali:

  • Nilai .env lama yang membandel. Setelah config:cache berjalan, Laravel membaca konfigurasi dari file cache, bukan dari .env. Mengedit .env tanpa membangun ulang cache tidak mengubah apa pun. Gejalanya: Anda sudah mengganti kata sandi basis data di .env, tapi aplikasi tetap memakai yang lama. Obatnya selalu sama: php artisan config:cache setelah setiap perubahan .env.
  • Route closure yang menggagalkan route:cache. Kalau ada route yang didefinisikan sebagai closure, optimize akan gagal di tahap route. Solusinya adalah memindahkan logika closure ke controller, seperti dibahas di artikel deploy otomatis. Sementara itu, bangun cache yang lain saja dan lewatkan route cache.

Langkah 7: Periksa OPcache

Di atas cache Laravel, ada satu lapis cache lagi: OPcache di level PHP. OPcache menyimpan bytecode PHP yang sudah dikompilasi, sehingga request berikutnya tidak perlu mengurai file sumber lagi. Masalah muncul saat file sumber berubah (deploy baru) tapi OPcache masih menyajikan bytecode lama. Secara default, OPcache memeriksa stempel waktu file secara berkala, tapi kalau pengaturan opcache.validate_timestamps dimatikan di produksi — praktik yang umum demi performa — maka file baru tidak akan dikenali sampai PHP-FPM di-reload atau OPcache di-reset.

Periksa pengaturannya:

php -i | grep -i "opcache.validate_timestamps\|opcache.enable"

Kalau validate_timestamps bernilai Off, maka setiap deploy wajib diakhiri dengan reload PHP-FPM:

sudo systemctl reload php8.3-fpm

Reload lebih lembut daripada restart: proses yang sedang berjalan tidak diputus paksa. Gejala klasik masalah OPcache: kode sudah diperbarui di server (terlihat dari cat file-nya), tapi perilaku aplikasi tidak berubah sama sekali. Kalau Anda menemukan diri Anda dalam situasi itu, OPcache adalah tersangka pertama.

Langkah 8: Periksa queue worker

Error yang melibatkan queue punya ciri khas: halaman web normal, tapi email tidak terkirim, notifikasi tidak sampai, atau job menumpuk tanpa kabar. Worker queue berjalan sebagai proses latar yang panjang, dan proses yang panjang tidak otomatis memakai kode baru setelah deploy. Periksa dulu antrean job yang gagal:

cd /var/www/nama-aplikasi/current
php artisan queue:failed

Perintah ini menampilkan job yang gagal beserta pesan exception-nya — seringkali jauh lebih informatif daripada sekadar "email tidak terkirim". Untuk status worker-nya, kalau Anda memakai Supervisor (seperti dibahas di artikel queue seri ini), cek dengan:

sudo supervisorctl status

Keluaran yang sehat menunjukkan worker berstatus RUNNING. Kalau worker berhenti, Supervisor biasanya menjalankannya lagi otomatis; kalau tidak, log Supervisor di /var/log/supervisor/ memberi tahu alasannya. Dan setelah setiap deploy, sinyal restart wajib diberikan:

php artisan queue:restart

Perintah ini tidak membunuh job yang sedang berjalan — worker menyelesaikan job aktifnya dulu, lalu berhenti, dan Supervisor memulainya lagi dengan kode baru. Melewatkan langkah ini adalah penyebab paling umum dari gejala "fitur baru sudah deploy tapi job masih berperilaku seperti versi lama".

Langkah 9: Kumpulkan bukti sebelum mengubah konfigurasi Nginx

Kalau semua lapis di atas sudah diperiksa dan error masih ada, barulah konfigurasi Nginx diperiksa — bukan di awal, karena mengubah Nginx tanpa bukti sering menggeser masalah, bukan menyelesaikannya. Validasi sintaksnya:

sudo nginx -t

Keluaran yang sehat: syntax is ok dan test is successful. Kalau gagal, pesan error-nya menunjuk baris yang bermasalah di file konfigurasi. Dua kesalahan konfigurasi yang paling sering ditemui di setup Laravel: root yang mengarah ke direktori aplikasi, bukan ke public/, dan blok try_files yang tidak meneruskan request ke index.php. Bentuk yang benar untuk keduanya:

root /var/www/nama-aplikasi/current/public;
index index.php;

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

Setelah mengubah konfigurasi, muat ulang Nginx (bukan restart):

sudo systemctl reload nginx

Tabel gejala → penyebab → perintah cek

GejalaKemungkinan penyebabPerintah cek pertama
500 di semua halaman, log Laravel menyebut tabel hilangMigrasi belum jalanphp artisan migrate:status
500 di semua halaman, log Laravel kosongLaravel tidak bisa menulis log (izin)ls -ld shared/storage dan cek kepemilikan
502 Bad GatewayPHP-FPM berhenti atau socket tidak cocoksystemctl status php8.3-fpm, tail /var/log/nginx/error.log
Halaman normal, file upload 404Symlink public/storage hilang setelah deployls -la current/public/storage
Data lama membandel setelah ubah .envCache konfigurasi belum dibangun ulangphp artisan config:cache
Kode baru tidak berpengaruh sama sekaliOPcache masih memegang bytecode lamaphp -i | grep opcache.validate_timestamps, lalu reload FPM
Email/notifikasi tidak terkirim, web normalWorker queue berhenti atau memakai kode lamaphp artisan queue:failed, supervisorctl status
Route baru 404, route lama normalCache route usang atau route closurephp artisan route:clear, lalu bangun ulang
Error muncul hanya setelah deploy tertentuMigrasi tidak backward-compatibleBandingkan migrate:status dengan file migrasi terbaru

Checklist akhir

  • APP_DEBUG dipastikan false di produksi; detail error dibaca dari log, bukan dari browser
  • storage/logs/laravel.log dibaca sampai blok error lengkap ditemukan, bukan hanya baris terakhir
  • Log Nginx (/var/log/nginx/error.log) dan status PHP-FPM diperiksa sebelum mengubah konfigurasi apa pun
  • Kepemilikan storage/ dan bootstrap/cache/ benar (deploy:www-data, 775) — tanpa jalan pintas 777
  • Symlink public/storage ada dan mengarah ke tempat yang benar
  • migrate:status dibandingkan dengan file migrasi; migrasi yang tertinggal dijalankan dengan --force
  • Tidak ada perintah perusak (migrate:fresh, hapus skema) yang dijalankan di produksi
  • Cache dibangun ulang (optimize:clear lalu optimize) setelah deploy dan setelah setiap perubahan .env
  • OPcache diperiksa: kalau validate_timestamps mati, PHP-FPM di-reload setiap deploy
  • Job gagal dicek (queue:failed), worker dipastikan RUNNING, dan queue:restart diberikan setiap deploy
  • Konfigurasi Nginx divalidasi (nginx -t) dan root dipastikan mengarah ke public/
  • Setelah perbaikan, halaman utama, satu file /storage/, dan satu aksi queue dicek langsung — bukan diasumsikan sembuh

Urutan di atas sengaja dibuat membosankan: baca log, cek satu lapis, coret, lanjut ke lapis berikutnya. Kebosanan itu adalah fitur. Error 500 jarang membutuhkan kecerdikan; ia membutuhkan kesabaran untuk tidak melewatkan lapis yang salah.

Gejala apa yang paling sering Anda temui di VPS Anda — 500 yang log-nya kosong, 502 dari FPM, atau job queue yang diam-diam berhenti? Ceritakan gejala dan perintah cek yang akhirnya mengungkap penyebabnya di komentar, supaya checklist ini bisa terus dilengkapi dari kasus nyata pembaca.

Baca juga: panduan membaca log server secara umum, Membaca log server: journalctl, logrotate, dan pola yang patut diwaspadai; panduan Nginx dan PHP-FPM untuk Laravel di seri ini, Nginx + PHP-FPM untuk Laravel: server block yang benar; dan untuk fondasi container di proyek lain, Docker Compose untuk self-hosting dari nol sampai jalan.

Komentar