# Развёртывание

## Требования production

| Компонент | Версия |
|---|---|
| PHP | 8.3+ с `pdo_mysql`, `mbstring`, `intl`, `bcmath`, `gd`, `exif`, `fileinfo`, `openssl`, `curl`, `zip`, `sodium`, `opcache` |
| MySQL | 8.x или совместимая MariaDB, InnoDB, utf8mb4 |
| Node | 22+ — только на этапе сборки |
| Веб-сервер | Nginx + PHP-FPM |

Отдельного сервера кеша не требуется: кеш, очереди и блокировки работают на MySQL — см.
[assumptions.md](assumptions.md), D1.

## Первичная установка

```bash
git clone git@gitverse.ru:restomesto/restomesto.git
cd restomesto

composer install --no-dev --optimize-autoloader
cp .env.example .env
php artisan key:generate
```

Заполнить `.env` (см. ниже), затем:

```bash
php artisan migrate --force
php artisan db:seed --force
php artisan storage:link

npm ci
npm run build

php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
```

## Обязательные переменные окружения

```dotenv
APP_ENV=production
APP_DEBUG=false
APP_URL=https://restomesto.ru
APP_TIMEZONE=UTC
APP_LOCALE=ru

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=restomesto
DB_USERNAME=
DB_PASSWORD=

CACHE_STORE=database
QUEUE_CONNECTION=database
SESSION_DRIVER=database
SESSION_SECURE_COOKIE=true

FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_BUCKET=
AWS_ENDPOINT=
AWS_URL=

# Партнёрский API Restoplace
RESTOPLACE_DRIVER=http
RESTOPLACE_BASE_URL=https://api.restoplace.ws
RESTOPLACE_PARTNER_TOKEN=
RESTOPLACE_WEBHOOK_SECRET=

# Карты
MAP_PROVIDER=yandex
YANDEX_MAPS_API_KEY=

# Вход гостя
FLASH_CALL_DRIVER=voicepassword
VOICEPASSWORD_API_KEY=

SOCIAL_AUTH_DRIVER=oauth
VK_CLIENT_ID=
VK_REDIRECT_URI=https://restomesto.ru/login/vk/callback
YANDEX_CLIENT_ID=
YANDEX_CLIENT_SECRET=
YANDEX_REDIRECT_URI=https://restomesto.ru/login/yandex/callback

# Модерация отзывов и уведомления
AI_DRIVER=deepseek
DEEPSEEK_API_KEY=
TELEGRAM_BOT_TOKEN=
TELEGRAM_ADMIN_CHAT_ID=
```

`APP_DEBUG=false` обязателен: stack trace в production не показывается (ТЗ §21).

Три драйвера-заглушки в production — авария, и приложение пишет об этом `critical` при старте:
`RESTOPLACE_DRIVER=fake` (канал `restoplace`), `FLASH_CALL_DRIVER=manual` (канал `auth`, код входа
фиксирован — войти сможет кто угодно) и `AI_DRIVER=fake` (канал `ai`, публикуется почти всё).
Проверьте все три перед первым релизом.

Без `TELEGRAM_BOT_TOKEN` служебные уведомления не теряются: они уходят в
`storage/logs/notifications.log` и помечаются в `notification_logs` как `failed`.

Адреса колбэков в кабинетах VK и Яндекса должны совпадать с `*_REDIRECT_URI` посимвольно, иначе
провайдер откажет. У VK ID секрет в обмене кода не участвует — это публичный PKCE-клиент.

Секреты хранятся только в окружении или secret storage и **никогда** в Git (ТЗ §21).

## Стенд restomesto.com (201.51.7.92)

Развёрнут на том же сервере, что и `teeu`, по его схеме. HestiaCP, Ubuntu 24.04, nginx → Apache,
PHP 8.3, MariaDB 11.4. Node на сервере нет — собранные ассеты приезжают из CI.

```
/home/restomesto/web/restomesto.com/
├── public_html -> private/restomesto/public   симлинк, docroot Hestia
└── private/
    ├── restomesto/                            приложение
    └── hestia-placeholder/                    заглушка Hestia, убрана из docroot
```

**Стенд работает на заглушках:** `RESTOPLACE_DRIVER=fake`, `FLASH_CALL_DRIVER=manual` (код входа
`0000`), `SOCIAL_AUTH_DRIVER=fake`, `AI_DRIVER=fake`. Приложение раз в час пишет об этом
`critical` в свои каналы — так заглушку нельзя не заметить. Для боевого запуска меняются четыре
значения в `.env` и подставляются ключи.

**Репозиторий** — GitVerse, `git@gitverse.ru:restomesto/restomesto.git`. На сервере лежит ключ
развёртывания **без права записи**: сервер только забирает код. CI описан в
`.gitverse/workflows/ci.yaml` — GitVerse читает конфигурацию оттуда и использует синтаксис
GitHub Actions.

**Обновление** — `deploy.sh` в корне приложения: `git reset --hard origin/main`, зависимости,
режим обслуживания, миграции, кеши, перезапуск очереди. Запускается от пользователя домена:

