Привет! Я Вася из Fastfox. В этой статье собрал практический чек-лист и разбор типичных грабель при размещении проектов на Laravel в условиях виртуального хостинга. Поговорим о том, как корректно работать с public/, обеспечить доступ к медиа через storage:link даже при запрете симлинков, закрыть .env, выбрать верный php-cli для artisan, запустить cron и настроить очереди без Supervisor. Для полноты картины покажу и эталонную схему с Supervisor для случаев, когда вы переедете на управляемую систему или облачный VDS.
Файловая структура Laravel и public dir
Идеальная схема — когда корень сайта указывает на директорию public/. Тогда PHP и веб-сервер не видят содержимого app, vendor, storage. На виртуальном хостинге у вас может быть ограничение: «корень сайта» фиксирован и указывает на корневую папку аккаунта или поддомен, и изменить путь до public/ нельзя. Есть три рабочих варианта.
Вариант 1 (лучший): сменить DocumentRoot на public/
Если панель позволяет выбрать папку сайта — укажите .../project/public. Это нивелирует проблемы с безопасностью и статикой. Дополнительно убедитесь, что в public/ есть .htaccess из коробки Laravel (он занимается маршрутизацией на index.php).
Вариант 2: переадресация в public/ через .htaccess
Если изменить корень нельзя, оставьте проект на один уровень выше, а в корне добавьте минимальные правила для переадресации всех запросов в public/:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/public/
RewriteRule ^(.*)$ public/$1 [L,QSA]
</IfModule>
Это быстрый вариант, который хорошо работает на большинстве виртуальных тарифов. Не забывайте, что в public/ должен лежать стандартный .htaccess Laravel с правилом RewriteRule ^ index.php [L] для маршрутизации.
Коротко: если видите 404 на всех маршрутах — проверьте, что включён
mod_rewriteи оба.htaccessлежат на местах.
Вариант 3 (как временная мера): перенести содержимое public/ в корень
Иногда проще переложить файлы из public/ в корень сайта и поправить пути в index.php. В оригинале там ссылки вида __DIR__.'/../vendor/autoload.php'. После переноса замените их на пути без ../:
// файл index.php после переноса содержимого public/ в корень
require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
Минусы: корень теперь «видит» все служебные директории, придётся сильнее полагаться на ограничения веб-сервера и следить за правилами доступа. Рассматривайте как временное решение.
storage:link на виртуальном хостинге
Команда php artisan storage:link создаёт символическую ссылку public/storage на storage/app/public. На некоторых виртуальных тарифах симлинки запрещены политикой безопасности. Попробуйте сначала:
php artisan storage:link
php artisan storage:link --relative
Если команда отработала, но статика 403/404 — возможно, в конфигурации Apache запрещены симлинки. Иногда помогает добавить в public/.htaccess:
Options +FollowSymLinks
Options +SymLinksIfOwnerMatch
Когда симлинки полностью запрещены, используйте один из обходных путей:
- Проксируйте файлы через контроллер. Это медленнее, но работает везде.
- Храните «общедоступные» медиа сразу в
public/(настроив файловую систему Laravel под это), если требования безопасности и регламенты проекта позволяют.
Маршрут-прокси для файлов на «паблик-диске»:
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Route;
Route::get('storage/{path}', function ($path) {
return Storage::disk('public')->response($path);
})->where('path', '.*');
И не забудьте корректно указать FILESYSTEM_DISK=public в .env (или FILESYSTEM_DRIVER для старых версий) и опубликовать права доступа к storage/.

