Rilis pertama di server baru sering berakhir dengan kejutan: aplikasi menampilkan error 500 padahal kode tidak berubah sama sekali. Penyebab klasiknya bukan bug, melainkan izin direktori — Laravel ternyata menulis ke disk hampir di setiap request, dan semua penulisan itu jatuh pada dua direktori yang secara bawaan tidak writable oleh user acak. Chapter ini memetakan siapa menulis apa, mendemonstrasikan error izin yang sesungguhnya, lalu menyusun model kepemilikan dan izin yang benar.

Siapa yang Menulis ke Disk #

Aplikasi Laravel bukan hanya membaca berkas — lima mekanisme aktif menulis ke storage/ selama aplikasi hidup:

AktivitasLokasi tulisPemicu
Log aplikasistorage/logs/laravel.logLog::info(), error, exception
Kompilasi Bladestorage/framework/views/Render view pertama / baru
Session (driver file)storage/framework/sessions/Setiap request ber-session
Cache (driver file)storage/framework/cache/Cache::remember()
Upload & aset publikstorage/app/Storage::put(), user upload
Cache konfigurasi & paketbootstrap/cache/config:cache, package:discover

Artinya user yang menjalankan PHP-FPM — biasanya www-data — harus punya izin tulis pada storage/ dan bootstrap/cache/, padahal kedua direktori ini dibuat oleh user kita sendiri saat git clone atau rsync di server. Di situlah bentrok muncul.

Dua Direktori Istimewa #

Kita periksa kondisi nyata kedua direktori tersebut pada aplikasi yang baru saja di-deploy:

bash
ls -la storage/ bootstrap/cache/

Output yang Dihasilkan:

text
bootstrap/cache/:
total 40
drwxrwxr-x 2 z z  4096 Oct 13 22:17 .
drwxrwxr-x 3 z z  4096 Sep 30 13:56 ..
-rw-rw-r-- 1 z z   14 Sep 30 13:56 .gitignore
-rwxrwxr-x 1 z z   963 Oct 13 22:17 packages.php
-rwxrwxr-x 1 z z 22575 Oct 13 22:17 services.php

storage/:
total 20
drwxrwxr-x 5 z z  4096 Sep 30 13:56 .
drwxrwxr-x 12 z z  4096 Oct 13 22:17 ..
drwxrwxr-x 4 z z  4096 Sep 30 13:56 app
drwxrwxr-x 6 z z  4096 Sep 30 13:56 framework
drwxrwxr-x 2 z z  4096 Sep 30 13:56 logs

Semua direktori bermode 775 — pemilik dan grup boleh menulis, user lain hanya membaca. packages.php dan services.php di bootstrap/cache/ terbentuk otomatis oleh Composer saat composer install dijalankan di server, itulah alasan direktori ini berstatus istimewa berdampingan dengan storage/.

Kesalahan Izin dalam Praktik #

Apa yang benar-benar terjadi saat izin hilang? Kita buktikan: cabut seluruh izin storage/ lalu jalankan perintah yang butuh menulis view:

bash
chmod 000 storage
php artisan view:cache

Output yang Dihasilkan:

text
   UnexpectedValueException 

  There is no existing directory at "/var/www/tokobudi/storage/logs" and it
  could not be created: Permission denied

  at vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php:282

Monolog gagal membuat storage/logs karena sistem operasi menolak — dan dalam konteks web, exception seperti ini yang merender halaman 500 ke pengguna. Yang menarik, pesan error-nya justru menyebut storage/logs padahal perintahnya menulis view: kegagalan menulis log terjadi lebih dulu saat framework mencatat kejadian. Kita pulihkan lalu ulangi perintah yang sama:

bash
chmod 775 storage
php artisan view:cache

Output yang Dihasilkan:

text
   INFO  Blade templates cached successfully.  

Perbaikan satu mode direktori mengembalikan semuanya. Saat menelusuri masalah serupa di server asli, pesan error dan path yang disebutnya adalah petunjuk direktori mana yang harus diperiksa.

Model Kepemilikan dan Izin #

Skema yang dianjurkan sederhana: seluruh aplikasi dimiliki user deploy kita, sedangkan PHP-FPM berjalan sebagai www-data dan hanya butuh tulis di dua direktori istimewa. Contoh tampilan penyetelannya di server:

bash
sudo chown -R deploy:deploy /var/www/tokobudi
sudo chown -R www-data:www-data /var/www/tokobudi/storage \
                                /var/www/tokobudi/bootstrap/cache

Dengan kepemilikan itu, panduan modenya konsisten: 775 untuk direktori yang ditulis www-data, 664 untuk berkas di dalamnya, dan 755/644 untuk sisa aplikasi yang hanya dibaca. Pola chmod -R 777 yang beredar di forum memang “menyelesaikan” error — dengan membuka tulis untuk semua user di server, termasuk user proses lain yang telah terkompromi; hindari kecuali sekadar eksperimen sekali pakai.

Untuk tim yang berbagi satu server, setfacl memberi izin per-user tanpa mengubah kepemilikan — contoh tampilan:

bash
sudo setfacl -R -m u:www-data:rwx -m u:budi:rwx storage bootstrap/cache
sudo setfacl -R -d -m u:www-data:rwx -m u:budi:rwx storage bootstrap/cache

Bendera -d (default) memastikan berkas baru yang lahir di direktori tersebut mewarisi ACL yang sama.

Upload user tersimpan di storage/app/public, sementara web server hanya melayani public/. Perintah storage:link menjembatani keduanya dengan symlink public/storage:

bash
php artisan storage:link

Output yang Dihasilkan:

text
   INFO  The [public/storage] link has been connected to [storage/app/public].  
bash
ls -la public/ | grep storage

Output yang Dihasilkan:

text
lrwxrwxrwx 1 z z 58 Oct 13 22:19 storage -> /var/www/tokobudi/storage/app/public

Symlink ini dibuat sekali per server — sejak Laravel 9, storage:link aman diulang pada deploy berikutnya karena perintah memeriksa keberadaan tautan sebelum membuat yang baru.

Penyimpanan lokal kini sehat dan writable. Namun aplikasi produksi masih punya penghuni yang hidup jauh lebih lama dari request HTTP — queue worker — yang menuntut pengawasan proses tersendiri lewat supervisor, kita bahas di Queue Worker di Production .