```bash
sudo -u restomesto /home/restomesto/web/restomesto.com/private/restomesto/deploy.sh
```

**Планировщик и очередь** — в cron пользователя `restomesto`, supervisor на сервере нет. Воркер
поднимается ежеминутно и живёт 55 секунд, `flock` не даёт запуститься второму:

```
* * * * * /usr/bin/php8.3 -d disable_functions= .../artisan schedule:run >/dev/null 2>&1
* * * * * flock -n /tmp/restomesto-queue.lock /usr/bin/php8.3 -d disable_functions= .../artisan \
    queue:work --stop-when-empty --sleep=1 --tries=3 --max-time=55 \
    --queue=bookings,webhooks,notifications,ai,images,sync,default >> .../storage/logs/worker.log 2>&1
```

`-d disable_functions=` обязателен: панель ограничивает функции в php.ini, а Laravel нужен
`proc_open`. Без него падает и планировщик, и composer.

**Канонический адрес — `https://restomesto.com` без `www`.** У домена три алиаса
(`www.restomesto.com`, `restomesto.online`, `www.restomesto.online`), все ведут в тот же каталог,
и сертификат Let's Encrypt выпущен на все четыре имени. Любой другой хост и любой запрос по
`http` получают 301 на канонический адрес — причём одним ответом, а не цепочкой: middleware
`CanonicalHost`, переменные `CANONICAL_HOST` и `FORCE_HTTPS`.

Правило живёт в приложении, а не в конфигурации панели: та перегенерирует свои файлы при любой
правке домена, и оно пропало бы молча.

Проверка Let's Encrypt (`/.well-known/acme-challenge/`) на HTTPS не перенаправляется: иначе
продление сертификата однажды тихо перестало бы работать.

При добавлении нового алиаса сертификат перевыпускается со всем списком сразу:

```bash
v-add-letsencrypt-domain restomesto restomesto.com "www.restomesto.com,restomesto.online,www.restomesto.online"
```

**Стенд закрыт паролем на время разработки.** HTTP-авторизация включается парой
`BASIC_AUTH_USER` / `BASIC_AUTH_PASSWORD` в окружении; пусто — сайт открыт. Без пароля отвечают
только три адреса: проверка Let's Encrypt `/.well-known/`, приём webhook `/api/webhooks/` и
проба `/up` — их шлют машины, ввести пароль они не могут.

Это заслонка от посторонних глаз, а не защита данных: пароль один на всех и передаётся в каждом
запросе. Доступ в кабинеты по-прежнему проверяют guard-ы. Снять — очистить `BASIC_AUTH_USER` и
выполнить `deploy.sh`; правка кода не нужна.

**Известные особенности сервера.** MariaDB 11.4 вместо MySQL 8.4 — расхождение с решением D4;
на текущем наборе миграций разницы не проявилось, но при добавлении оконных функций и JSON-путей
это стоит перепроверить.

После выпуска сертификата **обязательно перезагрузить и nginx, и Apache**: без перезагрузки
Apache запросы по HTTPS уходят на чужой vhost и возвращают чужие 200 и 404. На это ушло полчаса
разбирательств — симптом выглядит как «Laravel не видит маршруты».

## Статика Filament

Панель `/developer` раздаёт свой JavaScript из `public/js/filament` — эти файлы не в репозитории,
их раскладывает `php artisan filament:assets`. Команда вызывается сама: она входит в
`filament:upgrade`, а тот прописан в `post-autoload-dump` в `composer.json` и срабатывает на любой
установке зависимостей — локально, в CI и на сервере.

Если каталога нет, панель отдаёт страницы со ссылками на 404 и **выглядит почти рабочей**:
разметка на месте, но боковое меню не раскрывается, выпадающие списки уезжают за край экрана, а у
кнопок пропадают подписи. Отдельного признака «скрипт не загрузился» на странице нет — его видно
только в консоли браузера. За этим следит `tests/Feature/Admin/AdminPanelAssetsTest.php`: он
сверяет все ссылки на `/js/filament/` в разметке с файлами на диске.

Ассеты самого приложения (`public/build`) собирает Vite и они в репозиторий тоже не попадают —
это разные наборы, `npm run build` статику Filament не создаёт.

## Первый администратор

Регистрации в панели нет: первый администратор создаётся из консоли, остальных заводит он сам
(ТЗ §20.1).

```bash
php artisan db:seed --class=AdminRoleSeeder --force
php artisan admin:create --email=admin@restomesto.ru --name="Администратор" --role=super_admin
```

Пароль спрашивается скрытым вводом — аргументом он не принимается, чтобы не остаться в истории
команд и в списке процессов. Двухфакторная авторизация настраивается в профиле при первом входе и
обязательна.

## Резервные копии

Ежедневный дамп базы ставится планировщиком на 03:30 — до полного обхода каталога в 04:20, чтобы
копия снималась с состояния, которое ещё не начали массово переписывать.

```bash
php artisan backup:database
```

