Часть 2 — Структура проекта, первый запрос и nginx
Что сделаем. Разложим по полочкам проект ~/recoursia/project-laravel, установленный в части 1:
что в каком каталоге лежит и кто туда пишет. Прочитаем точку входа public/index.php
и поймём, как запрос вообще попадает в наш код. Напишем первый маршрут и первый шаблон
Blade, получим настоящий HTTP-ответ и измерим его. Затем уйдём со встроенного сервера
на nginx: соберём vhost для домена project-laravel.rs, по дороге сделаем две типовые ошибки
и разберём, почему они дают 403 и 404.
Что понадобится. Проект Laravel из части 1; стек из блока Linux — nginx 1.24,
PHP-FPM 8.3 и локальная зона .rs через dnsmasq (lesson.linux.05–lesson.linux.06);
sudo для правки /etc/nginx; терминал и редактор.
Все команды, файлы и снимки в этой части — настоящие захваты прогона на стенде
mint (Linux Mint 22 / Ubuntu 24.04, пользователь student). Ничего не сочинено:
что напечатала машина, то и на слайде.
Карта каталогов: что где лежит
Проект project-laravel — это не «одна папка со скриптами», а фиксированная структура: у каждого
каталога своя роль, и Laravel ищет файлы именно там, где договорились. Вот вся карта;
дальше мы зайдём в каждый каталог отдельно.
project-laravel/ # ~/recoursia/project-laravel — корень проекта├── app/ # НАШ код│ ├── Http/Controllers/Controller.php # базовый контроллер (пригодится в части 3)│ ├── Models/User.php # модель пользователя│ └── Providers/AppServiceProvider.php├── bootstrap/ # как собирается приложение│ ├── app.php # маршруты, middleware, обработка ошибок│ ├── providers.php # список провайдеров│ └── cache/ # кэш сборки, нужны права на запись├── config/ # 10 файлов конфигурации├── database/│ ├── migrations/ # 3 миграции: users, cache, jobs│ ├── factories/UserFactory.php # генератор тестовых строк│ ├── seeders/DatabaseSeeder.php # наполнение базы│ └── database.sqlite # файл базы по умолчанию├── public/ # ЕДИНСТВЕННАЯ папка, которую видит nginx│ ├── index.php # точка входа: сюда приходит любой запрос│ ├── .htaccess # настройка для Apache, nginx её не читает│ ├── favicon.ico # 0 байт│ └── robots.txt # 24 байта├── resources/│ ├── views/welcome.blade.php # шаблоны Blade│ ├── css/app.css # исходники для сборщика Vite│ └── js/app.js├── routes/│ ├── web.php # маршруты сайта (HTTP)│ └── console.php # маршруты консоли├── storage/ # всё, что приложение пишет само│ ├── app/ # private/ и public/ — файлы и загрузки│ ├── framework/ # cache/, sessions/, testing/, views/│ └── logs/ # логи приложения├── vendor/ # пакеты Composer — поставили в части 1├── artisan # консольный вход в приложение├── package.json # сборка ассетов (Vite)├── .env # секреты и настройки этой машины└── .gitignore
Каталоги vendor/, composer.json, composer.lock и .env появились в части 1 при
установке — здесь они показаны для полноты. Остальное взято из захватов этой части:
tree и ls — для каталогов, файловые захваты — для bootstrap/app.php,
bootstrap/providers.php, artisan, package.json и .gitignore. Ни одного
выдуманного пути: vendor/autoload.php подключает public/index.php, а .env
и /vendor перечислены в .gitignore проекта.
app/ — здесь живёт наш код
app├── Http│ └── Controllers│ └── Controller.php├── Models│ └── User.php└── Providers └── AppServiceProvider.php5 directories, 3 files
Разбор команды
Смотрим, что каркас оставил нам под собственный код: какие подкаталоги есть в app/ и какие файлы в них лежат.
tree app -L 3 --dirsfirst| Токен | Зачем он и что меняет |
|---|---|
tree | утилита, печатающая содержимое каталогов деревом: вложенность видна по отступам и линиям, в отличие от ls -R |
app | аргумент — корень обхода. Без него tree напечатал бы весь проект, включая vendor/ с тысячами файлов пакетов |
-L 3 | ограничить глубину тремя уровнями: app → Http → Controllers → файл. Глубже в app/ ничего нет, лишний вывод не нужен |
--dirsfirst | печатать каталоги раньше файлов: сначала видна структура, потом содержимое |
Что произойдёт
Печатает дерево app/: подкаталоги Http (с вложенным Controllers), Models, Providers и три файла — Controller.php, User.php, AppServiceProvider.php. Итог: «5 directories, 3 files».
На что смотреть
Итоговая строка: каталогов пять, хотя «своих» подкаталогов три — посчитаны сам app/ и промежуточный Http/. Файлов ровно три, и все три мы разберём на этом слайде.
Частые ошибки
tree: command not found— в Ubuntu/Mint пакет не входит в базовую установку:sudo apt install tree.- Без
-Lвывод уходит вглубь на все уровни — на каталоге проекта это тысячи строк изvendor/.
Три подкаталога и три файла — вот и всё, что каркас отдаёт нам «на откуп».
Итоговая строка tree говорит «5 directories, 3 files»: каталогов пять, потому что
посчитаны сам app/ и промежуточный Http/.
Models/User.php— модель. В блоке «Базы данных» вы писали запросы через PDO руками:prepare(), маркеры,execute(),fetchAll(). Модель — это класс, который берёт эту работу на себя: одна запись таблицы становится объектом, а обращение к свойству — значением колонки. Подробно — в части 4.Http/Controllers/Controller.php— базовый контроллер. Пока он пустой и ничем не используется: в этой части маршруты будут вызывать обычные замыкания. Свои контроллеры мы добавим в части 3.Providers/AppServiceProvider.php— провайдер: класс, в котором приложение «регистрирует» свои сервисы. Он упомянут вbootstrap/providers.php, и мы увидим его на следующем же слайде про сборку приложения.
Почему код лежит отдельно от шаблонов и конфигов? Потому что у каталогов разные права
и разный жизненный цикл: app/ меняете вы, storage/ пишет само приложение,
а public/ видит веб-сервер.
database/ и resources/: схема данных и шаблоны
database├── factories│ └── UserFactory.php├── migrations│ ├── 0001_01_01_000000_create_users_table.php│ ├── 0001_01_01_000001_create_cache_table.php│ └── 0001_01_01_000002_create_jobs_table.php├── seeders│ └── DatabaseSeeder.php└── database.sqlite4 directories, 6 files
Разбор команды
Проверяем, что приехало с каркасом в database/: описание схемы, наполнение и файл базы.
tree database -L 2 --dirsfirst| Токен | Зачем он и что меняет |
|---|---|
tree | вывод деревом — видно и подкаталоги, и файлы внутри них |
database | корень обхода: каталог проекта, а не сервер СУБД |
-L 2 | глубина 2 уровня: сам database и его подкаталоги. Файлы внутри migrations/ при этом ещё видны |
--dirsfirst | каталоги раньше файлов |
Что произойдёт
Видны factories/ (1 файл), migrations/ (3 файла), seeders/ (1 файл) и файл database.sqlite. Итог: «4 directories, 6 files».
На что смотреть
database.sqlite — база по умолчанию: отдельный сервер поднимать не нужно, это один файл. Три миграции каркаса: create_users_table, create_cache_table, create_jobs_table. Арифметика итога: 3 миграции + UserFactory.php + DatabaseSeeder.php + database.sqlite = 6 файлов.
Частые ошибки
- Без
-Lдерево уходит вглубь на все уровни: здесь разница невелика, но на корне проекта вывод стал бы огромным из-заvendor/. - Файлы из
database/наружу не отдаются: их нет вpublic/. Это код, а не контент сайта.
migrations/ — описание таблиц в коде. В блоке «Базы данных» вы создавали таблицу
rest в PHPMyAdmin руками: поля, varchar(255), auto_increment, первичный ключ.
Миграция делает то же самое, но файлом, который лежит в git и выполняется командой
php artisan migrate. Отсюда — воспроизводимость: у коллеги и на сервере получится
та же схема, а не «как у меня».
seeders/ — наполнение базы данными (как ваш truncate + вставка 42 ресторанов
циклом, только оформленная команда). factories/ — генератор правдоподобных
тестовых строк.
database.sqlite — файл базы SQLite, который каркас использует по умолчанию:
отдельный сервер поднимать не нужно. В части 4 переключимся на MariaDB.
resources├── css│ └── app.css├── js│ └── app.js└── views └── welcome.blade.php4 directories, 3 files
Разбор команды
Смотрим, где живут шаблоны и исходники ассетов.
tree resources -L 3 --dirsfirst| Токен | Зачем он и что меняет |
|---|---|
tree | вывод деревом |
resources | корень обхода |
-L 3 | три уровня: достаточно, чтобы увидеть файлы внутри css/, js/ и views/ |
--dirsfirst | каталоги раньше файлов |
Что произойдёт
Три подкаталога — css/, js/, views/ — и три файла: app.css, app.js, welcome.blade.php. Итог: «4 directories, 3 files».
На что смотреть
views/welcome.blade.php — шаблон стартовой страницы: именно его соберёт первый запрос. css/ и js/ — исходники для сборщика Vite, браузеру они не отдаются: наружу идёт результат сборки из public/build/.
Частые ошибки
- Файлов из
resources/вpublic/нет, поэтому «страница без стилей» здесь — не проблема прав, а отсутствие сборки Vite (проверяем её отдельно).
views/welcome.blade.php — шаблон стартовой страницы. Blade — это PHP-файл
с короткими метками вроде {{ $name }}; Laravel превращает его в обычный PHP
и уже его выполняет. Свои шаблоны положим сюда же в этой и в третьей части.
css/app.css и js/app.js — исходники для сборщика Vite, а не то, что отдаёт
nginx. Обратите внимание на разницу: файлы из resources/ наружу не видны, браузеру
отдаётся результат сборки из public/build/. Его здесь нет — и это нормально,
обсудим на слайде про Vite.
storage/ — всё, что приложение пишет само
storage├── app│ ├── private│ └── public├── framework│ ├── cache│ │ ├── data│ │ └── locks│ ├── sessions│ ├── testing│ └── views└── logs12 directories, 0 files
Разбор команды
Смотрим каталог, в который приложение пишет само: кэш, сессии, логи, загрузки.
tree storage -L 3 --dirsfirst| Токен | Зачем он и что меняет |
|---|---|
tree | вывод деревом |
storage | корень обхода |
-L 3 | три уровня: видно framework/cache/data и framework/cache/locks, которые на глубине 2 не показались бы |
--dirsfirst | каталоги раньше файлов |
Что произойдёт
Дерево из 12 каталогов и 0 файлов: app/private, app/public, framework/{cache,sessions,testing,views}, logs. Итог: «12 directories, 0 files».
На что смотреть
Файлов ноль — на момент съёмки Laravel ещё ни разу не ответил на запрос. Запомните два каталога: framework/views/ (сюда попадут скомпилированные шаблоны) и logs/ (первое место, куда смотреть при ошибке 500).
Частые ошибки
- «0 files» — норма до первого запроса, а не признак поломки.
- Содержимое
storage/— кэш и данные, в git ему не место; второй такой каталог —bootstrap/cache/.
Это каталог, который приложение изменяет во время работы (второй такой —
bootstrap/cache/, он в таблице ниже). Сейчас здесь 12 каталогов и 0 файлов:
на момент съёмки Laravel ещё ни разу не ответил на запрос.
| Каталог | Что там появляется |
|---|---|
framework/views/ |
скомпилированные шаблоны Blade (увидим в этой части) |
framework/cache/data/ |
кэш приложения |
framework/sessions/ |
сессии, если драйвер — файлы |
framework/testing/ |
артефакты тестов (часть 6) |
logs/ |
лог приложения — первое место, куда смотреть при ошибке 500 |
app/private/, app/public/ |
файлы и загрузки пользователей |
bootstrap/cache/ (рядом) |
кэш конфигурации и списка сервисов |
Отсюда два следствия, которые мы ещё проверим: каталогу нужны права на запись для того пользователя, от которого работает PHP-FPM (слайд про права), и его содержимое не должно попадать в git — это кэш, а не код.
public/ — единственная дверь наружу
total 20drwxrwxrwx 2 student student 4096 Sep 30 13:56 .drwxrwxrwx 12 student student 4096 Oct 17 10:19 ..-rw-rw-rw- 1 student student 740 Sep 30 13:56 .htaccess-rw-rw-rw- 1 student student 0 Sep 30 13:56 favicon.ico-rw-rw-rw- 1 student student 543 Sep 30 13:56 index.php-rw-rw-rw- 1 student student 24 Sep 30 13:56 robots.txt
Разбор команды
Смотрим единственную папку, которую видит веб-сервер.
ls -la public| Токен | Зачем он и что меняет |
|---|---|
ls | список содержимого каталога |
-la | два флага слитно: -l — длинный формат (права, владелец, размер в байтах, дата), -a — показывать и скрытые файлы, имена которых начинаются с точки. Без -a мы не увидели бы .htaccess, без -l — что favicon.ico пустой |
public | аргумент — каталог, содержимое которого печатаем |
Что произойдёт
Четыре файла: .htaccess (740 байт), favicon.ico (0), index.php (543), robots.txt (24). Каталога build/ нет — ассеты не собраны.
На что смотреть
index.php — точка входа: через FastCGI nginx передаёт в PHP-FPM именно его. .htaccess — настройка для Apache, nginx её не читает. Права -rw-rw-rw- — особенность этого стенда, а не требование Laravel.
Частые ошибки
- Строка
total 20— это занятые блоки, а не сумма байт файлов. - Без
-aфайлов покажется три, и возникнет ложный вывод, что скрытых настроек вpublic/нет.
Четыре файла — и это весь «сайт», насколько его видит nginx:
index.php(543 байта) — точка входа. Через FastCGI веб-сервер передаёт в PHP-FPM именно его; разбор запроса начнётся внутри. Это тот самыйindex.php, путь к которому вы настраивали вlesson.linux.06и разбирали вlesson.http.i.01: процесс PHP выполняет файл, вывод копится в буфере, буфер становится телом ответа..htaccess(740 байт) — файл настроек для Apache. Мы работаем с nginx, он.htaccessне читает; файл лежит «на всякий случай» — для тех, кто поставит проект на Apache.favicon.ico— 0 байт: заглушка, чтобы браузер не получал 404 на иконку вкладки.robots.txt— 24 байта, указание для поисковых роботов.
Здесь же, в public/, появится каталог build/ — если собрать ассеты Vite. И сюда же
в части 3 мы положим свой css/app.css, чтобы подключить его обычным <link>
без сборщика.
Почему корень сайта — public/, а не папка проекта
Правило, из которого вырастает вся настройка nginx в этой части:
Веб-сервер видит только
public/. Всё остальное —.env,vendor/,storage/,config/,app/,database/— лежит выше корня сайта и по HTTP не отдаётся вообще.
Это не косметика, а защита. В .env живут ключ приложения APP_KEY и доступ к базе:
кто получит этот файл, тот сможет расшифровать куки и прочитать вашу базу. Пока корень
сайта — public/, файл ~/recoursia/project-laravel/.env физически не попадает в область,
из которой nginx раздаёт файлы.
.env, /vendor, /node_modules и /public/build в игноре — и это логично:
.env— секреты конкретной машины: на вашем ноутбуке и на сервере они разные;/vendor— пакеты Composer, которые восстанавливаются одной командойcomposer installизcomposer.lock(мы ставили их в части 1);/public/build— результат сборки ассетов: его пересобирают, а не хранят.
Корень сайта в public/ — первый рубеж. В vhost мы добавим ещё и
location ~ /\. { deny all; } — запрет любых скрытых файлов. Зачем, если root
уже их спрятал? Затем, что root иногда указывают неверно: именно такую ошибку
мы сделаем намеренно через несколько слайдов. В конце части проверим, что запрос
/.env возвращает 403.
public/index.php: с чего начинается любой запрос
Строка 6. LARAVEL_START — момент старта в микросекундах. Позже по нему фреймворк
считает, сколько длилась обработка запроса.
Строки 8–11. Режим обслуживания: если существует
storage/framework/maintenance.php, выполняется он, а дальнейший код не запускается.
Файл появляется по команде php artisan down (она есть в списке команд artisan) —
и сайт начинает отвечать заглушкой «мы на обслуживании», пока вы катите обновление.
Строка 14. Подключается автозагрузчик Composer. Именно поэтому в проекте нет
сотен require: класс Illuminate\... находится по правилам PSR-4 сам.
Мы это видели в части 1 на примере nesbot/carbon.
Строка 18. bootstrap/app.php возвращает собранный объект приложения —
$app. Файл подключается через require once: приложение создаётся один раз
на запрос.
Строка 20. Всё. Request::capture() собирает запрос из того, что прислал
nginx (метод, путь, заголовки, параметры, тело), handleRequest() прогоняет его
через маршруты и middleware и отправляет ответ в буфер вывода.
Схема ровно та, что в lesson.http.i.01 и lesson.network.021: PHP-FPM запускает
процесс, процесс выполняет index.php, вывод копится в буфере, буфер отдаётся
nginx и уезжает браузеру. Laravel не меняет этот путь — он заполняет середину,
которую вы раньше писали руками.
bootstrap/app.php: как собирается приложение
withRouting — где искать маршруты. web: указывает на routes/web.php
(HTTP-запросы браузера), commands: — на routes/console.php (консольные команды).
health: '/up' добавляет служебный маршрут-проверку «приложение живо» — им удобно
мониторить деплой. Регистрирует его сам каркас, поэтому в нашем route:list
--except-vendor (слайд 30) его не будет.
withMiddleware — пустая заглушка //. Middleware — это «фильтры», через которые
проходит запрос до маршрута и ответ после него. Заполним её, когда понадобится
своё поведение между запросом и маршрутом.
withExceptions — как отдавать ошибки. Здесь настроено: если путь начинается
с api/ или клиент ждёт JSON, ошибку отдавать JSON-ом, а не HTML-страницей.
Для нашего сайта с HTML-страницами поведение остаётся обычным.
bootstrap/providers.php — список провайдеров приложения: один элемент,
AppServiceProvider::class — тот самый файл из app/Providers/, который мы видели
в дереве каталогов.
routes/web.php: что такое маршрут
total 16drwxrwxrwx 2 student student 4096 Sep 30 13:56 .drwxrwxrwx 12 student student 4096 Oct 17 10:19 ..-rw-rw-rw- 1 student student 210 Sep 30 13:56 console.php-rw-rw-rw- 1 student student 108 Sep 30 13:56 web.php
Разбор команды
Проверяем, где объявлены маршруты сайта и консоли.
ls -la routes| Токен | Зачем он и что меняет |
|---|---|
ls | список содержимого каталога |
-la | два флага слитно: -l — длинный формат, здесь интересны размеры файлов; -a — скрытые файлы (привычный флаг, хотя в routes/ их нет) |
routes | каталог маршрутов приложения |
Что произойдёт
Два файла: console.php (210 байт) и web.php (108 байт).
На что смотреть
Нас интересует web.php — маршруты HTTP. В нём строки 5 и 9, на которых объявлены оба наших маршрута; route:list в конце части это подтвердит.
Частые ошибки
- Строки
.и..в выводе-a— сам каталог и его родитель: они есть всегда и к содержимому отношения не имеют.
В routes/ два файла: web.php — маршруты сайта, console.php — маршруты консоли
(108 байт у web.php, 210 — у console.php). Нас интересует первый.
Маршрут — это правило «такой-то метод + такой-то путь = такое-то действие».
Ниже — весь путь запроса GET / в нашем проекте, от браузера до ответа; номера
строк — из route:list выше.
В lesson.http.i.02 вы разбирали путь запроса и query string руками: $_GET['page'],
$_GET['sort'], if/else на «какая страница нужна». В lesson.http.i.04–i.05
так выросла пагинация и сортировка списка рейсов. Роутер Laravel делает ту же работу:
сравнивает метод и путь с таблицей маршрутов и вызывает нужное действие — только
таблица объявлена заранее и лежит в одном файле, а не размазана по if-ам.
Параметры в фигурных скобках ({name}) — то, что в «чистом PHP» приезжало
в $_GET: часть адреса, которую маршрут ловит и передаёт в замыкание аргументом.
Проверим это через несколько слайдов.
config/: настройки и чем они отличаются от .env
app.phpauth.phpcache.phpdatabase.phpfilesystems.phplogging.phpmail.phpqueue.phpservices.phpsession.php
Разбор команды
Смотрим, из каких файлов состоит конфигурация приложения.
ls config| Токен | Зачем он и что меняет |
|---|---|
ls | список содержимого каталога |
config | аргумент-каталог. Флагов нет намеренно: нужны только имена файлов, а права и размеры ничего не объясняют — без -l вывод короче и читается одним списком |
Что произойдёт
Десять файлов — по одному на подсистему: app.php, auth.php, cache.php, database.php, filesystems.php, logging.php, mail.php, queue.php, services.php, session.php.
На что смотреть
Каждый файл — обычный PHP-массив, а значения в нём читаются через env('КЛЮЧ', 'значение по умолчанию'), то есть берутся из .env этой машины. config/ общий и лежит в git, .env — свой у каждой машины и в git не попадает.
Частые ошибки
- Если конфигурацию закэшировали (
php artisan optimize— видим его в списке команд), правка файловconfig/не применится, пока кэш не сбросили; в части 6 разбираем этот режим.
Десять файлов — по одному на подсистему: приложение (app.php), аутентификация
(auth.php), кэш, база данных, файловые хранилища, логирование, почта, очереди,
внешние сервисы, сессии.
Каждый файл — просто PHP-массив. Laravel читает их при старте, а значение из кода
достают как config('app.name').
Файл на 126 строк — блок прокручивается. Подсвечены строки 16, 29, 42, 68, 98 и 100; между ними длинные комментарии-баннеры, их можно не читать.
Конфиг — это не .env. Смотрите на подсвеченные строки: почти каждое значение
читается через env('КЛЮЧ', 'значение по умолчанию'). Первый аргумент — ключ из
.env этой машины, второй — запасной вариант, если ключа не дали.
Зачем два слоя? .env у каждого свой и в git не попадает, а config/ — общий
и в git попадает. Поэтому код не знает ни паролей, ни домена: он знает имена
настроек, а значения подставляются при старте.
Значения по умолчанию подобраны так, что приложение остаётся в безопасном режиме,
даже если в .env что-то забыли:
'env' => env('APP_ENV', 'production')— если не сказано иное, считаем сервер боевым;'debug' => (bool) env('APP_DEBUG', false)— показ деталей ошибок выключен по умолчанию. ВключённыйAPP_DEBUGна бою — это стектрейсы и переменные окружения прямо в ответе;
'timezone' => 'UTC'(строка 68) и'cipher' => 'AES-256-CBC'(строка 98) — значения безenv(): они одинаковы на всех машинах и в.envне вынесены;'key' => env('APP_KEY')(строка 100) — и без значения по умолчанию. Это тот ключ, который сгенерировала установка в части 1 («Application key set successfully»); им шифруются, например, куки — мы их увидим в заголовках ответа.
Кто пишет в storage/ и что будет без прав
drwxrwxrwx 2 student student 4096 Oct 17 10:19 bootstrap/cachedrwxrwxrwx 5 student student 4096 Sep 30 13:56 storagedrwxrwxrwx 6 student student 4096 Sep 30 13:56 storage/frameworkdrwxrwxrwx 2 student student 4096 Sep 30 13:56 storage/logsuid=1002(student) gid=1002(student) groups=1002(student)
Разбор команды
Проверяем, сможет ли процесс, обслуживающий запрос, писать туда, куда приложение пишет само.
ls -ld storage storage/framework storage/logs bootstrap/cache; id| Токен | Зачем он и что меняет |
|---|---|
ls | список содержимого — здесь он заменяется сведениями о самих объектах |
-ld | два флага слитно: -l — длинный формат, нужны права, владелец и группа; -d — не заходить внутрь каталогов. Без -d ls напечатал бы содержимое каждого из четырёх каталогов, а важны права самого каталога |
storage storage/framework storage/logs bootstrap/cache | четыре аргумента подряд: ls принимает список путей и печатает по строке на каждый — одна команда вместо четырёх |
; | разделитель команд shell: вторая команда выполняется безусловно, независимо от кода возврата первой |
id | печатает uid, gid и группы текущего пользователя — чтобы сравнить их с владельцем каталогов |
Что произойдёт
Все четыре каталога — drwxrwxrwx, владелец и группа student. id подтверждает: uid=1002(student) gid=1002(student) groups=1002(student).
На что смотреть
d в начале строки прав — каталог, дальше три тройки rwx для владельца, группы и остальных, то есть 777. Владелец каталогов и пользователь веб-стека совпадают (настройка из lesson.linux.06), поэтому запись проходит.
Частые ошибки
- На боевом сервере PHP-FPM обычно работает от
www-data, а файлы принадлежат другому пользователю: приложение не сможет ни скомпилировать шаблон, ни записать лог — и вы получите 500. В части 6 это снято по-настоящему:local.ERROR: tempnam(): file created in the system's temporary directory. 777— не требование Laravel, а особенность стенда: дажеindex.phpвpublic/лежит как-rw-rw-rw-. Правильная настройка — владелец = пользователь веб-стека и минимум нужных прав.- Проверять нужно не только
storage/:bootstrap/cache/приложение тоже пишет, и его забывают чаще всего — тогда не сохраняется кэш конфигурации и сервисов.
Что мы видим. Четыре каталога, которые приложению нужны на запись:
storage, storage/framework, storage/logs и bootstrap/cache. Права у всех —
drwxrwxrwx: d — каталог, дальше три тройки rwx для владельца, группы
и остальных, то есть 777 из lesson.programs.02. Владелец и группа — student,
и id подтверждает: uid=1002(student) gid=1002(student).
Почему это важно. Пишет в эти каталоги не вы из терминала, а процесс PHP-FPM,
который обслуживает запрос. В lesson.linux.06 вы настраивали nginx и PHP-FPM
на работу от своего пользователя — поэтому здесь владелец каталогов и пользователь
веб-стека совпадают, и запись проходит.
На боевом сервере PHP-FPM обычно работает от www-data, а файлы принадлежат
другому пользователю. Тогда приложение не сможет ни скомпилировать шаблон,
ни записать лог — и вы получите ошибку 500. Разбор такого случая — в части 6.
777 — не требование Laravel, а особенность этой машины: на стенде все файлы
созданы с широкими правами. Взгляните на вывод ls -la public выше — даже
index.php лежит как -rw-rw-rw- (666). Правильная настройка — это
«владелец = пользователь веб-стека, права = минимум нужного», а не 777 всем.
Миграции из коробки: три таблицы одним файлом
total 20drwxrwxrwx 2 student student 4096 Sep 30 13:56 .drwxrwxrwx 5 student student 4096 Oct 17 10:19 ..-rw-rw-rw- 1 student student 1473 Sep 30 13:56 0001_01_01_000000_create_users_table.php-rw-rw-rw- 1 student student 873 Sep 30 13:56 0001_01_01_000001_create_cache_table.php-rw-rw-rw- 1 student student 1883 Sep 30 13:56 0001_01_01_000002_create_jobs_table.php
Разбор команды
Смотрим миграции, которые приехали с каркасом, и порядок, в котором они применятся.
ls -la database/migrations| Токен | Зачем он и что меняет |
|---|---|
ls | список содержимого каталога |
-la | два флага слитно: -l — длинный формат (имена миграций длинные, а размеры показывают, что users — самая маленькая из трёх: 1473 байта против 1883 у jobs); -a — скрытые файлы, которых в этом каталоге нет |
database/migrations | путь через слэш — аргументом ls может быть любой путь, не только каталог первого уровня |
Что произойдёт
Три файла: 0001_01_01_000000_create_users_table.php (1473 байта), 0001_01_01_000001_create_cache_table.php (873), 0001_01_01_000002_create_jobs_table.php (1883).
На что смотреть
Имя миграции начинается с даты — это не украшение, а порядок применения: миграции выполняются по возрастанию имени. Дата 0001_01_01 у каркасных файлов гарантирует, что они отработают раньше ваших.
Частые ошибки
- Переименовывать миграцию нельзя: имя задаёт порядок, и оно же попадает в список выполненных (
php artisan migrate:statusпечатает «Migration name ... Batch / Status»). - Один файл может создавать несколько таблиц:
000000_create_users_table.phpсоздаётusers,password_reset_tokensиsessions.
Три файла, и имена начинаются с даты: 0001_01_01_000000_.... Это не украшение,
а порядок применения: миграции выполняются по возрастанию имени. Дата
0001_01_01 у каркасных файлов гарантирует, что они отработают раньше ваших.
Рядом лежат ..._000001_create_cache_table.php (таблица кэша) и
..._000002_create_jobs_table.php (очереди) — их мы не трогаем.
Подсвечены строки 14, 24, 30 — три вызова Schema::create(). Метод up() —
строки 12–38, обратный ему down() — 43–48.
Один файл создаёт три таблицы, и все три нужны самому каркасу:
users— пользователи.$table->id()— этоauto_increment-первичный ключ изlesson.db.03;string('email')->unique()— поле с уникальным индексом (индексы и их цену вы разбирали вlesson.db.vi.02–vi.03);rememberToken()— столбец для галочки «запомнить меня»;timestamps()—created_atиupdated_at, которые Laravel заполняет сам.password_reset_tokens— токены восстановления пароля:emailкак первичный ключ, сам токен и время создания.sessions— сессии в базе: строковыйid,foreignId('user_id')— внешний ключ на пользователя (связь «один ко многим» изlesson.db.v.01),ip_addressдлиной 45,user_agent,payloadи индекс поlast_activity— чтобы вычищать протухшие сессии.
down() делает обратное: Schema::dropIfExists('users') и так же для двух других
таблиц. Пара «up() / down()» и есть смысл миграции: схему можно накатить
и откатить, а не править руками в PHPMyAdmin. По-настоящему запустим их в части 4 —
тогда же переключимся с database.sqlite на MariaDB.
Модель User, которая приехала с каркасом
namespace App\Models; — пространство имён совпадает с путём: App\ → app/.
Это правило PSR-4 из composer.json, благодаря которому автозагрузчик находит класс
без единого require (часть 1).
class User extends Authenticatable — обычное наследование из блока ООП:
модель получает всё поведение «пользователя, которого можно аутентифицировать».
use HasFactory, Notifiable; — это трейты: способ подключить к классу
готовое поведение, не выстраивая наследование. В курсе вы реализовывали интерфейсы
(lesson.oop.polymorphism.01) — здесь идея похожая, но трейт приносит не только
обещание, а готовый код: HasFactory связывает модель с database/factories/UserFactory.php,
Notifiable — с отправкой уведомлений.
Атрибуты #[Fillable] и #[Hidden] — метаданные класса в квадратных скобках
с #. Fillable перечисляет поля, которые можно заполнять сразу массивом
(name, email, password); всё, чего в списке нет, массово не заполняется —
защита от ситуации, когда посетитель добавляет в форму поле is_admin.
Hidden прячет password и remember_token: при превращении модели в массив
или JSON их не будет.
Метод casts() описывает, как превращать значения из базы:
'email_verified_at' => 'datetime'— из строки в объекте даты;'password' => 'hashed'— при записи значение автоматически хешируется, поэтому в коде не нужно помнить «а не забыл ли я хешировать пароль».
К этой модели мы вернёмся в частях 4 и 5: регистрация, вход и форма пользователя работают ровно на этих настройках.
artisan: консольный вход в то же приложение
Сравните с public/index.php — это почти он же, только для терминала:
те же LARAVEL_START, тот же vendor/autoload.php, тот же bootstrap/app.php.
Разница в последних строках: вместо handleRequest(Request::capture()) —
handleCommand(new ArgvInput) и exit($status).
То есть запрос приходит не из браузера, а из аргументов командной строки
(ArgvInput), а код возврата уходит в shell. Отсюда важное следствие: команда
artisan видит то же приложение, те же конфиги и ту же базу, что и сайт.
Laravel Framework 13.37.0Usage: command [options] [arguments]Options: -h, --help Display help for the given command. When no command is given display help for the list command --silent Do not output any message -q, --quiet Only errors are displayed. All other output is suppressed -V, --version Display this application version --ansi|--no-ansi Force (or disable --no-ansi) ANSI output -n, --no-interaction Do not ask any interactive question --env[=ENV] The environment the command should run under -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debugAvailable commands: about Display basic information about your application clear-compiled Remove the compiled class file completion Dump the shell completion script db Start a new database CLI session dev Run the dev processes docs Access the Laravel documentation down Put the application into maintenance / demo mode env Display the current framework environment help Display help for a command inspire Display an inspiring quote list List commands migrate Run the database migrations optimize Cache framework bootstrap, configuration, and metadata to increase performance pail Tails the application logs reload Reload running services serve Serve the application on the PHP development server test Run the application tests tinker Interact with your application up Bring the application out of maintenance mode auth auth:clear-resets Flush expired password reset tokens cache cache:clear Flush the application cache cache:forget Remove an item from the cache cache:prune-stale-tags Prune stale cache tags from the cache (Redis only) channel
Разбор команды
Смотрим, какие команды даёт консольный вход в приложение: дальше нам понадобятся serve, tinker и route:list.
php artisan list | head -42| Токен | Зачем он и что меняет |
|---|---|
php | исполняемый файл интерпретатора; на стенде PHP 8.3 — подставьте свою версию (php -v) |
artisan | PHP-скрипт в корне проекта — консольный вход в то же приложение: тот же vendor/autoload.php и тот же bootstrap/app.php, что и в public/index.php, только запрос приходит из аргументов командной строки |
list | аргумент скрипта artisan: команда, печатающая список всех доступных команд. Для shell это обычный аргумент, а не отдельная программа |
| | пайп: stdout artisan list становится stdin следующей команды |
head | оставляет только начало вывода |
-42 | сколько строк оставить — 42. Полный список длиннее и не влезает в кадр; число подобрано так, чтобы вошли общие параметры и первые группы команд |
Что произойдёт
Первая строка — версия каркаса Laravel Framework 13.37.0, затем общие параметры (--help, --silent, --quiet, --version, --ansi, --no-interaction, --env, --verbose) и команды: about, serve, migrate, tinker, test, optimize, pail, down/up и другие. Вывод обрезан на 42-й строке — на группе channel.
На что смотреть
Команды сгруппированы по префиксам: в кадре видны auth:clear-resets, cache:clear, cache:forget, cache:prune-stale-tags. Группы make: здесь нет — она дальше по алфавиту, и именно с неё начнётся часть 3.
Частые ошибки
- Не нашли
make:controllerв этом выводе — он есть, просто обрезан:php artisan list | grep make:. php artisan ...вне корня проекта даётCould not open input file: artisan— скрипт ищется в текущем каталоге.headзакрывает пайп, не дочитав вывод; на результат это не влияет, но полагаться на код возврата такой связки в скриптах не стоит.
Первая строка вывода — версия каркаса: Laravel Framework 13.37.0. Дальше общие
параметры (--help, --verbose, --env) и первые команды. Мы вывод обрезали
(| head -42), полный список длиннее.
Что из показанного пригодится сразу:
about— сводка по приложению (мы запускали её в части 1);serve— встроенный сервер разработки, следующий слайд;migrate— применить миграции (часть 4);tinker— интерактивная консоль внутри приложения (ей мы воспользуемся через пару слайдов);down/up— режим обслуживания: тот самыйstorage/framework/maintenance.phpиз точки входа;
test— запуск тестов (часть 6);optimize— закэшировать конфиг и метаданные (часть 6);pail— смотреть лог приложения в реальном времени.
Команды сгруппированы по префиксам: auth:, cache: и дальше по алфавиту —
в обрезанном выводе видны auth:clear-resets, cache:clear, cache:forget,
cache:prune-stale-tags.
Есть и группа make: — команды, которые создают файлы за вас: контроллер, модель,
миграцию, тест. В этом выводе их нет (мы обрезали список), но именно с них начнётся
часть 3: вместо того чтобы создавать файлы руками и угадывать имена, мы скажем
artisan, что нужно.
Vite и ассеты: что это и почему мы его пока не включаем
9: @fonts12: @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))13: @vite(['resources/css/app.css', 'resources/js/app.js'])
Разбор команды
Проверяем, как стартовая страница обходится без сборки ассетов: ищем в шаблоне директивы Vite и условие на наличие манифеста.
grep -n "@vite\|@fonts\|file_exists(public_path" resources/views/welcome.blade.php| Токен | Зачем он и что меняет |
|---|---|
grep | поиск строк, подходящих под шаблон, и печать их на экран |
-n | печатать номер строки: без него не сказать, о каком месте шаблона идёт речь — в разборе мы ссылаемся на строки 9, 12 и 13 |
"@vite\|@fonts\|file_exists(public_path" | один шаблон поиска в двойных кавычках: ищем сразу три фрагмента — директиву @vite, директиву @fonts и начало условия с file_exists(public_path. Кавычки обязательны: внутри есть |, ( и \, которые shell истолковал бы сам |
\| | «ИЛИ» в базовом регулярном синтаксисе grep. Обратная косая нужна именно в базовом синтаксисе: там одиночный | — обычный символ |
resources/views/welcome.blade.php | файл, в котором ищем: шаблон стартовой страницы |
Что произойдёт
Три строки: 9 — @fonts, 12 — @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))), 13 — @vite(['resources/css/app.css', 'resources/js/app.js']).
На что смотреть
Строка 12 проверяет, есть ли вообще сборка ассетов: манифест public/build/manifest.json либо файл public/hot запущенного dev-сервера Vite. В выводе ls -la public нет ни того, ни другого — условие ложно, @vite не выполняется, и страница обходится стилями, зашитыми в сам шаблон.
Частые ошибки
- С флагом
-E(расширенный синтаксис) «ИЛИ» пишется как|, а\|превращается в поиск литерала — шаблон перестанет находить нужные строки. - Без кавычек shell увидит
|как пайп и попытается запустить команду@fonts: «command not found». - grep ничего не нашёл — код возврата 1, вывод пустой. Это не ошибка поиска, а факт: в шаблоне нет ни одной из трёх конструкций.
Это строки 9, 12 и 13 шаблона resources/views/welcome.blade.php. Логика простая:
@fonts— директива подключения шрифтов;@if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))— проверить, есть ли вообще сборка ассетов: либо готовый манифест вpublic/build/, либо запущенный dev-сервер Vite (он оставляет файлpublic/hot);@vite(['resources/css/app.css', 'resources/js/app.js'])— подключить собранные css и js.
В public/ ни build/, ни hot нет — мы это видели в ls -la public. Значит,
условие ложно, @vite не выполняется, и стартовая страница обходится теми стилями,
что зашиты в сам шаблон.
package.json описывает, чем собирают ассеты: скрипт build — это vite build,
dev — просто vite (dev-сервер с горячей перезаменой). В devDependencies —
vite ^8.0.0, laravel-vite-plugin ^3.1, tailwindcss ^4.0.0 и
@tailwindcss/vite, concurrently ^10.0.3.
Чтобы всё это заработало, нужны Node.js и npm install, а затем npm run build.
В public/ нет каталога build/ — состав public/ мы смотрели выше, там четыре
файла. Значит, сборку ассетов на стенде не запускали.
Свои стили мы положим обычным файлом в public/css/app.css и подключим тегом
<link> — как вы делали в блоке HTTP. Никакого сборщика и npm-пакетов: сохранили
файл — и он уже отдаётся nginx как статика. Vite при этом никуда
не девается: когда понадобится Tailwind или свой JavaScript, включите его ровно
тем условием, которое показано слева.
Заметьте, .gitignore проекта уже готов к обоим вариантам: в нём есть
/public/build (строка 17) и /public/hot (строка 19) — результат сборки
в git не хранят.
Первый запуск: php artisan serve
INFO Server running on [http://127.0.0.1:8000]. Press Ctrl+C to stop the serverGET / -> HTTP 200, 70403 байт, 0.439242 с<title>Laravel</title> PID PPID CMD 49206 49199 php artisan serve --host=127.0.0.1 --port=8000
Разбор команды
Поднимаем встроенный сервер разработки, ждём его старта и сразу меряем первый ответ — чтобы было с чем сравнивать после переезда на nginx.
nohup php artisan serve --host=127.0.0.1 --port=8000 > /tmp/serve.log 2>&1 &| Токен | Зачем он и что меняет |
|---|---|
nohup | не завершать процесс при закрытии терминала (игнорировать SIGHUP): сервер должен пережить сессию shell, из которой его запустили |
php | интерпретатор; на стенде PHP 8.3 — подставьте свою версию (php -v) |
artisan | консольный вход в приложение |
serve | команда запуска встроенного сервера разработки PHP: nginx и PHP-FPM для него не нужны |
--host=127.0.0.1 | на каком сетевом интерфейсе слушать. Локальный адрес — с другой машины сайт не открыть; это осознанное ограничение режима разработки |
--port=8000 | порт. 8000 — значение по умолчанию для serve, указываем явно, чтобы адрес в логе был предсказуемым |
> /tmp/serve.log | перенаправить stdout в файл: иначе сообщения сервера смешаются с выводом следующих команд |
2>&1 | stderr — туда же, куда stdout: и информационный блок, и ошибки окажутся в одном файле |
& | запуск в фоновом режиме: терминал сразу возвращает приглашение и остаётся свободным |
Что произойдёт
Сервер поднимается в фоне, весь его вывод уходит в /tmp/serve.log, а терминал готов к следующим командам.
На что смотреть
Команда ничего не печатает в терминал — это не ошибка: вывод перенаправлен в файл. Проверить, что сервер жив, можно следующим cat.
Частые ошибки
- Без
&сервер займёт терминал до Ctrl+C, и остальные команды захвата не выполнятся. - Если порт 8000 занят,
serveпредложит другой — и адрес в/tmp/serve.logизменится. Всегда читайте адрес из лога, а не из памяти.
sleep 4| Токен | Зачем он и что меняет |
|---|---|
sleep | пауза — ничего не делает заданное время |
4 | длительность в секундах |
Что произойдёт
Терминал ждёт 4 секунды.
На что смотреть
Пауза нужна, чтобы сервер успел начать слушать порт до первого запроса: без неё curl может получить «Connection refused».
Частые ошибки
- Фиксированная пауза — грубый приём: 4 с взяты с запасом для стенда. В скриптах правильнее опрашивать порт в цикле.
cat /tmp/serve.log| Токен | Зачем он и что меняет |
|---|---|
cat | напечатать содержимое файла |
/tmp/serve.log | абсолютный путь к файлу, куда ушёл вывод сервера |
Что произойдёт
Печатает блок INFO Server running on [http://127.0.0.1:8000] и подсказку Press Ctrl+C to stop the server.
На что смотреть
Адрес в квадратных скобках — подтверждение, что --host и --port приняты. Отдельных access.log и error.log, как у nginx, здесь нет: весь журнал — то, что сервер печатает в этот файл.
Частые ошибки
- Пустой файл — сервер не стартовал: смотрите, не занят ли порт и из того ли каталога запущен
artisan.
curl -sS -o /tmp/welcome.html -w "GET / -> HTTP %{http_code}, %{size_download} байт, %{time_total} с\n" http://127.0.0.1:8000/| Токен | Зачем он и что меняет |
|---|---|
curl | HTTP-клиент: показывает ровно то, что ушло по сети, без рендеринга браузером |
-sS | два флага слитно: -s убирает прогресс-бар (в захвате он был бы мусором), -S возвращает видимость ошибок, которые -s подавляет вместе с ним |
-o /tmp/welcome.html | сохранить тело ответа в файл, а не печатать его: десятки килобайт HTML в терминале не нужны |
-w "GET / -> HTTP %{http_code}, %{size_download} байт, %{time_total} с\n" | формат строки, которую curl напечатает после запроса: именно она превращает проверку в измерение. Двойные кавычки обязательны — внутри пробелы, без них shell разбил бы строку на отдельные аргументы |
%{http_code} | подстановка curl: статус-код ответа |
%{size_download} | подстановка curl: сколько байт тела принято |
%{time_total} | подстановка curl: полное время запроса в секундах |
\n | перевод строки внутри формата — иначе метрики и следующее приглашение окажутся на одной строке |
http://127.0.0.1:8000/ | адрес запроса — тот, на котором слушает serve |
Что произойдёт
GET / -> HTTP 200, 70403 байт, 0.439242 с; тело стартовой страницы сохранено в /tmp/welcome.html.
На что смотреть
Три числа: статус, размер тела и время. 0.439242 с — первый запрос к холодному приложению: загрузка классов каркаса и компиляция шаблона. Запомните их: наш собственный маршрут отдаст 435 байт за 0.029839 с.
Частые ошибки
-o файли-o /dev/null— разные вещи: первое сохраняет тело, второе выбрасывает. Перепутаете — и не сможете разобрать HTML следующей командой.- Без кавычек у
-wcurl получит словоGETкак URL и ответит ошибкой, а не измерением.
grep -o "<title>[^<]*</title>" /tmp/welcome.html| Токен | Зачем он и что меняет |
|---|---|
grep | поиск по содержимому файла |
-o | печатать только найденный фрагмент, а не всю строку: HTML-страница лежит длинными строками, без -o вывод был бы нечитаемым |
"<title>[^<]*</title>" | регулярное выражение в кавычках — иначе shell принял бы < за перенаправление ввода |
<title> | литеральная открывающая граница искомого фрагмента |
[^<]* | «ноль или более любых символов, кроме <»: сам текст заголовка. [^ — отрицательный класс символов, * — повторение |
</title> | литеральная закрывающая граница |
/tmp/welcome.html | файл, в котором ищем, — туда предыдущая команда сохранила тело ответа |
Что произойдёт
Печатает одну строку: <title>Laravel</title>.
На что смотреть
По заголовку видно, что сервер отдал именно шаблон welcome.blade.php, а не страницу ошибки.
Частые ошибки
- Без
-ogrep выдаст всю строку файла целиком — десятки килобайт HTML в терминале. - Без кавычек shell сообщит о синтаксической ошибке рядом с
<: он решит, что это перенаправление.
ps -o pid,ppid,cmd -C php | head -5| Токен | Зачем он и что меняет |
|---|---|
ps | снимок списка процессов (в отличие от top — одноразовый, без обновления) |
-o pid,ppid,cmd | какие колонки печатать: PID, PID родителя и полную командную строку |
-C php | отобрать только процессы с именем php |
| | пайп в следующую команду |
head | ограничить число строк |
-5 | не больше пяти строк — на случай, если php-процессов в системе много |
Что произойдёт
Ровно один процесс: PID 49206, родитель 49199 (оболочка, которая его запустила), командная строка php artisan serve --host=127.0.0.1 --port=8000.
На что смотреть
Один процесс на все запросы — в этом главная слабость serve: ни пула воркеров PHP-FPM, ни очереди nginx. Колонка CMD подтверждает, что --host и --port дошли до процесса.
Частые ошибки
-Cсравнивает имя процесса, а не аргументы: процессыphp-fpmпод этот фильтр не попадут.- Два процесса
serveв выводе — сервер запустили повторно; второй не слушает порт, он занят первым.
Что происходит. serve поднимает встроенный сервер разработки PHP на
127.0.0.1:8000 — nginx и PHP-FPM для него не нужны. Мы запустили его через
nohup ... &, а вывод увели в /tmp/serve.log: иначе сервер занял бы терминал
и не дал бы выполнить следующие команды. Отсюда же sleep 4 — даём серверу
подняться перед первым запросом.
Что намерили. GET / → HTTP 200, 70 403 байта тела, 0.439242 с.
Заголовок страницы — <title>Laravel</title>. Последний ps показывает ровно
один процесс php (PID 49206, родитель 49199 — оболочка, которая его запустила).
Почему это только для разработки.
- Адрес
127.0.0.1: сервер слушает локальный интерфейс, с другой машины сайт не открыть. - Один процесс на все запросы: ни пула воркеров PHP-FPM, ни очереди nginx.
- Логи — только то, что сервер печатает в консоль (мы увели это в
/tmp/serve.log). Отдельныхaccess.logиerror.log, как у nginx, а значит и привычного инструмента диагностики (lesson.programs.01), здесь нет. - Нет
server_name: второй сайт рядом не поселить.
Всё это даёт nginx — поэтому вторую половину части мы потратим на vhost.
Это первый запрос к холодному приложению: нужно загрузить классы каркаса и скомпилировать шаблон. Запомните цифру — дальше, на своём крошечном маршруте, мы получим ответ в разы быстрее.
Так выглядит ответ каркаса

Это resources/views/welcome.blade.php из каркаса по адресу http://127.0.0.1:8000/ —
те самые 70 403 байта HTML, которые намерил curl. Страница-заглушка: она нужна
ровно затем, чтобы убедиться, что установка живая. Мы её заменим.
Пишем свой маршрут: строка уже является ответом
Два изменения в routes/web.php:
- Корневой маршрут больше не рендерит
welcome, а возвращает строку. Никакогоecho, никакого HTML-каркаса: что вернуло замыкание, то и стало телом ответа. Обёртку в HTTP-ответ (статус, заголовки) Laravel берёт на себя. - Добавлен маршрут
/hello/{name}: фигурные скобки в пути — это параметр. Замыкание объявлено какfunction (string $name)— типизированный аргумент, значение берётся прямо из адреса. Внутри —view('hello', ['name' => $name]): «отрендерить шаблонhello, передав в него массив с ключомname».
Hello, Laravel!GET /hello/студент -> HTTP 200, 435 байт, 0.029839 с
Разбор команды
Проверяем оба маршрута без браузера: сначала тело корневого ответа, затем метрики маршрута с параметром.
curl -sS http://127.0.0.1:8000/; echo; curl -sS -o /dev/null -w "GET /hello/студент -> HTTP %{http_code}, %{size_download} байт, %{time_total} с\n" "http://127.0.0.1:8000/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82"| Токен | Зачем он и что меняет |
|---|---|
curl | HTTP-клиент: печатает ровно то, что пришло по сети |
-sS | тихо, но с сообщениями об ошибках: -s убирает прогресс-бар, -S возвращает видимость поломок |
http://127.0.0.1:8000/ | адрес корневого маршрута. Тело короткое, поэтому печатаем его прямо в терминал, без -o |
; | разделитель команд: вторая выполняется независимо от результата первой |
echo | без аргументов печатает пустую строку. Тело Hello, Laravel! не заканчивается переводом строки, и без echo метрики второго запроса прилипли бы к нему |
curl | второй запрос — к маршруту с параметром |
-sS | тот же тихий режим с ошибками |
-o /dev/null | тело ответа выбрасываем: нужны только цифры, а HTML мы снимем отдельной командой |
-w "GET /hello/студент -> HTTP %{http_code}, %{size_download} байт, %{time_total} с\n" | формат итоговой строки: своя подпись и три подстановки curl |
%{http_code} | статус-код ответа |
%{size_download} | число принятых байт тела |
%{time_total} | полное время запроса в секундах |
\n | перевод строки в конце формата |
"http://127.0.0.1:8000/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82" | URL в двойных кавычках. Кириллица закодирована в percent-encoding: «студент» — семь букв, каждая по два байта UTF-8, отсюда ровно семь пар %XX |
Что произойдёт
Первая строка вывода — тело корневого маршрута Hello, Laravel!, вторая — GET /hello/студент -> HTTP 200, 435 байт, 0.029839 с.
На что смотреть
Сравните два измерения: 70 403 байта и 0.439242 с у стартовой страницы против 435 байт и 0.029839 с у нашего маршрута. Разница — это в первую очередь размер ответа и стоимость большого шаблона, а не «магия фреймворка».
Частые ошибки
- Ответ 404 — маршрут не совпал: сверьтесь с
php artisan route:list.Connection refused— сервер не запущен или слушает другой порт. - Забыли
-o /dev/null— тело смешается со строкой метрик, и цифры придётся искать глазами. - В подпись
-wможно писать кириллицу, а в URL её нужно кодировать: браузер и curl делают это сами, и в лог nginx путь тоже попадёт в percent-encoding.
Проверяем без браузера — curl-ом, он показывает ровно то, что ушло по сети.
Первая команда печатает тело ответа на /: Hello, Laravel!. Вторая запрашивает
/hello/студент и меряет только метрики: HTTP 200, 435 байт, 0.029839 с.
%D1%81%D1%82...В URL разрешены не все символы: кириллицу браузер и curl кодируют в
percent-encoding — каждый байт UTF-8 как %XX (кодировки вы разбирали в
lesson.strings.01, структуру URL — в lesson.http.i.02). В слове «студент» семь
букв, каждая в UTF-8 занимает два байта, — отсюда ровно семь пар %XX. Laravel
отдаёт параметр уже раскодированным, поэтому в замыкание попадает нормальная строка.
Сравните два измерения: 70 403 байта и 0.439242 с у стартовой страницы против 435 байт и 0.029839 с у нашей. Разница — это в первую очередь размер ответа и стоимость большого шаблона, а не «магия фреймворка».
Шаблон Blade: {{ }} подставляет значение
view('hello') ищет файл resources/views/hello.blade.php: имя шаблона — это путь
относительно resources/views/ без суффикса .blade.php.
В шаблоне три подстановки:
- строка 6 и строка 9 —
{{ $name }}: переменная, которую мы передали массивом из маршрута; - строка 11 —
{{ now()->locale('ru')->isoFormat('D MMMM YYYY, HH:mm') }}:now()возвращает объект даты и времени (тот самый Carbon, который мы ставили через Composer в части 1),locale('ru')переключает язык,isoFormat()задаёт формат.
<!DOCTYPE html><html lang="ru"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Привет, студент</title></head><body> <h1>Привет, студент!</h1> <p>Это шаблон Blade: значение приехало из адреса <code>/hello/{name}</code>.</p> <p>Сейчас: 17 октября 2026, 10:20</p></body></html>
Разбор команды
Смотрим сам HTML, который собрал Blade, — чтобы сравнить его с шаблоном строка в строку.
curl -sS "http://127.0.0.1:8000/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82"| Токен | Зачем он и что меняет |
|---|---|
curl | HTTP-клиент; без -o тело ответа печатается в терминал — здесь оно короткое, и это ровно то, что нужно |
-sS | тихо, но с ошибками |
"http://127.0.0.1:8000/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82" | адрес маршрута с параметром в кавычках; %D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82 — это «студент» в percent-encoding |
Что произойдёт
13 строк HTML: <title>Привет, студент</title>, <h1>Привет, студент!</h1> и строка «Сейчас: 17 октября 2026, 10:20».
На что смотреть
Строк ровно столько же, сколько в шаблоне hello.blade.php: Blade не добавляет своей разметки, он только подставляет значения вместо {{ }}. Параметр {name} пришёл в замыкание уже раскодированным — в HTML стоит «студент», а не %D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82.
Частые ошибки
- Вместо «студент» кракозябры — смотрите кодировку: в шаблоне есть
<meta charset="utf-8">, а nginx отдаёт заголовокContent-Type: text/html; charset=utf-8. - Время в последней строке — момент рендера, а не момент запуска curl: шаблон собирается заново на каждый запрос.
Ответ — 13 строк, ровно как в шаблоне: Blade не добавляет своей разметки,
он только подставляет значения вместо {{ }}. Сравните:
| В шаблоне | В ответе |
|---|---|
<title>Привет, {{ $name }}</title> |
<title>Привет, студент</title> |
<h1>Привет, {{ $name }}!</h1> |
<h1>Привет, студент!</h1> |
{{ now()->locale('ru')->isoFormat('D MMMM YYYY, HH:mm') }} |
17 октября 2026, 10:20 |
Всё остальное — байт в байт тот же HTML, что мы написали.
Раньше вы собирали страницу конкатенацией строк и echo внутри циклов
(lesson.http.i.04: таблица рейсов, short tag). Blade — тот же вывод, только
разметка и данные разделены: шаблон читается как HTML, а подстановки видны
с первого взгляда.
Экранирование: почему {{ }} безопасен
<b>жирный</b>Разбор команды
Проверяем, что именно делает {{ }} под капотом: вызываем функцию экранирования e() внутри приложения.
php artisan tinker --execute="echo e('<b>жирный</b>'), PHP_EOL;"| Токен | Зачем он и что меняет |
|---|---|
php | интерпретатор; на стенде PHP 8.3 — подставьте свою версию (php -v) |
artisan | консольный вход в приложение |
tinker | интерактивная консоль Laravel: код выполняется внутри уже загруженного приложения, со всеми его классами и хелперами |
--execute= | опция с значением через = в той же строке: выполнить код и выйти, не открывая интерактивный сеанс — так проверку можно снять одним захватом |
"echo e('<b>жирный</b>'), PHP_EOL;" | значение опции в двойных кавычках: вся строка кода уходит одним аргументом, несмотря на пробелы и скобки. Внутренние кавычки одинарные, поэтому экранировать ничего не нужно |
echo | вывод результата в stdout |
e('<b>жирный</b>') | та самая функция экранирования, которую вызывает Blade; в одинарных кавычках < и > остаются собой — это строковый литерал PHP |
, PHP_EOL | второй аргумент echo через запятую — константа перевода строки: без неё вывод слипся бы с приглашением |
; | конец PHP-инструкции внутри строки кода |
Что произойдёт
Одна строка вывода: <b>жирный</b> — угловые скобки заменены HTML-сущностями.
На что смотреть
Это и есть работа {{ }}: Blade не вставляет значение в HTML «как есть», а сначала прогоняет его через e(). Передайте в /hello/{name} строку <script>alert(1)</script> — и в ответе окажутся <script>alert(1)</script>, то есть текст, а не выполнимый код.
Частые ошибки
- Двойные кавычки внутри значения
--execute="..."пришлось бы экранировать как\", иначе shell закрыл бы строку раньше времени; здесь внутри только одинарные — конфликта нет. - Для готовой HTML-разметки у Blade есть вывод без экранирования — но данным из адреса, формы или базы он противопоказан: вы своими руками отключаете защиту от XSS.
- Без
--executeоткроется интерактивный сеанс, который будет ждать ввода и не вернёт терминал.
Мы попросили приложение выполнить одну строку в tinker — интерактивной консоли
из списка команд artisan. Функция e() получила строку <b>жирный</b> и вернула
<b>жирный</b>: угловые скобки заменены на HTML-сущности.
Именно это делает {{ }} под капотом. Blade не вставляет значение в HTML «как
есть» — он сначала прогоняет его через e(). Отсюда следствие, которое стоит
запомнить: если в адресе /hello/{name} вместо «студент» передать
<script>alert(1)</script>, в ответе окажутся <script>..., и браузер
покажет это как текст, а не выполнит как код.
Параметр {name} приходит из адреса, то есть целиком под контролем посетителя.
Без e() чужая строка стала бы частью вашего HTML — а значит, чужой скрипт
выполнился бы в браузере другого пользователя, с его куки и его сессией. Это
атака XSS, и защита от неё — экранирование вывода.
Принцип вам уже знаком: в lesson.sql-injection.i.01–i.03 вы видели, как
параметр sort из адресной строки менял структуру SQL-запроса, и как от этого
защищаются — подготовленный запрос с маркерами (lesson.db.04) плюс белый список
допустимых значений через in_array. Здесь та же логика, только с другой стороны:
данные не должны становиться разметкой. Оба случая — одно правило:
не доверять тому, что пришло от клиента.
Если нужно вывести готовую HTML-разметку (например, текст статьи), у Blade есть отдельный синтаксис вывода без экранирования. Пользоваться им для данных из адреса, из формы или из базы нельзя: вы своими руками отключите защиту, которую только что разобрали.
Blade не интерпретируется: шаблон компилируется в PHP
total 88drwxrwxrwx 2 student student 4096 Oct 17 10:20 .drwxrwxrwx 6 student student 4096 Sep 30 13:56 ..-rw-rw-rw- 1 student student 14 Sep 30 13:56 .gitignore-rwxrwxrwx 1 student student 72662 Oct 17 10:20 2ee673e2468344c22c5e4a66e306b0a5.php-rwxrwxrwx 1 student student 593 Oct 17 10:20 6933e8cf03b2fe60f09b266c43cdf1ac.php
Разбор команды
Смотрим, куда Blade складывает скомпилированные шаблоны, — доказательство, что шаблон превращается в обычный PHP-файл.
ls -la storage/framework/views | head -8| Токен | Зачем он и что меняет |
|---|---|
ls | список содержимого каталога |
-la | два флага слитно: -l — длинный формат, здесь важны и размер, и минута изменения, и права; -a — скрытые файлы, без него не было бы видно .gitignore |
storage/framework/views | каталог кэша видов |
| | пайп: вывод ls передаётся следующей команде |
head | оставить только начало вывода |
-8 | ограничение в 8 строк — с запасом: здесь вывод короче (total, ., .., .gitignore и два файла) |
Что произойдёт
Два файла с именами-хэшами: 72 662 байта и 593 байта, оба с временем 10:20, права -rwxrwxrwx, владелец student; рядом .gitignore на 14 байт.
На что смотреть
Меньший файл — наш hello.blade.php: страница из ответа curl заканчивалась строкой «Сейчас: 17 октября 2026, 10:20», и оба файла датированы той же минутой — шаблоны собрались при наших запросах. Больший на два порядка — собранный welcome.blade.php: в стартовой странице стили зашиты в сам шаблон. Права и владелец показывают, что файлы записал процесс, обслуживший запрос.
Частые ошибки
- «Я поправил шаблон, а страница не изменилась» — смотрят именно сюда, в кэш видов; как его сбрасывать, разбираем в части 6.
- Нет права записи в
storage/— скомпилировать шаблон некуда: вместо страницы будет 500, а в логе приложенияtempnam(): file created in the system's temporary directory(снимаем это в части 6). - Имя файла — хэш, а не имя шаблона: найти «свой» файл по названию не получится, ориентируйтесь на размер и время.
Что лежит в storage/framework/views/. Два файла с именами-хэшами:
| Файл | Размер | Время | Что это |
|---|---|---|---|
2ee673e246...php |
72 662 байта | 10:20 | собранный welcome.blade.php |
6933e8cf03...php |
593 байта | 10:20 | собранный hello.blade.php |
Оба файла записаны в 10:20 — в ту минуту, когда прогон запрашивал страницы:
время «17 октября 2026, 10:20» видно в самом ответе на слайде про шаблон. Какой файл
какому шаблону соответствует, видно по размеру: стартовая страница — это 70 403 байта
HTML со стилями, зашитыми в шаблон, значит 72 662 байта — собранный
welcome.blade.php; наш hello.blade.php — 13 строк и 457 байт исходника,
откуда и 593 байта компиляции.
Рядом .gitignore (14 байт): содержимое каталога — кэш, в git ему не место.
Зачем это нужно. Blade-метки ({{ }}, @if, @foreach) — не PHP. Laravel
заранее переводит шаблон в обычный PHP-файл и дальше выполняет уже его. Разбор
Blade-синтаксиса делается один раз, а не на каждом запросе.
Права у файлов -rwxrwxrwx, владелец student — их записал тот самый процесс,
который обслуживал запрос. Это прямая иллюстрация слайда про права: не будет
записи в storage/ — не будет и скомпилированных шаблонов, а вместо страницы
вы получите ошибку.
Отсюда же растёт типовая проблема «я поправил шаблон, а страница не изменилась»: в таком случае смотрят на кэш видов. Как его сбрасывать — разберём в части 6.
Тот же ответ в браузере

Адрес http://127.0.0.1:8000/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82 —
то, что curl отдал текстом, браузер показал страницей. Пока всё это работает
через встроенный сервер разработки. Дальше — перенос на nginx.
Уходим со встроенного сервера на nginx
total 0lrwxrwxrwx 1 root root 34 Oct 17 06:04 default -> /etc/nginx/sites-available/default
Разбор команды
Смотрим, что включено в nginx до нас, — чтобы понимать, куда добавлять свой vhost и что при этом уже работает.
ls -l /etc/nginx/sites-enabled/| Токен | Зачем он и что меняет |
|---|---|
ls | список содержимого каталога |
-l | длинный формат: только он показывает, что запись — символическая ссылка, и куда она ведёт |
/etc/nginx/sites-enabled/ | каталог включённых сайтов: nginx читает именно его, а не sites-available/ |
Что произойдёт
Одна запись: default -> /etc/nginx/sites-available/default — символическая ссылка на дефолтный сайт из пакета.
На что смотреть
Первый символ прав l (lrwxrwxrwx) означает ссылку, стрелка показывает цель. Дефолтный сайт отвечает на запросы, для которых не нашлось своего server_name. Дальше работаем по схеме из lesson.linux.05–lesson.linux.06: файл vhost кладём в sites-available/, ссылку на него — в sites-enabled/.
Частые ошибки
- Править файл прямо в
sites-enabled/— типовая ошибка: это ссылка, и вы измените файл вsites-available/. - Удалять
defaultмолча не стоит: роль сайта по умолчанию перейдёт к первому оставшемуся блокуserver, и запросы к чужому домену попадут в ваше приложение.
В sites-enabled/ одна символическая ссылка — default на
sites-available/default. Это дефолтный сайт из пакета: он отвечает на запросы,
для которых не нашлось своего server_name.
Работаем по схеме из lesson.linux.05–lesson.linux.06: файл vhost кладём
в sites-available/, ссылку на него — в sites-enabled/. Домен project-laravel.rs
при этом уже работает: dnsmasq отдаёт для всей зоны .rs адрес 127.0.0.1,
поэтому настраивать DNS заново не нужно. Позже access.log это подтвердит —
в строках будет client: 127.0.0.1.
/etc/nginx/fastcgi.conf:2:fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;Разбор команды
Проверяем, есть ли в системных файлах FastCGI параметр SCRIPT_FILENAME — от этого зависит, придётся ли задавать его руками.
grep -n "SCRIPT_FILENAME" /etc/nginx/fastcgi.conf /etc/nginx/fastcgi_params || echo "в fastcgi_params SCRIPT_FILENAME нет"| Токен | Зачем он и что меняет |
|---|---|
grep | поиск строк по шаблону |
-n | добавлять номер строки — будем ссылаться на конкретное место файла |
"SCRIPT_FILENAME" | шаблон поиска в двойных кавычках. Спецсимволов внутри нет, но с кавычками grep гарантированно получит ровно один аргумент |
/etc/nginx/fastcgi.conf /etc/nginx/fastcgi_params | два файла одним вызовом: когда аргументов несколько, grep печатает перед строкой имя файла — сравнение видно сразу |
|| | «ИЛИ» с коротким замыканием: правая часть выполняется только если grep вернул ненулевой код, то есть ничего не нашёл |
echo "в fastcgi_params SCRIPT_FILENAME нет" | запасное сообщение: отсутствие параметра будет напечатано текстом, а не пустым выводом |
Что произойдёт
Одна строка: /etc/nginx/fastcgi.conf:2:fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;. В fastcgi_params параметр отсутствует — иначе grep напечатал бы и его строку, а echo не сработал бы.
На что смотреть
Мы подключаем fastcgi_params и дописываем параметр руками. $document_root — значение директивы root для этого location, $fastcgi_script_name — путь к скрипту из запроса; вместе они дают полный путь к public/index.php нашего проекта.
Частые ошибки
- Подключили
fastcgi_paramsи не задалиSCRIPT_FILENAME— PHP-FPM не знает, какой файл выполнять; обычно это заканчивается коротким теломFile not found. ||реагирует на любой ненулевой код возврата: если файла вообще нет на диске, grep завершится ошибкой, и сообщениеechoскроет настоящую причину.
grep искал SCRIPT_FILENAME в двух файлах и нашёл только в одном:
/etc/nginx/fastcgi.conf, строка 2. Значит, в /etc/nginx/fastcgi_params
этого параметра нет — иначе grep напечатал бы и его строку.
Почему это важно: по протоколу FastCGI веб-сервер передаёт PHP-FPM набор
параметров, и SCRIPT_FILENAME — путь к файлу, который нужно выполнить.
Без него PHP-FPM не знает, что запускать.
Мы подключаем fastcgi_params и дописываем параметр руками:
$document_root — значение директивы root для этого location,
$fastcgi_script_name — путь к скрипту из запроса. Вместе они дадут
/home/student/recoursia/project-laravel/public/index.php.
serve работаетСтатика напрямую из nginx, а не через PHP-процесс; access.log и error.log;
несколько сайтов на одном порту 80 через server_name; пул процессов PHP-FPM
вместо одного процесса. И главное — это боевая схема: так же ваш проект будет
стоять на настоящем сервере.
Ошибка № 1: root указывает на папку проекта — получаем 403
Конфиг выглядит разумно: server_name project-laravel.rs, index index.php index.html,
.php уходит в PHP-FPM через сокет php8.3-fpm.sock. Одна строка всё ломает —
root /home/student/recoursia/project-laravel;.
Дальше — привычный ритуал из lesson.linux.06: ссылка в sites-enabled,
sudo nginx -t (проверить синтаксис до перезагрузки), systemctl reload nginx
(перечитать конфиг без обрыва соединений), и только потом запрос.
nginx: the configuration file /etc/nginx/nginx.conf syntax is oknginx: configuration file /etc/nginx/nginx.conf test is successfulGET http://project-laravel.rs/ -> 403<html><head><title>403 Forbidden</title></head><body><center><h1>403 Forbidden</h1></center><hr><center>nginx/1.24.0 (Ubuntu)</center></body></html>--- access.log ---127.0.0.1 - - [17/Oct/2026:10:20:33 +0000] "GET / HTTP/1.1" 403 162 "-" "curl/8.5.0"127.0.0.1 - - [17/Oct/2026:10:20:33 +0000] "GET / HTTP/1.1" 403 162 "-" "curl/8.5.0"
Разбор команды
Включаем первый вариант vhost — с намеренной ошибкой в root — и проходим весь ритуал: ссылка, проверка синтаксиса, перезагрузка, запрос, лог.
sudo ln -sf /etc/nginx/sites-available/project-laravel.rs.conf /etc/nginx/sites-enabled/project-laravel.rs.conf| Токен | Зачем он и что меняет |
|---|---|
sudo | запись в /etc/nginx требует прав root (на стенде sudo настроен без пароля) |
ln | создание ссылки |
-sf | два флага слитно: -s — символическая ссылка, а не жёсткая (так устроена схема nginx: файл в sites-available/, ссылка в sites-enabled/); -f — перезаписать цель без вопроса, если ссылка уже есть, иначе повторный запуск упадёт |
/etc/nginx/sites-available/project-laravel.rs.conf | источник — файл vhost, который мы написали |
/etc/nginx/sites-enabled/project-laravel.rs.conf | цель — имя ссылки в каталоге, который читает nginx |
Что произойдёт
Ссылка создана, конфиг считается включённым. Сам nginx о нём ещё не знает — нужна проверка и reload.
На что смотреть
Порядок аргументов: сначала источник, потом цель. Перепутаете — и в sites-available/ окажется ссылка, а не файл.
Частые ошибки
- Без
-fпри уже существующей ссылкеlnответитFile exists. - Ссылка сама по себе ничего не меняет в работе сервера: конфиг применяется только после reload.
sudo nginx -t| Токен | Зачем он и что меняет |
|---|---|
sudo | чтение чужих конфигов и проверка от root |
nginx | сам сервер |
-t | test: разобрать все конфиги и проверить синтаксис, ничего не запуская и не перезагружая |
Что произойдёт
Две строки: the configuration file /etc/nginx/nginx.conf syntax is ok и test is successful.
На что смотреть
Проверка ловит только синтаксис. Наша ошибка смысловая — root указывает на папку проекта, а не на public, — поэтому nginx -t молчит. Это и есть главная ловушка: «конфиг проверен» не значит «конфиг верен».
Частые ошибки
nginx -tне проверяет, существует ли каталог изrootи лежит ли в нёмindex.php; проверку нельзя заменить запросом — и наоборот.
sudo systemctl reload nginx| Токен | Зачем он и что меняет |
|---|---|
sudo | управление системным сервисом требует root |
systemctl | интерфейс к systemd |
reload | перечитать конфигурацию без остановки процесса и без обрыва соединений — в отличие от restart |
nginx | имя сервиса |
Что произойдёт
nginx перечитывает конфиги и поднимает блок server для домена project-laravel.rs.
На что смотреть
reload сохраняет существующие соединения. Если в конфиге ошибка, перезагрузка не применится — поэтому перед ней и стоит nginx -t.
Частые ошибки
- Забыли reload — запросы идут по старому конфигу, и вы «исправляете» то, что уже исправлено.
sleep 1| Токен | Зачем он и что меняет |
|---|---|
sleep | пауза |
1 | одна секунда |
Что произойдёт
Даём reload завершиться до первого запроса.
На что смотреть
Обычно секунды хватает; если ответ выглядит как старый, увеличьте паузу или посмотрите systemctl status nginx.
curl -sS -o /dev/null -w "GET http://project-laravel.rs/ -> %{http_code}\n" http://project-laravel.rs/| Токен | Зачем он и что меняет |
|---|---|
curl | HTTP-клиент |
-sS | тихо, но с сообщениями об ошибках |
-o /dev/null | тело ответа выбросить: на этом шаге нужен только код |
-w "GET http://project-laravel.rs/ -> %{http_code}\n" | своя подпись и одна подстановка — код ответа; \n в конце обязателен, иначе строка сольётся с приглашением |
http://project-laravel.rs/ | адрес запроса: домен локальной зоны .rs, для которой dnsmasq отдаёт 127.0.0.1 |
Что произойдёт
GET http://project-laravel.rs/ -> 403.
На что смотреть
403, а не 404: nginx искал в корне сайта index.php или index.html, не нашёл, а показывать список каталога запрещено — autoindex по умолчанию выключен.
Частые ошибки
- По одному коду причину не определить — следующим шагом всегда смотрим тело ответа и
error.log.
curl -sS http://project-laravel.rs/ | head -8| Токен | Зачем он и что меняет |
|---|---|
curl | второй запрос — теперь ради тела ответа |
-sS | тихо, но с ошибками |
http://project-laravel.rs/ | тот же адрес |
| | пайп: тело ответа передаётся следующей команде |
head | оставить только начало |
-8 | первые 8 строк: страница ошибки nginx короткая и целиком в них помещается |
Что произойдёт
HTML-страница ошибки самого nginx: <title>403 Forbidden</title>, <center><h1>403 Forbidden</h1></center> и подпись nginx/1.24.0 (Ubuntu).
На что смотреть
Подпись nginx/... — ключ к диагнозу: ошибку вернул веб-сервер, до PHP-FPM дело не дошло и Laravel не запускался. У Laravel страница ошибки своя.
Частые ошибки
- Пустое тело вместо страницы — обычно значит, что PHP-FPM не ответил; смотрите заголовки (
curl -I) иerror.log. - Такой
rootопасен не только кодом 403: снаружи оказывается виден весь проект, включая.env, — запрета скрытых файлов в этом варианте нет.
echo "--- access.log ---"; sudo tail -2 /var/log/nginx/access.log| Токен | Зачем он и что меняет |
|---|---|
echo "--- access.log ---" | подпись-разделитель: в одном захвате видно, где кончается вывод curl и где начинается лог |
; | разделитель команд shell |
sudo | лог принадлежит root — читать его нужно с повышением |
tail | печатать конец файла |
-2 | последние две строки — наши два запроса |
/var/log/nginx/access.log | журнал запросов nginx |
Что произойдёт
Строка --- access.log --- и две одинаковые записи — по одной на каждый из двух наших запросов: 127.0.0.1 - - [17/Oct/2026:10:20:33 +0000] "GET / HTTP/1.1" 403 162 "-" "curl/8.5.0".
На что смотреть
Код 403 и 162 байта — размер страницы ошибки nginx. Клиент 127.0.0.1: домен project-laravel.rs разрешился в локальный адрес, это работа dnsmasq.
Частые ошибки
access.logфиксирует факт, но не причину: объяснение 403 лежит вerror.log—directory index of "/home/student/recoursia/project-laravel/" is forbidden.
Разбор. nginx -t говорит, что синтаксис в порядке: конфиг корректен,
ошибка смысловая, её проверка синтаксиса не ловит.
GET / → 403. И тело ответа — страница самого nginx:
<center>nginx/1.24.0 (Ubuntu)</center>. Это значит, что до PHP дело не дошло:
Laravel не запускался, ошибку вернул веб-сервер.
Почему 403, а не 404? nginx ищет в корне сайта index.php или index.html.
В папке проекта их нет — index.php лежит на уровень глубже, в public/.
Не найдя индексного файла, nginx хотел бы показать список каталога, но
autoindex по умолчанию выключен — отсюда «доступ запрещён». Строку
directory index of "/home/student/recoursia/project-laravel/" is forbidden мы увидим
в error.log на слайде про логи.
И это ещё полбеды: при таком root снаружи виден весь проект, включая .env
с ключом приложения и доступом к базе. Никакого deny для скрытых файлов
в этом варианте нет — проверять мы это, разумеется, не будем.
8.3 и php8.3 в командах выше — версия стенда. Проверьте свою: php -v. Если она другая — замените 8.3 на свою во всех путях (/etc/php/…, /run/php/…), именах пакетов (php8.3-*) и сервисе (php8.3-fpm).
Ошибка № 2: root верный, но нет try_files — 404 на маршрутах
Поправили одну строку: root теперь указывает на public. Всё остальное
прежнее — и location / с try_files по-прежнему нет.
Что должно произойти по нашей теории? Корень сайта правильный, index.php
в нём есть, значит главная страница заработает. А вот /hello/студент —
не файл на диске, и nginx об этом ничего не знает.
nginx: the configuration file /etc/nginx/nginx.conf syntax is oknginx: configuration file /etc/nginx/nginx.conf test is successfulGET / -> 200GET /hello/студент -> 404<html><head><title>404 Not Found</title></head><body><center><h1>404 Not Found</h1></center><hr><center>nginx/1.24.0 (Ubuntu)</center></body>
Разбор команды
Проверяем второй вариант: root уже указывает на public, но try_files по-прежнему нет. Тот же ритуал — проверка, reload, два запроса.
sudo nginx -t && sudo systemctl reload nginx| Токен | Зачем он и что меняет |
|---|---|
sudo | права root для работы с конфигом и сервисом |
nginx | сервер |
-t | проверить синтаксис конфигов, ничего не перезагружая |
&& | логическое «И»: вторая команда выполнится, только если первая завершилась с кодом 0 — нельзя «перезалить» заведомо сломанный конфиг |
sudo systemctl reload nginx | перечитать конфигурацию без обрыва соединений |
Что произойдёт
syntax is ok и test is successful, затем конфиг перечитан.
На что смотреть
Связка через &&, а не через ; — это страховка: перезагрузка зависит от результата проверки. Смысловых ошибок nginx -t не видит, поэтому отсутствие try_files он не заметит.
Частые ошибки
- Если проверка не прошла, reload не выполнится вовсе, и nginx продолжит работать по старому конфигу — в логе сервиса останется сообщение об ошибке.
sleep 1| Токен | Зачем он и что меняет |
|---|---|
sleep | пауза |
1 | одна секунда — даём reload завершиться |
Что произойдёт
Терминал ждёт секунду до первого запроса.
На что смотреть
Без паузы запрос может попасть в момент перечитывания конфига, и вы снимете старое поведение.
curl -sS -o /dev/null -w "GET / -> %{http_code}\n" http://project-laravel.rs/| Токен | Зачем он и что меняет |
|---|---|
curl | HTTP-клиент |
-sS | тихо, но с ошибками |
-o /dev/null | тело не нужно — меряем код ответа |
-w "GET / -> %{http_code}\n" | подпись и код ответа одной строкой |
http://project-laravel.rs/ | корневой адрес сайта |
Что произойдёт
GET / -> 200 — главная страница заработала.
На что смотреть
Сработала директива index index.php: nginx нашёл файл в public/, передал его в PHP-FPM, и Laravel вернул Hello, Laravel!.
Частые ошибки
- 200 на главной не означает, что работают маршруты: следующий запрос это и проверяет.
curl -sS -o /dev/null -w "GET /hello/студент -> %{http_code}\n" "http://project-laravel.rs/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82"| Токен | Зачем он и что меняет |
|---|---|
curl | запрос к маршруту с параметром |
-sS | тихо, но с ошибками |
-o /dev/null | тело выбрасываем |
-w "GET /hello/студент -> %{http_code}\n" | подпись с кириллицей и код ответа: в терминале — читаемый текст |
"http://project-laravel.rs/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82" | URL в кавычках; по сети кириллица уходит в percent-encoding — семь пар %XX на слово «студент» |
Что произойдёт
GET /hello/студент -> 404.
На что смотреть
Этот 404 вернул nginx, а не Laravel: маршрут существует, мы его видели в route:list, но до роутера запрос не дошёл.
Частые ошибки
- Отличать источники ошибки полезно: страница nginx подписана
nginx/1.24.0 (Ubuntu), у Laravel страница ошибки своя.
curl -sS "http://project-laravel.rs/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82" | head -6| Токен | Зачем он и что меняет |
|---|---|
curl | тот же адрес — теперь ради тела ответа |
-sS | тихо, но с ошибками |
"http://project-laravel.rs/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82" | адрес в кавычках |
| | пайп |
head | оставить начало вывода |
-6 | шесть строк: страница ошибки nginx почти целиком — последняя строка </html> в них не попала |
Что произойдёт
Страница <title>404 Not Found</title> с подписью nginx/1.24.0 (Ubuntu); последняя видимая строка — </body>, закрывающий </html> обрезан head -6.
На что смотреть
Подпись подтверждает: 404 сгенерировал веб-сервер. Причина — nginx обслуживает запрос как запрос к файлу: ищет на диске public/hello/студент, не находит и сам отдаёт 404. В error.log это записано как open() "/home/student/recoursia/project-laravel/public/hello/студент" failed (2: No such file or directory).
Частые ошибки
- Маршруты Laravel — не файлы, их нет на диске: они объявлены в
routes/web.php. Веб-серверу нужно прямо сказать, что всё несуществующее уходит вindex.php. - Лечится одной директивой:
try_files $uri $uri/ /index.php?$query_string;. Без?$query_stringпараметры вроде?page=2потеряются при передаче.
Разбор. GET / → 200: index index.php сработал, nginx передал
public/index.php в PHP-FPM, Laravel отдал строку Hello, Laravel!.
GET /hello/студент → 404, и снова страница с подписью
nginx/1.24.0 (Ubuntu) — то есть 404 вернул nginx, не Laravel. Отличать
их полезно: у Laravel страница ошибки своя.
Причина: nginx обслуживает запрос как запрос к файлу. Он ищет на диске
/home/student/recoursia/project-laravel/public/hello/студент, не находит и сам отдаёт
404. До index.php — а значит, и до таблицы маршрутов — запрос не доходит
вообще.
Маршруты Laravel — не файлы. Их нет на диске, они объявлены в routes/web.php.
Поэтому веб-серверу нужно прямо сказать: «всё, чего нет в файловой системе,
передавай в index.php». Это и делает try_files.
8.3 и php8.3 в командах выше — версия стенда. Проверьте свою: php -v. Если она другая — замените 8.3 на свою во всех путях (/etc/php/…, /run/php/…), именах пакетов (php8.3-*) и сервисе (php8.3-fpm).
Итоговый vhost: четыре директивы, которые всё решают
Строки 3–5. listen 80 и listen [::]:80 — принимаем HTTP на 80-м порту
по IPv4 и IPv6. server_name project-laravel.rs — этим доменом занимается именно этот
блок server.
Строка 8. root .../project-laravel/public — корень сайта обязательно public.
Всё, что выше, для nginx не существует.
Строка 9. index index.php index.html — чем отвечать на запрос к каталогу.
Строки 12–13. Главное новое:
Проверки идут слева направо, срабатывает первая совпавшая:
Именно поэтому статика из public/ отдаётся быстро и без PHP, а всё остальное
уходит в роутер Laravel.
?$query_string в конце — обязательная деталь: без неё параметры после «?»
потеряются при передаче, и ?page=2 из пагинации (lesson.http.i.05) до
Laravel не доедет.
Строки 17–20. .php уходит в PHP-FPM через unix-сокет
/run/php/php8.3-fpm.sock — тот же адрес и тот же протокол FastCGI, что
в lesson.linux.06. include fastcgi_params + явный SCRIPT_FILENAME —
по причине, которую мы проверили grep-ом.
Строки 24–25. location ~ /\. { deny all; } — любой путь, в котором есть
«точка после слэша» (.env, .git, .htaccess), запрещён. Второй рубеж защиты
к тому, что root уже спрятал эти файлы.
Схема ровно та, что вы собрали в lesson.linux.06: server_name → root →
location для .php → fastcgi_pass в сокет. Отличия два: root на один
уровень глубже (/public), и добавлен try_files, потому что теперь
адреса не обязаны совпадать с файлами на диске.
8.3 и php8.3 в командах выше — версия стенда. Проверьте свою: php -v. Если она другая — замените 8.3 на свою во всех путях (/etc/php/…, /run/php/…), именах пакетов (php8.3-*) и сервисе (php8.3-fpm).
Проверяем: 200 на оба маршрута и заголовки ответа
nginx: the configuration file /etc/nginx/nginx.conf syntax is oknginx: configuration file /etc/nginx/nginx.conf test is successfulGET / -> 200GET /hello/студент -> 200Hello, Laravel!HTTP/1.1 200 OKServer: nginx/1.24.0 (Ubuntu)Content-Type: text/html; charset=utf-8Connection: keep-aliveCache-Control: no-cache, privateDate: Sat, 17 Oct 2026 10:20:38 GMTSet-Cookie: XSRF-TOKEN=eyJpdiI6IlJhQ0YyMlV0czRPM0hsMm1pd0huSXc9PSIsInZhbHVlIjoiVWxVeHBIL3llTjNRYUdMNGdOcWxiRWhvYXh6RWxQZndmQWlwdVZaVkR0ai8yU213Ty8zTTZvMDJUd2Y3eG52c3pUVys1bjVCWldMT1VlVGExT3FWL01UYjZwbVhWRjlTQ1dUcFY0dTNOWnI0V0xlQTRTdDBwYS8yOHJldXY4eUsiLCJtYWMiOiJjMTM1ZDc1MTBjZDJkMWFlYTI0ZDRjMjY2NjUyMTBmOGU1ZmNkYjNkODUyN2M0OWRjNDdkNTkwOWUzOGQxNjcwIiwidGFnIjoiIn0%3D; expires=Sat, 17 Oct 2026 12:20:38 GMT; Max-Age=7200; path=/; samesite=laxSet-Cookie: laravel-session=eyJpdiI6IlVMMDE5WmJIcGtVOXdBbzQ1Ly9CSWc9PSIsInZhbHVlIjoidzRESEFBcGhIOStwcThPK3RSeDRJcVdvS1N5NHJiaVZxNURINmVJOXZiSjJiaUM5UUE2YzBLc052aEhpQkZsU2xaNHkvMjZrWEhkRkZZRFVENGJSd2VPc0pXV2VQQVpNVXZETmh1M0VWSVZNQUJqMEpOdXBJNjlzWHFvMXJqbi8iLCJtYWMiOiJkNmRjNTg0YjRmZmQyNWMwMDc4ODlmODAyMWI4NTEwMjRlOTNmZDcyODAyY2NiNjBiMWI5ZDI4OWY3OWZlZWI4IiwidGFnIjoiIn0%3D; expires=Sat, 17 Oct 2026 12:20:38 GMT; Max-Age=7200; path=/; httponly; samesite=lax
Разбор команды
Третий вариант vhost — с try_files и запретом скрытых файлов. Проверяем оба маршрута и смотрим, что отвечает сервер в заголовках.
sudo nginx -t && sudo systemctl reload nginx| Токен | Зачем он и что меняет |
|---|---|
sudo | права root |
nginx | сервер |
-t | проверка синтаксиса конфигов |
&& | вторая команда выполнится только при нулевом коде возврата первой |
sudo systemctl reload nginx | перечитать конфигурацию без обрыва соединений |
Что произойдёт
syntax is ok, test is successful, конфиг перечитан.
На что смотреть
Ритуал тот же, что в двух предыдущих вариантах: меняли только файл vhost — порядок проверки и перезагрузки не менялся.
Частые ошибки
nginx -tпо-прежнему не гарантирует, что сайт заработает: он проверяет синтаксис, а не поведение.
sleep 1| Токен | Зачем он и что меняет |
|---|---|
sleep | пауза |
1 | секунда до первого запроса |
Что произойдёт
Ждём завершения reload.
На что смотреть
Без паузы первый запрос рискует попасть на старый конфиг.
curl -sS -o /dev/null -w "GET / -> %{http_code}\n" http://project-laravel.rs/| Токен | Зачем он и что меняет |
|---|---|
curl | HTTP-клиент |
-sS | тихо, но с ошибками |
-o /dev/null | тело не выводим — нужен код |
-w "GET / -> %{http_code}\n" | подпись и код ответа одной строкой |
http://project-laravel.rs/ | корень сайта |
Что произойдёт
GET / -> 200.
На что смотреть
Корневой маршрут работает и через nginx: статика и index.php нашлись в public/.
Частые ошибки
- 200 здесь не новость — он был и в варианте 2. Новое начнётся со следующего запроса.
curl -sS -o /dev/null -w "GET /hello/студент -> %{http_code}\n" "http://project-laravel.rs/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82"| Токен | Зачем он и что меняет |
|---|---|
curl | запрос к маршруту с параметром |
-sS | тихо, но с ошибками |
-o /dev/null | тело не выводим |
-w "GET /hello/студент -> %{http_code}\n" | подпись и код ответа |
"http://project-laravel.rs/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82" | URL в кавычках, кириллица в percent-encoding |
Что произойдёт
GET /hello/студент -> 200 — вместо 404 из предыдущего варианта.
На что смотреть
Заработал try_files: файла hello/студент на диске нет, поэтому запрос внутренне передан на /index.php, а роутер Laravel нашёл маршрут hello/{name} в своей таблице.
Частые ошибки
- Если бы здесь снова был 404 с подписью nginx —
try_filesне применился: проверяйте, что директива внутриlocation /и что reload действительно прошёл.
curl -sS http://project-laravel.rs/| Токен | Зачем он и что меняет |
|---|---|
curl | запрос ради тела ответа |
-sS | тихо, но с ошибками |
http://project-laravel.rs/ | корень сайта; без -o тело печатается в терминал |
Что произойдёт
Одна строка: Hello, Laravel!.
На что смотреть
Тело ровно то, что вернуло замыкание в routes/web.php: статус и заголовки Laravel добавил сам. По access.log на тело ушло 25 байт.
Частые ошибки
- 200 с пустым телом — другой случай: запрос дошёл до
index.php, но PHP-FPM не получилSCRIPT_FILENAME.
echo| Токен | Зачем он и что меняет |
|---|---|
echo | без аргументов печатает пустую строку — разделитель между телом предыдущего ответа и заголовками следующего |
Что произойдёт
В выводе появляется пустая строка.
На что смотреть
Тело ответа не заканчивается переводом строки, поэтому без echo заголовки следующего curl прилипли бы к Hello, Laravel!.
curl -sSI http://project-laravel.rs/ | head -8| Токен | Зачем он и что меняет |
|---|---|
curl | HTTP-клиент |
-sSI | флаги слитно: -s — без прогресс-бара, -S — но с сообщениями об ошибках, -I — отправить HEAD-запрос и напечатать только заголовки, без тела; в access.log это останется как "HEAD / HTTP/1.1" 200 0 |
http://project-laravel.rs/ | адрес запроса |
| | пайп |
head | оставить начало вывода |
-8 | первые 8 строк: статус, пять заголовков и обе куки |
Что произойдёт
HTTP/1.1 200 OK, Server: nginx/1.24.0 (Ubuntu), Content-Type: text/html; charset=utf-8, Connection: keep-alive, Cache-Control: no-cache, private, Date: Sat, 17 Oct 2026 10:20:38 GMT и две куки: XSRF-TOKEN=... и laravel-session=..., обе с Max-Age=7200, path=/, samesite=lax.
На что смотреть
Server: nginx/1.24.0 (Ubuntu) — отвечает nginx, встроенный сервер разработки больше не участвует. charset=utf-8 — поэтому «студент» не превращается в кракозябры. Cache-Control: no-cache, private — ответ динамический и личный, промежуточным кэшам хранить его нельзя. У laravel-session дополнительно стоит httponly: такую куку не прочитать из JavaScript.
Частые ошибки
-Iотправляет HEAD, а не GET: приложение может ответить на него иначе, чем на GET.- Значения кук длинные и выглядят как мусор — это base64 от JSON с полями
iv,value,macиtag, а не опечатка. head -8обрезает вывод по строкам: обе куки попали целиком, но заголовки могут идти и дальше —Content-Lengthв первых восьми строках нет.
Оба маршрута работают. GET / → 200 и тело Hello, Laravel!,
GET /hello/студент → 200. Значит, try_files передаёт запрос в index.php,
а роутер Laravel находит в таблице и строку, и параметр.
Кто отвечает. Server: nginx/1.24.0 (Ubuntu) — отвечает nginx, встроенный
сервер разработки больше не участвует.
Content-Type: text/html; charset=utf-8 — заголовок, по которому браузер
понимает, как показывать тело (вы разбирали его в lesson.http.i.02). Указана
кодировка UTF-8 — поэтому «студент» в ответе не превращается в кракозябры.
Cache-Control: no-cache, private — ответ динамический: промежуточным кэшам
его хранить нельзя, он личный для этого пользователя.
Connection: keep-alive — соединение не закрывается после ответа, следующий
запрос уйдёт по нему же.
Две куки в ответе.
Set-Cookie: XSRF-TOKEN=... и Set-Cookie: laravel-session=..., обе
с Max-Age=7200 (два часа), path=/ и samesite=lax. У laravel-session
дополнительно стоит httponly — такую куку не прочитать из JavaScript.
Значение — не открытый текст. Это base64; если раскодировать, получится JSON
из четырёх полей: iv (24 символа base64 = 16 байт — размер блока шифра,
в config/app.php стоит 'cipher' => 'AES-256-CBC'), value (зашифрованное
содержимое), mac (64 hex-символа — код аутентификации, проверка, что значение
не подменили) и пустой tag.
То есть приложение уже ведёт сессию и выдаёт токен защиты от подделки запроса.
Что с этим делать — разберём в части 5, когда будем отправлять формы: без
XSRF-TOKEN POST-запрос будет отклонён.
Логи: где искать причину, когда ответ «не тот»
127.0.0.1 - - [17/Oct/2026:10:20:38 +0000] "GET / HTTP/1.1" 200 25 "-" "curl/8.5.0"127.0.0.1 - - [17/Oct/2026:10:20:38 +0000] "GET /hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82 HTTP/1.1" 200 447 "-" "curl/8.5.0"127.0.0.1 - - [17/Oct/2026:10:20:38 +0000] "GET / HTTP/1.1" 200 25 "-" "curl/8.5.0"127.0.0.1 - - [17/Oct/2026:10:20:38 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/8.5.0"--- error.log ---2026/10/17 10:20:33 [error] 49663#49663: *315 directory index of "/home/student/recoursia/project-laravel/" is forbidden, client: 127.0.0.1, server: project-laravel.rs, request: "GET / HTTP/1.1", host: "project-laravel.rs"2026/10/17 10:20:35 [error] 49757#49757: *318 open() "/home/student/recoursia/project-laravel/public/hello/студент" failed (2: No such file or directory), client: 127.0.0.1, server: project-laravel.rs, request: "GET /hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82 HTTP/1.1", host: "project-laravel.rs"2026/10/17 10:20:35 [error] 49758#49758: *319 open() "/home/student/recoursia/project-laravel/public/hello/студент" failed (2: No such file or directory), client: 127.0.0.1, server: project-laravel.rs, request: "GET /hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82 HTTP/1.1", host: "project-laravel.rs"
Разбор команды
Смотрим оба журнала nginx: кто приходил и почему не получалось. Причины обеих наших ошибок записаны текстом.
sudo tail -4 /var/log/nginx/access.log; echo "--- error.log ---"; sudo tail -3 /var/log/nginx/error.log| Токен | Зачем он и что меняет |
|---|---|
sudo | логи принадлежат root — читать их нужно с повышением |
tail | печатать конец файла |
-4 | последние четыре строки access.log — ровно наши четыре запроса из предыдущего слайда |
/var/log/nginx/access.log | журнал запросов: факты — кто, когда, что и с каким кодом |
; | разделитель команд shell |
echo "--- error.log ---" | подпись-разделитель перед вторым блоком вывода |
sudo tail | то же чтение от root |
-3 | последние три строки журнала ошибок: одна запись варианта 1 и две записи варианта 2 |
/var/log/nginx/error.log | журнал ошибок: здесь причина, а не факт |
Что произойдёт
Четыре строки access.log — "GET / HTTP/1.1" 200 25, "GET /hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82 HTTP/1.1" 200 447, снова "GET / HTTP/1.1" 200 25 и "HEAD / HTTP/1.1" 200 0. Затем три строки error.log: directory index of "/home/student/recoursia/project-laravel/" is forbidden (10:20:33) и дважды open() "/home/student/recoursia/project-laravel/public/hello/студент" failed (2: No such file or directory) (10:20:35).
На что смотреть
Поля access.log: IP клиента, время, «метод путь версия» в кавычках, статус, число байт, referer и User-Agent (curl/8.5.0). Путь записан в percent-encoding — как пришёл по сети. "HEAD / HTTP/1.1" 200 0 — наш curl -I: заголовки есть, тела нет. В error.log времена совпадают с попытками: 10:20:33 — вариант 1 (те самые 403), 10:20:35 — вариант 2 (404), причём строка показывает полный путь, который nginx подставил.
Частые ошибки
- Получили от nginx неожиданный код — сначала
error.log, потом догадки:nginx -tмолчал, потому что синтаксис конфигов был верным. tailпоказывает только хвост: при большом потоке запросов нужную строку ищутgrep-ом по времени, статусу или пути.access.logиerror.logпишутся разными процессами-воркерами — вerror.logвидны их PID (49663#49663), по ним можно связать записи.
access.log — кто приходил. Поля в строке: IP клиента, время,
"метод путь версия", статус-код, число байт, referer и User-Agent
(у нас curl/8.5.0). Этот формат вы разбирали в lesson.programs.01
и lesson.http.i.01.
Три детали, на которые стоит посмотреть:
- путь записан как
/hello/%D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82— в лог попадает URL в том виде, в каком он пришёл по сети, в percent-encoding; "HEAD / HTTP/1.1" 200 0— нашcurl -Iотправил HEAD-запрос: заголовки те же, что и на GET, а тела нет, поэтому байт 0;client: 127.0.0.1— доменproject-laravel.rsразрешился в локальный адрес: это работа dnsmasq изlesson.linux.05.
error.log — почему не получилось. Здесь лежат объяснения обеих наших ошибок,
и времена совпадают с попытками:
10:20:33—directory index of "/home/student/recoursia/project-laravel/" is forbidden. Это вариант 1:rootна папке проекта, индексного файла нет, список каталога показывать запрещено → те самые 403.10:20:35—open() "/home/student/recoursia/project-laravel/public/hello/студент" failed (2: No such file or directory), дважды. Это вариант 2: nginx буквально искал файл с таким именем → 404. Строка даже показывает путь, который он подставил, — сразу видно, чтоrootуже правильный, аtry_filesнет.
Получили от nginx неожиданный код — сначала error.log, потом догадки.
В обеих наших ошибках причина была написана текстом; nginx -t при этом
молчал, потому что синтаксис конфигов был верным.
Список маршрутов и проверка, что .env снаружи не отдаётся
GET|HEAD / .......................................... routes/web.php:5 GET|HEAD hello/{name} ............................... routes/web.php:9 Showing [2] routes
Разбор команды
Сверяем таблицу маршрутов с тем, что написано в routes/web.php, — за секунду видно, зарегистрированы ли наши маршруты и где именно.
php artisan route:list --except-vendor| Токен | Зачем он и что меняет |
|---|---|
php | интерпретатор; на стенде PHP 8.3 — подставьте свою версию (php -v) |
artisan | консольный вход в приложение |
route:list | команда вывода таблицы маршрутов: метод, путь и место, где маршрут объявлен |
--except-vendor | не показывать маршруты, которые регистрируют каркас и пакеты: в списке остаются только наши |
Что произойдёт
Две строки: GET|HEAD с путём / и указанием routes/web.php:5, затем GET|HEAD с путём hello/{name} и указанием routes/web.php:9; итог — Showing [2] routes.
На что смотреть
Номера строк совпадают с местами, где написаны Route::get('/', ...) и Route::get('/hello/{name}', ...). Методов у каждого маршрута два: HEAD — это GET без тела ответа, такой запрос мы видели в access.log. Служебный /up из bootstrap/app.php в списке не показан: его регистрирует каркас, и флаг его отфильтровал.
Частые ошибки
- Без
--except-vendorсписок длиннее, и свои маршруты в нём теряются. - Маршрута нет в таблице — значит, файл с ним не подключён:
withRoutingвbootstrap/app.phpуказывает наroutes/web.php. - После кэширования маршрутов (
php artisan optimize) таблица берётся из кэша — свежую правку файла она не покажет, пока кэш не сбросили.
php artisan route:list печатает таблицу маршрутов: метод, путь и место,
где маршрут объявлен. Обе строки указывают на routes/web.php, строки 5 и 9 —
ровно те номера, на которых мы написали Route::get('/', ...) и
Route::get('/hello/{name}', ...). Проверить себя можно за секунду.
Методов у каждого маршрута два: GET|HEAD. HEAD — это GET без тела ответа:
мы только что видели такой запрос в access.log ("HEAD /" 200 0).
Флаг --except-vendor убирает из списка маршруты, которые регистрирует сам
каркас, и оставляет только наши; поэтому итог — Showing [2] routes. Служебный
/up из bootstrap/app.php в этом списке не показан.
GET /.env -> 403Разбор команды
Проверяем второй рубеж защиты: файл .env с ключом приложения и доступом к базе не должен отдаваться снаружи.
curl -sS -o /dev/null -w "GET /.env -> %{http_code}\n" http://project-laravel.rs/.env| Токен | Зачем он и что меняет |
|---|---|
curl | HTTP-клиент |
-sS | тихо, но с сообщениями об ошибках |
-o /dev/null | тело ответа выбрасываем. Здесь это принципиально: если бы файл отдавался, секрет оказался бы в терминале и в истории |
-w "GET /.env -> %{http_code}\n" | подпись и код ответа одной строкой; \n не даёт строке слиться с приглашением |
http://project-laravel.rs/.env | запрос к скрытому файлу в корне сайта — именно так это сделал бы злоумышленник |
Что произойдёт
GET /.env -> 403.
На что смотреть
403, а не 404 — важная деталь. Файла public/.env не существует (в public/ четыре файла), «честным» ответом был бы 404. Но сработал location ~ /\. { deny all; }: nginx запрещает запрос, даже не проверяя наличие файла.
Частые ошибки
- 404 вместо 403 означает, что запрета нет и вас защищает только
root: ошибётесь вroot— и.envуедет наружу. Такую ошибку мы сами сделали в варианте 1. - Утечка
.env— этоAPP_KEYи доступ к базе: получив файл, посетитель сможет расшифровать куки приложения.
403, а не 404 — и это важная деталь.
Файла public/.env не существует: в public/ четыре файла, мы их перечисляли.
«Честным» ответом был бы 404. Но сработал location ~ /\. { deny all; } —
nginx запрещает запрос, даже не проверяя наличие файла.
Зачем второй рубеж, если root уже спрятал .env выше корня сайта? Затем, что
root — настройка, которую меняют руками. Мы сами ошиблись в ней на этой же
паре слайдов. Если когда-нибудь vhost уедет на папку проекта, запрет скрытых
файлов всё равно не даст скачать .env с ключом приложения и доступом к базе.
GET / → 200, GET /hello/студент → 200, GET /.env → 403,
route:list → 2 наших маршрута.
Те же страницы — теперь на домене project-laravel.rs


Слева — корень домена: маршрут, который возвращает строку, и Laravel заворачивает
её в полноценный HTTP-ответ. Справа — маршрут с параметром: строка, собранная
шаблоном hello.blade.php.
Обратите внимание на адресную строку второго снимка: браузер сам закодировал
кириллицу в %D1%81%D1%82%D1%83%D0%B4%D0%B5%D0%BD%D1%82. Путь этого запроса —
dnsmasq, nginx с нашим vhost, unix-сокет, PHP-FPM и Laravel — целиком собран
схемой на следующем слайде.
Жизненный цикл запроса целиком
Соберём в одну схему всё, что в этой части проверено захватами: девять узлов —
девять шагов пути запроса GET /hello/студент.
8.3 и php8.3 в путях и именах сервисов этой части — версия стенда. Проверьте свою: php -v. Если она другая — замените 8.3 на свою во всех путях (/etc/php/…, /run/php/…), именах пакетов (php8.3-*) и сервисе (php8.3-fpm).
Что мы разобрали и где это лежит
Вне пути запроса, но в этой же части
app/Models/User.php— таблицаusersкак класс (части 4–5);database/migrations/— схема базы в коде,up()/down()(часть 4);artisan— те жеbootstrap/app.php, ноhandleCommand();/etc/nginx/sites-available/project-laravel.rs.conf+ ссылка вsites-enabled/;/var/log/nginx/error.log— почему не получилось, когдаaccess.logсказал «нет».
Числа этой части
- 4 файла в
public/— всё, что видит nginx; - 10 файлов конфигурации в
config/; - 3 миграции из коробки → 3 таблицы (
users,password_reset_tokens,sessions); - 2 маршрута показывает
route:list --except-vendor; - 12 каталогов и 0 файлов в
storage/до первого запроса.
Измерения
- 200 / 200 —
/и/hello/студентчерез nginx наproject-laravel.rs; - 403 — на
/.env, и столько же вернул неверныйroot; 404 — маршруты безtry_files; - 70 403 байта, 0.439242 с — стартовая страница каркаса через
serve; - 435 байт, 0.029839 с — наш маршрут через
serve; - 72 662 и 593 байта — два скомпилированных шаблона в
storage/framework/views/; - Laravel Framework 13.37.0, nginx/1.24.0 (Ubuntu), сокет php8.3-fpm.sock.
8.3 и php8.3 в командах выше — версия стенда. Проверьте свою: php -v. Если она другая — замените 8.3 на свою во всех путях (/etc/php/…, /run/php/…), именах пакетов (php8.3-*) и сервисе (php8.3-fpm).
Проверьте себя
Отвечайте своими словами, не подглядывая в слайды. Ответы — на следующем слайде.
- Почему
rootв nginx должен указывать наpublic/, а не на папку проекта? Какой конкретно файл утечёт, если указать неверно, и что в нём лежит? - Через nginx
/отвечает 200, а/hello/студент— 404 со страницейnginx/1.24.0 (Ubuntu). Какой директивы не хватает в vhost и почему без неё маршрут Laravel не срабатывает? - Чем
config/app.phpотличается от.env? Где правитьAPP_DEBUG, пароль базы и где живёт'timezone' => 'UTC'? - Что делает
{{ $name }}в Blade кроме подстановки значения? От какой атаки это защищает и какой знакомый вам приём защиты играет ту же роль на стороне SQL? - Куда Laravel складывает скомпилированные шаблоны и почему отсутствие прав на запись в этот каталог роняет сайт?
Коротко: ответы
1. Корень сайта — область, из которой nginx раздаёт файлы. Всё остальное лежит
выше и по HTTP недоступно. При root на папке проекта снаружи виден .env —
ключ приложения APP_KEY и доступ к базе. У нас в vhost есть второй рубеж:
location ~ /\. { deny all; }, поэтому GET /.env → 403 (а не 404: nginx
запрещает запрос, не проверяя наличие файла).
2. try_files $uri $uri/ /index.php?$query_string;. Без неё nginx обслуживает
запрос как запрос к файлу: ищет public/hello/студент на диске, не находит
и сам отдаёт 404 — до index.php и таблицы маршрутов дело не доходит.
В error.log это выглядит как open() ".../public/hello/студент" failed
(2: No such file or directory).
3. config/*.php — общие для всех окружений, лежат в git, значения читают
через env('КЛЮЧ', 'по умолчанию'). .env — секреты и отличия конкретной машины,
в git не попадает (он в .gitignore). APP_DEBUG и пароль базы — в .env;
'timezone' => 'UTC' — прямо в config/app.php, строка 68, без env().
4. Прогоняет значение через e(): < и > заменяются на < и >,
поэтому данные из адреса не становятся разметкой. Это защита от XSS. На стороне SQL
ту же работу делает подготовленный запрос с маркерами (PDO): значение передаётся
отдельно от текста запроса и не может изменить его структуру. Общее правило —
не доверять данным клиента.
5. storage/framework/views/ — файлы с именами-хэшами; в нашем прогоне их два,
72 662 и 593 байта. Blade-метки — не PHP: шаблон сначала компилируется, и результат
нужно записать. Нет прав на запись у пользователя PHP-FPM — нечего выполнять,
запрос падает. То же касается bootstrap/cache/: там кэш конфигурации и сервисов.
Права мы проверяли: drwxrwxrwx, владелец student — тот же пользователь,
от которого работает веб-стек.
Что дальше
Часть 3 — контроллеры, Blade и список пользователей из массива.
Что возьмём с собой из этой части:
- проект
~/recoursia/project-laravelи vhostproject-laravel.rs— сайт уже отвечает через nginx, домен менять не будем; routes/web.phpс двумя маршрутами — их мы перепишем;- понимание, что шаблон компилируется в
storage/framework/views/, а значит, права на запись там должны остаться рабочими.
Что будет нового:
- замыкания в маршрутах заменим на контроллеры: метод класса вместо анонимной
функции — их создадим командой
php artisan make:controller; - макет страницы: общий каркас с шапкой и меню, от которого наследуются остальные шаблоны;
- список пользователей из массива с выводом в таблицу — та же задача, что
вы решали в
lesson.http.i.04, только через Blade и без ручногоecho; - свои стили: положим
public/css/app.cssи подключим<link>— сборщик Vite для этого не нужен.
Ничего переустанавливать не требуется: нужны проект из части 1, работающий vhost
из этой части и запущенные nginx с PHP-FPM. Проверить, что всё на месте, можно
двумя командами: curl -sS -o /dev/null -w "%{http_code}\n" http://project-laravel.rs/
и php artisan route:list --except-vendor.