Skip to content

Partizap Runbook

Серверы

AliasIPSSHРоль
main85.239.48.136ssh serpens@192.168.0.4 (из VPS 2)Production: partizap.ru, admin.partizap.ru
dev192.168.0.5localhost (мы на VPS 2)Development: dev.partizap.ru, dev-admin.partizap.ru, GitLab, YouTrack

Сервисы

main (85.239.48.136)

СервисТипПортУправление
nginxsystemd80, 443systemctl reload nginx (v1.28.3, nginx.org)
Nuxt (prod)docker3001docker restart partizap-frontend-prod
PHP-FPM (prod)systemdsocksystemctl restart php8.3-fpm
Centrifugo (prod)docker8002docker restart partizap-centrifugo-prod
Admin panel (prod)PHP-FPMphp8.3-fpm-prod.sock/var/www/partizap/admin-production (mTLS)

dev (192.168.0.5)

СервисТипПортУправление
nginxsystemd80systemctl reload nginx
Nuxt (dev)docker3000см. ниже
Nuxt (prod)docker3001docker restart partizap-frontend-prod
PHP-FPM (dev)systemdsocksystemctl restart php8.3-fpm
Centrifugo (dev)dockerdocker restart partizap-centrifugo-dev
Admin panel (dev)PHP-FPM8084 (nginx)/var/www/partizap/admin-development
Queue Worker (dev)systemdsystemctl restart partizap-worker-dev
GitLabomnibus8081gitlab-ctl restart
YouTrackdocker8080docker restart youtrack

Queue Worker (systemd)

Воркер запускается через systemd unit partizap-worker-dev (dev) / partizap-worker-prod (prod).

Важно: unit-файл НЕ должен содержать Environment=APP_ENV=.... Все переменные окружения загружаются из .env через Dotenv в config/app.php. Если systemd задаёт APP_ENV через Environment=, то Dotenv::createImmutable() видит её в getenv() и не записывает в $_ENV, из-за чего приложение падает с пустым $_ENV['APP_ENV'].


502 Bad Gateway: диагностика и восстановление

Быстрая диагностика

bash
# 1. Определить какой сайт отдает 502
curl -sI https://partizap.ru        # production
curl -sI https://dev.partizap.ru    # development (через basic auth)

# 2. Проверить nginx
ssh root@85.239.48.136 "nginx -t && systemctl status nginx"
ssh -J root@85.239.48.136 root@192.168.0.5 "nginx -t && systemctl status nginx"

# 3. Посмотреть логи nginx (последние ошибки покажут какой upstream упал)
ssh root@85.239.48.136 "tail -20 /var/log/nginx/error.log"
ssh -J root@85.239.48.136 root@192.168.0.5 "tail -20 /var/log/partizap/development/nginx-error.log"

partizap.ru — 502

Причина: один из upstream-сервисов на main не отвечает.

Nuxt (порт 3001):

bash
ssh root@85.239.48.136
curl -sI http://127.0.0.1:3001          # проверить
docker ps | grep front                   # статус контейнера
docker restart partizap-frontend-prod    # перезапуск
docker logs partizap-frontend-prod --tail 50  # логи

PHP-FPM API (sock):

bash
ssh root@85.239.48.136
systemctl status php8.3-fpm              # статус
systemctl restart php8.3-fpm             # перезапуск
ls -la /run/php/php8.3-fpm-prod.sock     # проверить сокет

Centrifugo WebSocket (порт 8002):

bash
ssh root@85.239.48.136
docker ps | grep centrifugo
docker restart partizap-centrifugo-prod

dev.partizap.ru — 502

Трафик идет: клиент → main (SSL + proxy) → dev (HTTP). 502 может быть на любом из двух серверов.

Шаг 1: проверить что VPS 2 отвечает (с VPS 1):

bash
ssh root@85.239.48.136 "curl -sI http://192.168.0.5:80 -H 'Host: dev.partizap.ru'"

Если 502 — проблема на VPS 2. Если connection refused — nginx на VPS 2 не работает.

Шаг 2: на VPS 2 проверить upstream-сервисы:

bash
ssh -J root@85.239.48.136 root@192.168.0.5

# Nuxt dev (порт 3000) — самая частая причина
curl -sI http://127.0.0.1:3000
docker ps | grep partizap-frontend-dev

# Если контейнер не запущен:
cd /home/gitlab-runner/builds/SfDoCZhxV/0/root/partizap-frontend
docker compose -f docker-compose.dev.yml up -d

# Если контейнер есть, но не отвечает:
docker restart partizap-frontend-dev
docker logs partizap-frontend-dev --tail 50

# PHP-FPM dev
systemctl status php8.3-fpm
systemctl restart php8.3-fpm

gitlab.partizap.ru — 502

bash
ssh -J root@85.239.48.136 root@192.168.0.5
gitlab-ctl status           # статус всех компонентов
gitlab-ctl restart          # полный перезапуск

track.partizap.ru — 502

bash
ssh -J root@85.239.48.136 root@192.168.0.5
docker ps | grep youtrack
docker restart youtrack

Деплой бэкенда

Единый скрипт deploy/deploy.sh деплоит оба приложения (marketplace + admin) за один запуск. Оператору не нужно деплоить их отдельно — это невозможно забыть.

Как запускается

СредаТриггерСкриптОткуда выполняется
developПуш в developdeploy/deploy.sh developmentАвтоматически (GitLab CI), на VPS 2
productionПуш в maindeploy/deploy.sh productionВручную (кнопка в GitLab CI), VPS 2 → SSH на VPS 1

Что делает скрипт

Порядок выполнения для каждого окружения:

1. Marketplace  (/var/www/partizap/{env})
   ├── git fetch + reset --hard origin/{branch}
   ├── composer install
   ├── rm -rf var/cache/*  (DI container)
   ├── migrations:migrate  (только marketplace, от partizap_owner)
   ├── grants/admin.sql    (GRANT привилегий для admin DB role)
   └── regenerate Doctrine proxies + clear metadata cache

2. Admin  (/var/www/partizap/admin-{env})
   ├── git fetch + reset --hard origin/{branch}
   ├── composer install
   ├── rm -rf var/cache/*
   └── regenerate Doctrine proxies + clear metadata cache

3. Shared runtime refresh  (если хотя бы одно приложение обновилось)
   ├── OPcache reset (cachetool)
   ├── systemctl restart partizap-worker-{env}  (только если marketplace изменился)
   └── systemctl reload php8.3-fpm

4. Health check
   └── curl https://{domain}/api/health  (только marketplace, admin за mTLS)

Миграции и гранты

Миграции запускаются только из marketplace. Admin-приложение использует DB role partizap_admin_{env}, у которой нет DDL-привилегий. grants/admin.sql применяется после миграций, чтобы новые таблицы получили нужные GRANT.

Деплой на develop

Автоматический — при пуше в develop CI запускает deploy-dev job.

bash
# Ручной деплой (если нужно без CI):
cd /var/www/partizap/development && sudo -u serpens git pull
cd /var/www/partizap/admin-development && sudo -u serpens git pull
sudo systemctl reload php8.3-fpm

Деплой на production

  1. Создать MR из developmain в GitLab
  2. Смержить MR
  3. В GitLab CI → pipeline на main → нажать кнопку deploy-prod (manual job)
  4. Скрипт выполнится по SSH на VPS 1 (serpens@192.168.0.4)
bash
# Ручной деплой (если CI недоступен):
ssh serpens@192.168.0.4
cd /var/www/partizap/production && git fetch origin && git reset --hard origin/main
composer install --no-dev --optimize-autoloader --no-interaction
rm -rf var/cache/*
php bin/console migrations:migrate --no-interaction
cd /var/www/partizap/admin-production && git fetch origin && git reset --hard origin/main
composer install --no-dev --optimize-autoloader --no-interaction
rm -rf var/cache/*
sudo systemctl reload php8.3-fpm
curl -sI https://partizap.ru/api/health  # должен быть 200

Сброс кэша после деплоя

deploy.sh сбрасывает DI container (var/cache/), Doctrine metadata (Redis) и proxies автоматически. Дополнительные действия нужны только при изменении справочных данных:

bash
# Сброс кэша справочников (категории, авто, гео) — Redis DB 1
redis-cli -n 1 KEYS "prod:*" | xargs -r redis-cli -n 1 DEL   # production
redis-cli -n 1 KEYS "dev:*"  | xargs -r redis-cli -n 1 DEL   # development

Откат

Скрипт делает git reset --hard origin/{branch} — откат = force-push предыдущего коммита в main/develop и повторный запуск деплоя. Миграции с down() можно откатить вручную:

bash
php bin/console migrations:migrate prev --no-interaction

Пермишены var/

var/ должен быть настроен один раз per environment. Deploy-скрипт работает от serpens, PHP-FPM от www-data:

bash
sudo chown -R serpens:www-data /var/www/partizap/{env}/var
sudo chmod -R u+rwX,g+rwX /var/www/partizap/{env}/var
sudo find /var/www/partizap/{env}/var -type d -exec chmod g+s {} \;

g+s (setgid) на директориях гарантирует, что файлы созданные www-data наследуют группу www-data и доступны serpens через group.


SSR: пустые данные на главной странице

Симптомы

  • Категории и объявления на главной пусты при холодной загрузке (F5, очистка кэша)
  • При клиентской навигации (SPA-переход) данные появляются
  • В Network-табе нет запросов к /api/store/categories и /api/store/productsuseAsyncData не перезапрашивает на клиенте, т.к. считает данные уже hydrated

Причина

Nitro devProxy не перехватывает внутренние $fetch-вызовы во время SSR. Когда Nuxt рендерит страницу на сервере, $fetch('/api/...') делает internal call к Nitro H3, а не HTTP-запрос через proxy. Nitro не находит handler → 404.

На клиенте запрос идёт через браузер → Nginx → API, поэтому работает.

Диагностика

bash
# Проверить SSR-ответ — есть ли данные в HTML:
curl -s http://localhost:3000/ | grep -oE 'home-category-[0-9]+'
# Если пусто — SSR не получает данные

# Проверить Nuxt payload на наличие NuxtError:
curl -s http://localhost:3000/ | grep -oE 'NuxtError'
# Если есть — SSR-запросы к API падают

Решение

Для локальной разработки обязательно задать NUXT_API_BASE_SERVER в .env — абсолютный URL бэкенда для SSR:

dotenv
# .env (local dev)
NUXT_API_BASE_SERVER=https://dev.partizap.ru/api
NUXT_DEV_BASIC_AUTH=Basic <credentials>

На dev/prod серверах эти переменные задаются в docker-compose.*.ymlenvironment.

Как это работает

# Клиент (браузер): /api → Nginx devProxy → API ✓
# SSR (Nitro $fetch): /api → internal H3 call → нет handler → 404 ✗
# SSR с NUXT_API_BASE_SERVER: https://dev.partizap.ru/api → прямой HTTP → API ✓

Логика выбора baseURL (shared/api/client.ts):

typescript
const baseURL = import.meta.server
  ? (config.apiBaseServer || config.public.apiBase)  // SSR: абсолютный URL
  : config.public.apiBase                            // Client: /api (через proxy)