Файловые хранилища и S3 в Laravel: делаем правильно сразу
Приватные файлы, отданные напрямую, ссылки, которые протухли, и картинки, слитые в публичный бакет. Разбираем, как настроить диски и S3 в Laravel так, чтобы не переделывать потом.

Типичная история: проект стартовал на local-диске, файлы сложили в storage/app, приватные документы отдавали через Storage::url() — и обнаружили, что они доступны любому по прямой ссылке. Или наоборот: перешли на S3, а команда Storage::url() начала отдавать ссылки на приватный бакет, которые не открываются. Оба случая — следствие того, что диск подключили не под ту задачу.
Разберём, как в Laravel 11.x устроены файловые диски, чем публичный доступ отличается от временных ссылок и на каких настройках S3-совместимого хранилища спотыкаются чаще всего. Формат — практика: у вас уже есть проект, и вопрос в том, как загрузку и раздачу файлов сделать правильно, а не переделывать через полгода.
Диски: local, public и s3 — не взаимозаменяемы
Laravel работает с файлами через компонент Flysystem. Всё описано в config/filesystems.php, где заданы диски — именованные конфигурации хранилища. По умолчанию их три, и путать их — главная ошибка.
- local —
storage/app/private(в свежих версиях путь именно private). Файлы недоступны из веба напрямую. Для документов, которые нельзя отдавать всем. - public —
storage/app/public. Доступны по URL только послеphp artisan storage:link, который создаёт симлинкpublic/storageна этот каталог. Для аватарок, обложек, публичных картинок. - s3 — объектное хранилище (AWS S3, Yandex Object Storage, MinIO, Selectel и другие S3-совместимые). Доступ зависит от политики бакета.
Ключевой момент: Storage::url($path) для public вернёт рабочую ссылку, а для local — путь, который никуда не ведёт. Для приватных файлов url в лоб не работает вообще — их отдают либо через контроллер, либо через временную ссылку.
Выбор диска — это не «где хранить», а «кто имеет право видеть файл». Сначала ответьте на второй вопрос, потом настраивайте первый.
Публичный доступ против приватного: три сценария
Публичные файлы
Аватар пользователя, который и так виден всем. Кладёте на public или в публичный бакет S3, отдаёте прямой ссылкой. Кеш браузера и CDN работают из коробки, нагрузки на PHP нет.
// диск public
$path = $request->file('avatar')->store('avatars', 'public');
// в шаблоне
$url = Storage::disk('public')->url($path);Приватные файлы через контроллер
Договор, скан паспорта, счёт. Файл лежит на приватном диске, а доступ проверяет ваш код: авторизация, принадлежность файла пользователю, права. PHP отдаёт файл потоком.
use Illuminate\Support\Facades\Storage;
public function download(Document $document)
{
// проверяем право на конкретный документ
$this->authorize('view', $document);
// download отдаёт файл с заголовком attachment
return Storage::disk('local')->download($document->path);
}Минус — каждый запрос проходит через ваш PHP-процесс. Для крупных файлов и высокой нагрузки это дорого.
Приватные файлы через временные ссылки S3
Компромисс: файл приватный, но вы генерируете подписанную ссылку с истечением срока. Клиент качает напрямую из S3, минуя ваш сервер. Работает только для дисков, поддерживающих temporaryUrl — это S3-совместимые хранилища, но не локальный диск.
use Illuminate\Support\Facades\Storage;
// ссылка живёт 5 минут
$url = Storage::disk('s3')->temporaryUrl(
$document->path,
now()->addMinutes(5)
);Сравнение подходов
| Подход | Кто проверяет доступ | Нагрузка на PHP | Когда выбирать |
|---|---|---|---|
| Публичный диск / бакет | Никто, файл открыт | Нет | Данные и так публичны |
| Отдача через контроллер | Ваш код на каждый запрос | Есть, на каждую отдачу | Мелкие приватные файлы, редкие скачивания |
| Временная ссылка S3 | Ваш код при генерации ссылки | Только на генерацию | Крупные приватные файлы, частые скачивания |
Настройка S3-совместимого хранилища
Для работы с S3 нужен пакет league/flysystem-aws-s3-v3. Установите его отдельно — в базовую поставку он не входит:
composer require league/flysystem-aws-s3-v3 "^3.0"Дальше — переменные окружения в .env. Реальные значения не коммитьте, храните в секретах.
AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY
AWS_SECRET_ACCESS_KEY=YOUR_SECRET_KEY
AWS_DEFAULT_REGION=ru-central1
AWS_BUCKET=your-bucket-name
AWS_ENDPOINT=https://storage.yandexcloud.net
AWS_USE_PATH_STYLE_ENDPOINT=falseТочные имена этих переменных и структуру блока s3 в config/filesystems.php сверяйте с актуальной документацией Laravel вашей версии — состав ключей менялся между релизами. Три параметра, на которых спотыкаются чаще всего:
- AWS_ENDPOINT — обязателен для всего, кроме самого AWS. Для Yandex, Selectel, MinIO без него SDK будет стучаться в амазоновский адрес и получать ошибки соединения.
- AWS_USE_PATH_STYLE_ENDPOINT —
trueдля MinIO и части провайдеров, где бакет идёт в пути (endpoint/bucket/key), а не поддоменом. Неверное значение даёт 403 или невозможность найти бакет. - AWS_DEFAULT_REGION — у S3-совместимых хранилищ регион свой; амазоновский
us-east-1по инерции здесь не подойдёт.
После правки .env сбросьте кеш конфига, если он собран: php artisan config:clear. Иначе новые значения не подхватятся, а вы будете искать ошибку в коде.
Что ломается на практике
- storage:link не выполнили. Файлы на
publicесть, а по URL — 404. Симлинк создаётся один раз командойphp artisan storage:link; при деплое в чистый каталог его нужно повторить. - Приватный файл на публичном диске. Скан документа положили на
public, потому что «так ссылка сразу работает». Ссылку по перебору или из логов найдут — данные утекут. Приватное — только наlocalили приватный бакет. - Проверка размера через client-имя файла. Валидируйте загрузку правилами Laravel (
file,max,mimes), а не доверием к тому, что прислал клиент. Имя и MIME от браузера подделываются. - Публичный бакет вместо приватного с подписью. Проще открыть бакет целиком, но тогда любой файл доступен по угадываемому ключу. Держите бакет приватным и раздавайте через
temporaryUrl. - Хардкод диска в коде. Вместо
Storage::disk('s3')по всему проекту задайте дефолтный диск черезFILESYSTEM_DISKи обращайтесь кStorage::без явного имени там, где это уместно. Тогда переезд с local на s3 — вопрос одной переменной.
Как выбрать диск под задачу
Короткий критерий без «зависит от задачи»:
- Файл виден всем без ограничений → public или публичный бакет.
- Файл приватный, мелкий, скачивают редко → local + отдача через контроллер с проверкой прав.
- Файл приватный, крупный или качают часто → приватный S3-бакет +
temporaryUrl.
Локальную разработку удобно держать на local/public, а прод — на S3, переключая одной переменной окружения. Тесты гоняйте на фейковом диске: Storage::fake('s3') в начале теста подменяет реальное хранилище на локальную заглушку, и вы проверяете факт загрузки без обращения к сети.
Что настроить дальше
Когда базовая раздача работает, займитесь тремя вещами. Первое — время жизни временных ссылок: слишком долгие сводят на нет всю приватность, ставьте минуты, а не часы. Второе — CORS-политика бакета, если файлы грузятся из браузера напрямую в S3 (прямая загрузка минуя PHP — отдельная тема, здесь её не разворачиваем). Третье — резервное копирование бакета и версионирование объектов: удаление файла в S3 без версионирования необратимо. Настройку версионирования и жизненного цикла объектов делают на стороне провайдера, а не в Laravel.
Дефектный акт по фото: Осмотр по фотографиям: классификация дефектов, ссылки на нормы, требования к устранению. Готовый акт в Word.
Спросить за 80 ₽Источники: Laravel — File Storage (официальная документация)
Частые вопросы
У меня другая версия Laravel, конфиг filesystems выглядит иначе — что делать?
Storage::url() возвращает несуществующую ссылку, файл при этом загрузился. В чём дело?
Можно ли получить временную ссылку для локального диска?
Не сработало: после смены значений в .env S3 всё равно не подключается.
Насколько безопасно держать бакет публичным ради простоты?
Как проверить загрузку файлов в тестах, не обращаясь к реальному S3?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.