.env: безопасность и права
Файл .env должен быть недоступен из веба при любом из трёх вариантов выше. Самый простой барьер для Apache — правило в public/.htaccess или в корне сайта, если используете Вариант 2/3:
<Files ".env">
Require all denied
</Files>
Права на .env ставьте максимально строгие: для одного пользователя под PHP-FPM обычно достаточно 600 или 640 с корректной группой. Проверьте, чтобы PHP-процесс запускался от вашего пользователя (часто так и есть на виртуальном хостинге). Для директорий storage и bootstrap/cache используйте 775, для файлов — 664. Пример команд:
cd ~/www/project
find storage bootstrap/cache -type d -exec chmod 775 {} \;
find storage bootstrap/cache -type f -exec chmod 664 {} \;
На некоторых тарифах удобнее настроить маску по умолчанию, чтобы новые файлы создавались с «дружественными» правами для веб-сервера:
umask 002
В продакшене обязательно выключите дебаг:
APP_ENV=production,APP_DEBUG=false. Проверяйте, что кэш конфигурации обновлён:php artisan config:cache. И сразу включайте HTTPS: это не только про SEO и куки, но и про безопасность форм авторизации — оформите SSL-сертификаты.
php-cli: выбрать правильный бинарь для artisan и cron
Частая ловушка: веб-сервер крутится на PHP 8.2, а в консоли у вас по умолчанию PHP 7.4. В результате artisan падает на синтаксисе или несовместимых пакетах. Перед настройкой cron определите путь к нужной версии:
php -v
which php
php82 -v
/usr/local/bin/php82 -v
На большинстве панелей есть несколько алиасов: php81, php82 и т. п. Узнайте у поддержки/в панели точный путь и используйте его в заданиях cron и при запуске artisan. Примеры далее буду показывать с обобщённым /usr/bin/php — замените на свой.
Cron и планировщик Laravel
Laravel ожидает, что системный планировщик вызовет artisan schedule:run раз в минуту. На виртуальном хостинге cron обычно доступен и запускается от вашего пользователя. Минимальная запись:
* * * * * /usr/bin/php /home/user/www/project/artisan schedule:run --quiet
Чтобы исключить гонки и параллельные запуски в условиях долгих задач, добавьте блокировку с flock и заведите каталог для локов:
mkdir -p /home/user/cronlocks
* * * * * /usr/bin/flock -n /home/user/cronlocks/schedule.lock /usr/bin/php /home/user/www/project/artisan schedule:run --quiet
Дальше вся логика расписаний живёт в app/Console/Kernel.php. Старайтесь избегать длительных бесконечных процессов в schedule() — на виртуальном хостинге лучше дробить задачи и запускать их часто, с явными ограничениями по времени.
Очереди без Supervisor: рабочие паттерны
На виртуальном хостинге вы чаще всего не можете держать фоновые процессы постоянно. Но очереди нужны. Рабочие варианты:
Вариант А: драйвер database с «одноразовым» воркером
Создайте таблицу задач и переключитесь на драйвер database:
php artisan queue:table
php artisan migrate
В .env:
QUEUE_CONNECTION=database
Дальше запускайте воркера каждую минуту, ограничивая время работы и избегая параллельных копий через flock:
* * * * * /usr/bin/flock -n /home/user/cronlocks/qworker.lock /usr/bin/php /home/user/www/project/artisan queue:work database --sleep=1 --tries=3 --max-time=55 --timeout=60 --queue=default --stop-when-empty
Параметр --stop-when-empty гарантирует завершение воркера, если очередь опустела. Это удобно на виртуальном хостинге: cron сам перезапускает воркера ежеминутно.
Вариант Б: работать через schedule()
Иногда удобнее запускать воркер из планировщика, чтобы централизованно управлять логикой:
// app/Console/Kernel.php
protected function schedule(\Illuminate\Console\Scheduling\Schedule $schedule)
{
$schedule->command('queue:work database --sleep=1 --tries=3 --max-time=55 --timeout=60 --stop-when-empty')
->withoutOverlapping()
->runInBackground()
->everyMinute();
}
В cron при этом достаточно одного задания schedule:run. Минус — если на тарифе жёсткие лимиты по времени выполнения, долгие бэкграунд-процессы могут быть преждевременно остановлены.
Вариант В: Redis + короткоживущие воркеры
Если тариф даёт Redis, получите меньшие накладные расходы и более стабильную обработку. В остальном схема та же: запускать короткоживущие queue:work через cron с блокировкой. Про плюсы и нюансы кешей читайте в разборе Redis и Memcached для PHP.
Не используйте бесконечные циклы
while (true)на виртуальном хостинге. Их убьёт менеджер процессов/лимиты, а вы получите «залипшие» задания и непредсказуемые задержки.

