Laravel storage link và quyền thư mục: sửa lỗi không hiển thị ảnh

Quy trình chẩn đoán Laravel storage link, symbolic link và quyền thư mục khi ảnh upload thành công nhưng URL trả 404, 403 hoặc ảnh bị vỡ.

NHT
· 8 phút đọc
Laravel storage link, symbolic link và quyền thư mục khi sửa lỗi ảnh không hiển thị

Laravel storage link và quyền thư mục là hai điểm cần kiểm tra đầu tiên khi ảnh upload thành công nhưng trình duyệt trả 404, 403 hoặc hiển thị ảnh vỡ. Laravel tách khu vực lưu file khỏi document root để tránh mọi file trong storage tự động trở thành tài nguyên public. Vì vậy, ứng dụng cần một disk phù hợp, symbolic link đúng đích và quyền đọc hợp lệ cho user chạy web server.

Bài viết này trình bày quy trình chẩn đoán thực tế trên Ubuntu/Nginx hoặc Apache: xác định disk, kiểm tra `storage/app/public`, tạo `public/storage` bằng `php artisan storage:link`, đối chiếu document root, kiểm tra ownership và permission, sau đó xác minh URL bằng curl. Mục tiêu không phải chmod 777 một cách mù quáng mà là tìm đúng lớp đang chặn request [1].

Nguyên tắc an toàn: trước khi sửa quyền, hãy xác định file cần public, user chạy PHP/web server và document root thực tế. Symlink đúng nhưng target không readable vẫn tạo ra lỗi; permission rộng hơn không sửa được một symlink trỏ sai.

Laravel filesystem cung cấp abstraction cho local disk, public disk, SFTP, S3 và các driver khác. Với cấu hình local mặc định, disk `public` thường trỏ tới `storage/app/public`. Đây là nơi phù hợp cho ảnh hoặc file cần được ứng dụng phục vụ công khai, nhưng thư mục này không nằm trực tiếp dưới document root `public/`. Nếu đang triển khai PHP-FPM nhiều phiên bản, hãy đối chiếu thêm bài chạy nhiều phiên bản PHP với Nginx để tránh dùng nhầm runtime.

Lệnh `php artisan storage:link` tạo symbolic link từ `public/storage` tới `storage/app/public`. Khi đó, file `storage/app/public/avatar.jpg` có thể được truy cập qua đường dẫn `/storage/avatar.jpg`, còn code có thể dùng `asset("storage/avatar.jpg")`. Laravel cũng hỗ trợ `storage:unlink` để xóa các link đã cấu hình [1].

LớpVí dụCần kiểm tra
Disk Laravelpublicroot/path và driver trong config/filesystems.php
File thậtstorage/app/public/photos/a.jpgfile có tồn tại sau upload không
Public linkpublic/storagelink có trỏ đúng target không
URL browser/storage/photos/a.jpgURL có khớp tên file và document root không
Web userwww-data hoặc user hostingcó quyền traverse/read không

2. Kiểm tra cấu hình filesystem trước khi sửa server

Hãy chạy các lệnh dưới đây trong đúng release của ứng dụng. Không chạy từ thư mục home hoặc một website khác, vì Laravel có thể đọc `.env` và `config` của project không đúng. Nếu ứng dụng dùng config cache, thay đổi `.env` chưa chắc phản ánh ngay vào runtime.

cd /var/www/example.com/current
php artisan about | sed -n '1,160p'
php artisan config:show filesystems 2>/dev/null || true
sed -n '1,220p' config/filesystems.php
printf '
== storage ==
'
find storage/app/public -maxdepth 2 -type f -printf '%p %s bytes\n' | head -30
printf '
== public link ==
'
ls -ld public public/storage storage storage/app storage/app/public
readlink -f public/storage 2>/dev/null || true

