Файлы в Laravel: что настроить новичку до первой загрузки
Куда Laravel кладёт загруженные файлы, почему картинка не открывается по ссылке и как хранить пользовательские загрузки отдельно от кода. Разбор для тех, кто только начал.

Вы сделали форму загрузки аватара, файл вроде бы сохранился, а по ссылке отдаётся 404. Знакомо. Проблема почти всегда в одном: в Laravel есть разница между тем, куда файл физически лёг, и тем, откуда его отдаёт веб-сервер. Разберём, как устроена работа с файлами в Laravel 11.x, что настроить один раз в начале проекта и почему нельзя просто складывать всё в public/. К концу статьи у вас будет рабочая загрузка файла с публичной ссылкой и понимание, где что лежит.
Что нужно иметь до начала
- Laravel 11.x (в 10.x всё то же самое, отличий по файлам почти нет);
- PHP 8.2 или новее — этого требует сам Laravel 11;
- права на запись в каталог
storage/у пользователя, под которым работает PHP; - доступ к консоли проекта, чтобы выполнить
php artisan.
Проверить версию можно командой php artisan --version в корне проекта. Если версия ниже 10 — часть путей и команд может отличаться, сверяйтесь с документацией вашей версии.
Три места, которые путают в начале
Laravel работает с файлами через слой абстракции — дисков1. Диск описывает, где физически хранятся файлы: локальная папка, S3, что-то ещё. Настройки дисков лежат в config/filesystems.php. Из коробки их три, и именно их путаница создаёт большинство вопросов новичка.
| Диск | Куда пишет | Виден снаружи по URL |
|---|---|---|
local | storage/app/private | Нет |
public | storage/app/public | Да, но только через симлинк |
s3 | внешнее хранилище S3 | Да, по URL бакета |
Расположение папок для дисков local и public в Laravel 11 поменялось по сравнению со старыми версиями: приватные файлы теперь по умолчанию лежат в storage/app/private. Если читаете гайд под Laravel 8 и там путь storage/app — это про старую версию.
Почему нельзя просто писать в public/
Соблазн есть: папка public/ отдаётся веб-сервером напрямую, ссылка работает сразу. Но пользовательские загрузки в public/ — это мина. При деплое через git эту папку перезаписывают, при выкатке новой версии файлы теряются, а сам каталог с кодом становится вперемешку с загрузками. Правильный путь — писать в storage/ и связать его с public/ симлинком. Так код и данные разделены.
Шаг за шагом: загрузка файла с публичной ссылкой
-
Создайте симлинк. Выполните в корне проекта:
php artisan storage:linkПроверка: в папке
public/появится ссылкаstorage, указывающая наstorage/app/public. На Windows без прав администратора команда может не создать симлинк — тогда запустите консоль от имени администратора. -
Опишите маршрут и форму. Простейший контроллер для приёма файла:
<?php // app/Http/Controllers/AvatarController.php namespace App\Http\Controllers; use Illuminate\Http\Request; class AvatarController extends Controller { public function store(Request $request) { // Проверяем, что пришёл файл-изображение до 2 МБ $validated = $request->validate([ 'avatar' => 'required|image|max:2048', ]); // Сохраняем на диск public в папку avatars // store() сам сгенерирует уникальное имя $path = $request->file('avatar')->store('avatars', 'public'); // $path вида avatars/9xKq...jpg — это относительный путь на диске return back()->with('avatarUrl', asset('storage/' . $path)); } }Проверка: после отправки формы в
storage/app/public/avatarsпоявится файл со случайным именем. -
Получите публичную ссылку. Файл лежит на диске под относительным путём. URL строится через фасад
Storage:use Illuminate\Support\Facades\Storage; // $path — то, что вернул store(), например avatars/9xKq...jpg $url = Storage::disk('public')->url($path); // Вернёт что-то вроде /storage/avatars/9xKq...jpgПроверка: вставьте
$urlв<img src>или откройте прямо в браузере — картинка должна отобразиться.
Запомните разделение:store()возвращает путь на диске, а не готовую ссылку. Ссылку строитStorage::url()илиasset('storage/...'). Хранить в базе нужно относительный путь, а не полный URL — иначе при смене домена или переезде на S3 всё сломается.
Ещё три вещи, которые новичок пропускает
- Валидация размера и типа. Правило
imageпроверяет, что это картинка,max:2048— что не больше 2 МБ (значение в килобайтах). Без этого пользователь загрузит 200 МБ видео с расширением .jpg. - Лимиты PHP. Laravel не поднимет файл больше, чем разрешает сам PHP. Смотрите
upload_max_filesizeиpost_max_sizeвphp.ini. Если форма молча теряет большой файл — почти всегда дело в них, а не в коде. - Приватные файлы не отдавайте симлинком. Паспорта, договоры и прочее кладите на диск
local(папкаstorage/app/private) и отдавайте через контроллер с проверкой прав и методомStorage::download(), а не по прямой ссылке.
Если не получилось
- Файл сохранился, но по ссылке 404. Не создан симлинк. Выполните
php artisan storage:link. Если он уже есть, но битый — удалите ссылкуpublic/storageвручную и создайте заново. - Ошибка
The stream or file could not be opened ... Permission denied. У пользователя веб-сервера нет прав на запись вstorage/. Выдайте права каталогуstorageи его подпапкам, владельцем сделайте пользователя, под которым работает PHP-FPM или nginx. - Форма отправилась, но
$request->file('avatar')пустой. В теге<form>не указанenctype="multipart/form-data". Без него файл не уйдёт на сервер. - Большой файл молча пропадает. Превышен
post_max_sizeилиupload_max_filesizeв php.ini. Поднимите оба значения и перезапустите PHP-FPM. - Ошибка валидации
The avatar must be an imageна явной картинке. Не установлено или отключено расширениеfileinfoв PHP — Laravel не может определить MIME-тип. Проверьтеphp -m | grep fileinfo.
Что делать дальше
Когда локальная загрузка работает, следующий шаг — вынести файлы во внешнее хранилище, чтобы они не занимали диск сервера и переживали редеплой. В Laravel это делается сменой драйвера на s3 в config/filesystems.php без переписывания кода контроллера: тот же store() и Storage::url() будут работать с S3-совместимым хранилищем. Обработка изображений (ресайз, превью) — отдельная тема, здесь мы её не трогали; смотрите в сторону пакета для работы с картинками, но подключайте его уже после того, как базовая загрузка стабильна.
Разбор научной статьи: Сложный текст — понятным языком, с терминами
Спросить за 12 ₽Источники: Laravel 11 — File Storage (официальная документация), Laravel 11 — File Uploads в разделе Requests
Частые вопросы
У меня Laravel 10, а не 11 — инструкция подойдёт?
Что хранить в базе — путь или полную ссылку на файл?
Почему картинка не открывается, хотя файл точно сохранился?
Как ограничить размер загружаемого файла?
Можно ли раздавать приватные файлы, например договоры, по прямой ссылке?
Что делать, если файл на проде не загружается, хотя локально всё работало?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.