Supervisor и Horizon: эталон для постоянных воркеров
Когда проект вырастет и потребуется постоянный пул воркеров, входящие веб-хуки, широковещательные события — используйте Supervisor или систему инициализации (systemd/runit). На виртуальном хостинге это, как правило, недоступно, поэтому показан эталон для окружений, где вы управляете системой (например, на выделенной машине или облачном VDS):
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=/usr/bin/php /var/www/project/artisan queue:work redis --sleep=1 --tries=3 --timeout=60
autostart=true
autorestart=true
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/supervisor/laravel-worker.log
stopwaitsecs=3600
stopasgroup=true
killasgroup=true
environment=APP_ENV=production
Для Horizon добавляют отдельную программу с artisan horizon и таймаутами остановки. Важные практики: логируйте stdout/err, используйте stopasgroup/killasgroup, считайте, сколько воркеров нужно на ваш SLA, и держите запас по CPU/RAM.
Оптимизация билда и кэшей
Перед выкладкой на виртуальный хостинг прогоните кэши, чтобы снизить нагрузку и ускорить отклик:
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
После обновления зависимости/конфигов не забудьте инвализировать и пересобрать кэш. Отдельно проверьте, что версия PHP-CLI совпадает с той, на которой раскладывались кэши.
.htaccess и PHP-настройки на уровне каталога
Если PHP работает через mod_php, многие параметры можно задавать в .htaccess через php_value. На PHP-FPM лучше использовать .user.ini в корне документа (обычно public/):
memory_limit=256M
upload_max_filesize=50M
post_max_size=50M
max_execution_time=60
Проверяйте, что ограничения разумны для проекта и тарифа. Для крупных загрузок и генерации миниатюр стоит предусмотреть фоновые очереди, чтобы не упираться в max_execution_time.
Диагностика типовых ошибок
- Все маршруты отдают 404: проверьте
mod_rewrite, корректность.htaccessв корне и вpublic/, а также права наpublic/index.php. - Скачивается файл вместо отображения страницы: у провайдера отключён PHP для каталога. Уточните обработчик PHP в панели, проверьте, что
.htaccessне блокирует интерпретацию. - 403 при обращении к
/storage/...: не работает симлинк. Проверьтеstorage:link,Options +FollowSymLinks, владельца/права каталогов. - Artisan падает на синтаксисе: в консоли другая версия PHP. Используйте явный путь к
php-cli. - Cron «молчит»: включите логирование вывода команд во временный файл, проверьте
crondу провайдера и используйтеflockдля борьбы с гонками. - Очереди стопорятся: для драйвера
databaseпроверьте индексы таблицы jobs, таймауты, количество попыток, а также блокировки транзакций в моменты пиковых вставок.
Чек-лист деплоя Laravel на виртуальном хостинге
- Скопируйте проект и проверьте, что корень сайта указывает на
public/. Если нет — примените Вариант 2 или 3. - Сгенерируйте
.env, установите строгие права и запретите веб-доступ через правилоFiles ".env". - Убедитесь, что
php-cliсовпадает по версии с вебом. Используйте явный путь в cron. - Соберите кэши:
config,route,view,event. - Настройте
storage:link. Если симлинки запрещены — используйте маршрут-прокси или храните публичные файлы вpublic/. - Создайте задания cron:
schedule:runраз в минуту сflock. При необходимости — «одноразовый» воркер очередей. - Проверьте права на
storageиbootstrap/cache, настроьте.user.iniпод вашу нагрузку. - Пройдитесь по логам приложения и веб-сервера после первого запуска, убедитесь, что нет 500/403/404 на статике и API.
Итоги
Laravel прекрасно работает на виртуальном хостинге, если грамотно организовать доступ к public/, закрыть .env, аккуратно обращаться с storage:link и запускать планировщик/очереди через cron. Когда проект вырастет и потребуется гарантированная обработка задач в реальном времени, переезжайте на окружение с Supervisor — это даст контролируемые долгоживущие воркеры, гибкую масштабируемость и стабильную задержку выполнения.