Nếu `public/storage` không tồn tại, đó là dấu hiệu rõ ràng nhưng không phải nguyên nhân duy nhất. Nếu link tồn tại, hãy xem `readlink -f` có trả về `/var/www/example.com/current/storage/app/public` hay target của release cũ. Với deploy dùng symlink `current`, link được tạo trong release nào cũng cần được kiểm tra vì đường dẫn tương đối có thể trở nên khó đoán.

Từ thư mục project, chạy `php artisan storage:link`. Laravel sẽ dùng danh sách links trong `filesystems.links`; không nên tự tạo link sang một thư mục không được cấu hình nếu muốn các lệnh Artisan quản lý nhất quán. Nếu link cũ trỏ sai, có thể dùng `php artisan storage:unlink` rồi tạo lại sau khi xác nhận target.

cd /var/www/example.com/current
php artisan storage:unlink
php artisan storage:link
ls -la public/storage
readlink public/storage
readlink -f public/storage

Nếu hosting shared không cho phép symlink hoặc báo `Operation not permitted`, không nên cố chmod toàn bộ project. Hãy kiểm tra policy của hosting, document root và lựa chọn disk/object storage được nhà cung cấp hỗ trợ. Với cPanel, document root có thể là `public_html`, `domain/public` hoặc thư mục được cấu hình riêng; Laravel phải được trỏ đúng nơi có `index.php` và `public/storage`.

4. Phân biệt 404, 403 và lỗi URL asset

Triệu chứngKhả năng caoCách xác minh
404 Not FoundSai URL, thiếu file hoặc link không tồn tạicurl -I và find target
403 ForbiddenThiếu quyền traverse/read hoặc rule web serverlog Nginx/Apache và namei -l
200 nhưng ảnh vỡContent-Type, file hỏng hoặc URL trả HTMLcurl -I, file, identify
Link trỏ release cũDeploy đổi current nhưng link không được tạo lạireadlink -f và realpath
Chỉ lỗi trong appCache URL/config hoặc code tạo sai pathlog Laravel và inspect output
URL='https://example.com/storage/photos/sample.jpg'
curl -I "$URL"
curl -fsSL "$URL" -o /tmp/sample.jpg
file /tmp/sample.jpg
# kiểm tra từng lớp đường dẫn trên Linux
namei -l /var/www/example.com/current/public/storage/photos/sample.jpg

Nếu `curl -I` trả 200 nhưng `file` nhận diện HTML, web server hoặc ứng dụng đang trả trang lỗi với mã trạng thái không phù hợp. Nếu trả 403, kiểm tra từng thư mục cha vì user web server cần quyền execute/traverse trên directory và quyền read trên file. Đây là lý do không nên chỉ chạy `chmod 644` cho file rồi bỏ qua permission của directory.

5. Permission và ownership: sửa tối thiểu, không chmod 777

Permission đúng phụ thuộc mô hình deploy. Với Ubuntu/Nginx phổ biến, PHP-FPM hoặc web server có thể chạy dưới `www-data`; với shared hosting, user chạy Node/PHP hoặc Apache có thể là account riêng. Hãy kiểm tra process và path thay vì sao chép `www-data` từ một máy khác.

ps -eo user,group,comm,args | grep -E '[p]hp-fpm|[n]ginx|[a]pache'
namei -l /var/www/example.com/current/storage/app/public/photos/sample.jpg
stat -c '%A %a %U:%G %n' storage storage/app storage/app/public storage/app/public/photos
# ví dụ tham khảo, chỉ chạy sau khi xác nhận user/group thực tế
sudo chown -R deploy:www-data storage bootstrap/cache
sudo find storage bootstrap/cache -type d -exec chmod 775 {} \\;
sudo find storage bootstrap/cache -type f -exec chmod 664 {} \\;

Không dùng lệnh trên nguyên trạng nếu hệ thống dùng user khác. Mục tiêu là user chạy ứng dụng có thể ghi các thư mục cần ghi, còn web server có thể đọc file public. Các thư mục chứa secret, `.env`, backup database hoặc file private không nên nằm dưới document root và không nên mở quyền theo cách này.