**Диск обязательно вынести с сервера базы.** Значение по умолчанию — локальный диск; оно спасает
от ошибки оператора и не спасает от потери сервера:

```dotenv
BACKUP_DISK=s3
BACKUP_PATH=restomesto/backups
BACKUP_KEEP=14
```

Восстановление — обычным `mysql`, никакого своего формата:

```bash
mysql -u restomesto -p restomesto < restomesto-2026-08-05-033000.sql
```

Копии проверяйте восстановлением. Резервная копия, которую ни разу не разворачивали, — это не
копия, а предположение.

## Content-Security-Policy

Политика включена по умолчанию. При добавлении внешнего сервиса — CDN для картинок, счётчика,
виджета — хост добавляется настройкой, а не правкой кода:

```dotenv
CSP_EXTRA_IMG_SRC=https://cdn.restomesto.ru
```

Правки политики безопаснее выкатывать в режиме наблюдения: браузер не блокирует, но сообщает о
нарушениях.

```dotenv
CSP_REPORT_ONLY=true
CSP_REPORT_URI=https://restomesto.ru/csp-report
```

## Планировщик

```
* * * * * cd /var/www/restomesto && php artisan schedule:run >> /dev/null 2>&1
```

## Очереди

Воркер как systemd-сервис. Очереди перечисляются в порядке приоритета — задачи слева
обрабатываются раньше (`App\Support\Queues::priority()`):

```ini
[Unit]
Description=RestoMesto queue worker
After=network.target mysql.service

[Service]
Type=simple
User=www-data
Restart=always
RestartSec=3
WorkingDirectory=/var/www/restomesto
ExecStart=/usr/bin/php artisan queue:work \
    --queue=bookings,webhooks,notifications,ai,images,sync,default \
    --tries=3 --backoff=10,60,300 --max-time=3600 --sleep=1

[Install]
WantedBy=multi-user.target
```

`--max-time=3600` заставляет процесс перезапускаться раз в час: так утечки памяти в долгих
воркерах не накапливаются, а systemd поднимает процесс заново.

После деплоя воркер нужно перезапустить, чтобы он подхватил новый код:

```bash
php artisan queue:restart
```

## Обновление версии

Порядок из ТЗ §26. Миграции должны быть безопасны для больших таблиц: добавление колонок без
блокирующего `ALTER`, индексы — отдельными шагами.

```bash
php artisan down --render=errors::503 --retry=60   # если релиз ломающий

php artisan backup:database                        # см. раздел «Резервное копирование»

git pull origin main
composer install --no-dev --optimize-autoloader
npm ci && npm run build

php artisan migrate --force

php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

php artisan queue:restart                          # воркеры подхватят новый код

curl -fsS https://restomesto.ru/up                 # health check

php artisan up
```

Нулевой простой при неразрушающих изменениях достигается без `down`: миграции применяются на
работающем приложении, воркеры перезапускаются после.

## Nginx

```nginx
server {
    listen 443 ssl http2;
    server_name restomesto.ru;
    root /var/www/restomesto/public;

    index index.php;
    charset utf-8;

    add_header X-Content-Type-Options "nosniff" always;

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

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Загруженные файлы не должны исполняться (ТЗ §16.4)
    location ~* /storage/.*\.(php|phtml|phar)$ {
        deny all;
    }

    location ~ /\.(?!well-known) {
        deny all;
    }

    client_max_body_size 12M;   # лимит загрузки изображений 10 МБ + запас
}
```

## Health check

`GET /up` — стандартный health endpoint Laravel. Расширенная проверка (доступность Restoplace,
состояние очередей и планировщика) добавляется на Этапе 7 вместе с мониторингом и разделом
контроля очередей в админке.

## Резервное копирование (ТЗ §27)

Настраивается на Этапе 8. Требования:

- ежедневный бэкап базы и пользовательских изображений;
- несколько поколений;
- шифрование;
- хранение отдельно от основного сервера;
- регулярная проверка восстановления;
- документированная процедура восстановления.

## CI/CD (ТЗ §26)

Репозиторий и CI — на GitVerse. Конфигурация в `.gitverse/workflows/ci.yaml`: GitVerse читает
workflow оттуда и использует синтаксис GitHub Actions.

Задачи идут от дешёвых к дорогим — стиль и статический анализ падают за секунды и ловят
большую часть опечаток, а тесты поднимают MySQL и идут пару минут:

```
Pint  →  PHPStan  →  PHPUnit (MySQL)  →  сборка ассетов артефактом
```

Ассеты уезжают артефактом, потому что Node на сервере нет.

Локально то же самое одной командой:

```bash
composer check
```

## Локальная разработка (Laragon, Windows)

`php`, `composer` и `node` не всегда в PATH. В PowerShell:

```powershell
$env:PATH = "D:\laragon\bin\php\php-8.3.30-Win32-vs16-x64;D:\laragon\bin\composer;D:\laragon\bin\nodejs\node-v22;" + $env:PATH
```

Дальше — как в [README](../README.md).
