Pada lingkungan production, kasus file Laravel yang berhasil di-upload tetapi saat dibuka justru menghasilkan 403 Forbidden atau 404 Not Found adalah masalah yang sangat umum. Biasanya gejalanya terlihat seperti ini: file ada di server, path tampak benar, database menyimpan nama file, tetapi URL file tidak bisa diakses dari browser.

Dalam banyak kasus, penyebabnya bukan pada proses upload, melainkan pada cara Laravel mengekspos file dari storage ke web server. Akar masalah yang paling sering adalah symlink public/storage belum ada, permission folder salah, owner file tidak sesuai dengan user web server, konfigurasi disk public keliru, atau URL asset yang dipakai tidak tepat.

Artikel ini fokus pada pendekatan problem-oriented: apa yang harus dicek, kenapa itu penting, dan bagaimana memverifikasi hasilnya di server Linux dan browser.

Memahami alur akses file Laravel

Secara default, file yang di-upload ke disk public Laravel akan disimpan di:

storage/app/public

Agar file itu bisa diakses dari browser, Laravel mengandalkan symlink:

public/storage -> storage/app/public

Dengan begitu, file seperti:

storage/app/public/uploads/avatar.jpg

bisa diakses melalui URL:

https://domainanda.com/storage/uploads/avatar.jpg

Jika salah satu bagian dari alur ini bermasalah, browser bisa menerima 403 atau 404:

  • 404 biasanya berarti path publik tidak ditemukan, symlink belum ada, atau URL salah.
  • 403 biasanya berarti file atau folder ditemukan, tetapi web server tidak punya hak baca/traverse yang cukup.

Penyebab paling umum dan cara mengeceknya

1. Symlink storage:link belum dibuat atau rusak

Ini penyebab paling sering. Laravel tidak otomatis membuat symlink di semua deployment. Jika folder public/storage tidak ada atau bukan symlink yang benar, URL file akan gagal.

Cek di server Linux:

ls -l public

Pastikan ada output mirip:

storage -> /path/to/project/storage/app/public

Jika belum ada, jalankan:

php artisan storage:link

Jika sudah ada tetapi menunjuk ke path yang salah, hapus lalu buat ulang:

rm -f public/storage
php artisan storage:link

Kenapa ini penting? Karena browser hanya bisa mengakses file di document root web server, biasanya folder public. Folder storage/app/public sendiri tidak langsung diekspos ke web tanpa symlink.

Catatan: pada deployment berbasis symlink release directory atau shared storage, pastikan symlink tidak mengarah ke folder lama yang sudah tidak aktif.

2. Permission folder atau file salah

Jika symlink sudah benar tetapi browser mengembalikan 403, periksa permission direktori dan file. Web server membutuhkan izin untuk masuk ke setiap folder pada path file dan membaca file tersebut.

Cek permission:

ls -ld storage storage/app storage/app/public public public/storage
find storage/app/public -type d | head
find storage/app/public -type f | head

Umumnya, permission yang aman dan cukup adalah:

  • Folder: 755
  • File: 644

Perbaiki dengan:

find storage -type d -exec chmod 755 {} \;
find storage -type f -exec chmod 644 {} \;
find public -type d -exec chmod 755 {} \;
find public -type f -exec chmod 644 {} \;

Jika aplikasi perlu menulis ke folder tertentu, seperti storage dan bootstrap/cache, user web server juga harus memiliki izin tulis. Banyak orang langsung memberi 777, tetapi itu bukan praktik yang baik untuk production karena terlalu longgar dan berisiko keamanan.

Kenapa 403 bisa terjadi? Karena meskipun file ada, web server seperti Nginx atau Apache tidak bisa membaca file atau melewati direktori di atasnya jika bit execute/read pada folder tidak memadai.

3. Owner file tidak sesuai dengan user web server

Masalah lain yang sering muncul adalah file di-upload atau dipindahkan oleh user deploy, sedangkan web server berjalan sebagai user lain, misalnya www-data, nginx, atau apache. Akibatnya, aplikasi bisa tampak normal tetapi akses file gagal atau upload berikutnya bermasalah.

Cek owner dan group:

ls -l storage/app/public
ls -ld storage bootstrap/cache public

Di Ubuntu dengan Nginx/Apache, user web server sering adalah www-data. Di CentOS/AlmaLinux bisa nginx atau apache. Anda bisa cek proses web server:

ps aux | egrep '(nginx|apache|httpd|php-fpm)'

Jika owner belum sesuai, perbaiki misalnya:

sudo chown -R www-data:www-data storage bootstrap/cache public/storage

Atau, jika strategi deployment Anda memakai group bersama:

sudo chgrp -R www-data storage bootstrap/cache public/storage
sudo chmod -R g+rw storage bootstrap/cache

Trade-off: Mengubah owner seluruh project ke user web server memang memudahkan, tetapi tidak selalu ideal untuk alur deploy. Banyak tim memilih user deploy sebagai owner dan group web server untuk menyeimbangkan keamanan dan operasional.

4. Konfigurasi disk public di config/filesystems.php keliru

Jika Anda menyimpan file ke disk yang salah, URL yang dihasilkan juga bisa salah. Untuk akses browser biasa, disk public harus mengarah ke storage/app/public dan URL ke APP_URL/storage.

Periksa konfigurasi:

'public' => [
    'driver' => 'local',
    'root' => storage_path('app/public'),
    'url' => env('APP_URL').'/storage',
    'visibility' => 'public',
],

Pastikan proses upload memang memakai disk ini, misalnya:

$path = $request->file('image')->store('uploads', 'public');

Atau:

$path = Storage::disk('public')->putFile('uploads', $request->file('image'));

Jika Anda tanpa sadar menyimpan ke disk default local, file akan masuk ke storage/app, bukan storage/app/public, sehingga URL /storage/... tidak akan cocok.

Setelah mengubah konfigurasi, jangan lupa bersihkan cache config:

php artisan config:clear
php artisan cache:clear

Jika aplikasi menggunakan cache produksi:

php artisan config:cache

5. URL asset tidak tepat

Banyak kasus sebenarnya sederhana: file tersimpan benar, symlink ada, tetapi URL yang dibangun salah. Misalnya database menyimpan path uploads/file.pdf, tetapi di view dipanggil sebagai:

{{ asset($file) }}

Hasilnya menjadi:

https://domainanda.com/uploads/file.pdf

Padahal file publik Laravel ada di bawah prefix /storage. Yang benar biasanya:

{{ asset('storage/' . $file) }}

Atau jika menyimpan path hasil store(..., 'public') yang memang relatif terhadap disk public:

{{ Storage::url($file) }}

Kenapa Storage::url() sering lebih aman? Karena URL dibangun berdasarkan konfigurasi disk, sehingga lebih konsisten daripada merangkai string manual.

Periksa juga nilai APP_URL di environment. Jika salah domain, salah skema HTTP/HTTPS, atau ada subfolder yang terlupa, URL file bisa tampak rusak walaupun file tersedia.

Checklist diagnosis cepat di server Linux

Berikut urutan cek yang praktis saat menerima laporan file storage Laravel error 403 atau 404:

  1. Pastikan file benar-benar ada
    ls -l storage/app/public/uploads
  2. Cek symlink publik
    ls -l public/storage
  3. Cek permission dan owner
    namei -l public/storage/uploads/file.jpg

    Perintah namei -l sangat berguna karena menampilkan permission tiap komponen path, bukan hanya file akhirnya.

  4. Cek user web server
    ps aux | egrep '(nginx|apache|httpd|php-fpm)'
  5. Cek konfigurasi disk dan cache config
    php artisan tinker
    config('filesystems.disks.public')
  6. Uji URL langsung dari server
    curl -I https://domainanda.com/storage/uploads/file.jpg

Jika hasilnya 404, fokus ke symlink dan URL. Jika 403, fokus ke permission, owner, atau aturan web server.

Verifikasi dari sisi aplikasi dan browser

Verifikasi hasil upload

Jangan hanya mengandalkan bahwa upload “berhasil” karena tidak ada exception. Cek path yang benar-benar tersimpan:

$path = $request->file('image')->store('uploads', 'public');
logger()->info('Uploaded file path', ['path' => $path]);

Nilai $path umumnya seperti:

uploads/abc123.jpg

Itu berarti file fisik seharusnya ada di:

storage/app/public/uploads/abc123.jpg

Dan URL publiknya seharusnya:

/storage/uploads/abc123.jpg

Verifikasi di browser

Salin URL file dan buka langsung di browser, bukan lewat halaman aplikasi dulu. Lalu cek:

  • Apakah statusnya 403 atau 404?
  • Apakah URL mengarah ke domain yang benar?
  • Apakah menggunakan HTTPS jika situs dipaksa HTTPS?
  • Apakah file bisa diakses jika nama file sederhana tanpa spasi atau karakter khusus?

Di DevTools browser, tab Network akan membantu memastikan apakah masalah ada di URL, redirect, atau response dari server.

Kesalahan umum yang sering terlewat

  • Menjalankan storage:link di lokal tetapi tidak di production.
  • Menyimpan file ke disk local tetapi mengakses seolah-olah disk public.
  • Menggunakan asset($path) untuk path storage tanpa prefix storage/.
  • Permission file benar, tetapi permission folder parent salah.
  • Owner berubah setelah deploy, rsync, atau extract archive.
  • Config sudah diperbaiki, tetapi cache config belum dibersihkan.
  • Document root web server tidak menunjuk ke folder public.

Poin terakhir penting. Jika Nginx atau Apache diarahkan ke root project alih-alih folder public, perilaku akses file bisa membingungkan dan berisiko secara keamanan.

Penutup: mulai dari path fisik, lalu naik ke URL publik

Saat menghadapi file Laravel yang error 403 atau 404 di production, pendekatan terbaik adalah memeriksa alurnya dari bawah ke atas: apakah file fisik ada, apakah symlink benar, apakah permission dan owner sesuai, apakah disk public terkonfigurasi benar, lalu apakah URL yang dibangun aplikasi memang tepat.

Dalam praktik sehari-hari, sebagian besar kasus selesai dengan tiga tindakan: buat ulang storage:link, benahi owner/permission, dan pastikan view menggunakan Storage::url() atau asset('storage/...') yang benar. Jika setelah itu masih gagal, barulah lanjut memeriksa konfigurasi web server, cache aplikasi, dan struktur deployment.

Intinya, error 403 atau 404 pada file storage Laravel hampir selalu bisa dilokalisasi dengan cepat jika Anda memisahkan masalah menjadi tiga lapisan: penyimpanan file, eksposur ke public path, dan URL akses dari aplikasi.