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:
| Aktivitas | Lokasi tulis | Pemicu |
|---|---|---|
| Log aplikasi | storage/logs/laravel.log | Log::info(), error, exception |
| Kompilasi Blade | storage/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 publik | storage/app/ | Storage::put(), user upload |
| Cache konfigurasi & paket | bootstrap/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:
ls -la storage/ bootstrap/cache/Output yang Dihasilkan:
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 logsSemua 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:
chmod 000 storage
php artisan view:cacheOutput yang Dihasilkan:
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:282Monolog 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:
chmod 775 storage
php artisan view:cacheOutput yang Dihasilkan:
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:
sudo chown -R deploy:deploy /var/www/tokobudi
sudo chown -R www-data:www-data /var/www/tokobudi/storage \
/var/www/tokobudi/bootstrap/cacheDengan 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:
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/cacheBendera -d (default) memastikan berkas baru yang lahir di direktori tersebut mewarisi ACL yang sama.
storage:link untuk Aset Publik #
Upload user tersimpan di storage/app/public, sementara web server hanya melayani public/. Perintah storage:link menjembatani keduanya dengan symlink public/storage:
php artisan storage:linkOutput yang Dihasilkan:
INFO The [public/storage] link has been connected to [storage/app/public]. ls -la public/ | grep storageOutput yang Dihasilkan:
lrwxrwxrwx 1 z z 58 Oct 13 22:19 storage -> /var/www/tokobudi/storage/app/publicSymlink 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 .