6. Kiểm tra Nginx/Apache và document root

Khi symlink và permission đúng nhưng URL vẫn sai, hãy kiểm tra virtual host. Nginx cần `root` trỏ tới thư mục public của Laravel, không phải toàn bộ project. Apache cần document root, `AllowOverride` và rule rewrite phù hợp nếu ứng dụng dùng `.htaccess`. Một website nhiều domain có thể đang trả request từ virtual host khác.

server {
    server_name example.com www.example.com;
    root /var/www/example.com/current/public;

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

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}

Sau khi sửa Nginx, dùng `nginx -t` rồi reload có kiểm soát. Đừng dùng cấu hình của domain khác để thay thế nếu socket PHP-FPM, root hoặc user khác nhau. Khi `root` trỏ nhầm vào `/var/www/example.com` thay vì `/var/www/example.com/current/public`, `/storage/...` có thể bị map thành đường dẫn không tồn tại hoặc lộ cấu trúc project.

7. Xóa cache config và kiểm tra URL được tạo bởi ứng dụng

Một lỗi phổ biến là `.env` đã đổi `APP_URL` hoặc disk nhưng config cache vẫn giữ giá trị cũ. Sau khi backup và xác nhận release đúng, có thể rebuild cache config; không xóa cache tùy tiện trên production khi chưa biết ứng dụng đang chạy release nào.

cd /var/www/example.com/current
php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan tinker --execute="dump(config('filesystems.disks.public')); dump(asset('storage/photos/sample.jpg'));"

Nếu helper tạo URL có domain cũ, kiểm tra `APP_URL`, reverse proxy headers và trusted proxies. Nếu URL có `/public/storage` hoặc thiếu `/storage`, code upload đang tạo path không đúng abstraction của Laravel. Nên lưu path tương đối trên disk, sau đó dùng `Storage::url($path)` hoặc helper phù hợp thay vì ghép chuỗi tuyệt đối trong controller.

8. Quy trình xử lý ảnh không hiển thị trong 10 phút

  • Ghi lại URL lỗi và mã HTTP thực tế bằng curl.
  • Kiểm tra file thật có tồn tại trên disk mà code đang dùng.
  • Kiểm tra `public/storage` có phải symbolic link và trỏ đúng target.
  • Dùng `namei -l` để kiểm tra quyền traverse từng thư mục.
  • Đối chiếu document root và virtual host của domain.
  • Kiểm tra log Nginx/Apache, PHP-FPM và Laravel cùng thời điểm.
  • Xóa/rebuild config cache khi `.env` hoặc filesystem config vừa đổi.
  • Sau khi sửa, kiểm tra Content-Type và nội dung file chứ không chỉ mã 200.

9. Các lỗi cần tránh

Artisan sẽ không dùng đúng config nếu bạn chạy lệnh từ một thư mục không phải project. Hãy `pwd`, kiểm tra `artisan` và xác nhận release trước khi thao tác.

Chỉ sửa permission của file

Directory cha có thể không cho phép traverse. Hãy kiểm tra cả path bằng `namei -l`; không mở quyền toàn bộ project để che giấu nguyên nhân.

Lưu file private vào public disk

Ảnh public và tài liệu riêng tư có yêu cầu khác nhau. Nếu file chứa dữ liệu cá nhân, dùng private disk và route có authorization thay vì đặt dưới `public/storage`.

10. Kết luận

Sửa lỗi Laravel không hiển thị ảnh cần đi từ application filesystem đến file thật, symbolic link, permission, document root và cache. Khi kiểm tra theo từng lớp, bạn thường tìm được nguyên nhân mà không cần chmod 777 hoặc di chuyển toàn bộ storage. Hãy ghi lại disk, target symlink, user web server và lệnh xác minh vào runbook để lần deploy sau không lặp lại lỗi.

Nguồn tham khảo

[1] Laravel Documentation — File Storage; [2] Laravel Documentation — The Public Disk.