Перейти к содержанию

Операционный справочник

Этот Runbook охватывает повседневные операции, процедуры деплоя, реагирование на инциденты и восстановление для платформы TryOn SaaS.

Быстрый справочник

Сервер: 185.246.222.107
Домен: ziex-tryon.com
SSH: ssh -i ~/.ssh/id_ed25519 [email protected]
Корень приложения: /opt/tryon-saas
Резервные копии: /opt/tryon-saas-backups/


Быстрая проверка состояния

# Состояние платформы (выполняется откуда угодно)
curl https://api.ziex-tryon.com/health | python3 -m json.tool

# Строгая проверка готовности (БД + Redis)
curl -I https://api.ziex-tryon.com/readiness

# SSH на сервер и проверка всех Docker-сервисов
ssh -i ~/.ssh/id_ed25519 [email protected]
sudo docker ps

Ожидаемый ответ health:

{
  "status": "ok",
  "checks": {
    "database": "ok",
    "redis": "ok",
    "worker_heartbeat": "ok",
    "fal_api": "configured",
    "storage_backend": "local",
    "media_base_url_public": true
  }
}

HTTP 200 недостаточно

Эндпоинт /health возвращает 200 даже при деградации отдельных проверок. Всегда инспектируйте объект checks, не только HTTP-статус.


Рутинные операции

Просмотр логов

# Следить за логами admin-api
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs -f admin-api

# Следить за логами ml-worker
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs -f ml-worker

# Все сервисы, последние 100 строк
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs --tail=100

# Поиск ошибок
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs admin-api | grep '"level":"error"'

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

# Все контейнеры
sudo docker ps

# Использование ресурсов
sudo docker stats --no-stream

# Брандмауэр UFW
sudo ufw status verbose

# fail2ban (защита от брутфорса SSH)
sudo fail2ban-client status sshd

# Использование диска
df -h /
du -sh /opt/tryon-saas-backups/

Ручное резервное копирование

cd /opt/tryon-saas
bash scripts/backup.sh
ls -lh /opt/tryon-saas-backups/

Применение миграций базы данных

cd /opt/tryon-saas
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec admin-api alembic upgrade head

Миграции не запускаются автоматически

После деплоя кода, содержащего новую миграцию Alembic, необходимо вручную выполнить alembic upgrade head. create_all только создаёт таблицы для чистых установок — новые столбцы из миграций оно не добавляет.

Перезапуск одного сервиса

cd /opt/tryon-saas
docker compose -f docker-compose.yml -f docker-compose.prod.yml restart admin-api
docker compose -f docker-compose.yml -f docker-compose.prod.yml restart ml-worker

Применение изменений кода без полного деплоя

cd /opt/tryon-saas
git pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml build admin-api
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d admin-api

Деплой

Полный production-деплой (CI/CD)

Деплой запускается автоматически через GitHub Actions, когда CI проходит на main. Ничего не собирается на сервере — образы приходят из GHCR.

Что на самом деле делает конвейер:

  1. CI (ci.yml) — линт, тесты, сборка фронтенда, затем публикация образов admin-api и ml-worker в GHCR
  2. Deploy (deploy.yml), только если CI прошёл — собирает nginx-образ (внутрь запекается собранный фронтенд и документация) и публикует его
  3. Подключается по SSH к серверу и там: docker login ghcr.iodocker pull каждого образа → docker compose up -d
  4. Накатывает миграции: docker compose exec -T -e PYTHONPATH=/app admin-api alembic upgrade head
  5. Проверяет здоровье через http://localhost/health изнутри сервера (внешние проверки Cloudflare блокирует для IP раннеров CI)

Никогда не собирайте образы на сервере

Продакшн работает на образах из GHCR. Локальный docker compose build на сервере перетегирует ссылку GHCR самосбором, который следующий pull молча затрёт. А собранный таким образом фронтенд вообще не попадёт в контейнер: в docker-compose.prod.yml стоит volumes: !override, монтируются только ssl и uploads. Статика живёт внутри nginx-образа.

scripts/deploy.sh устарел — не запускайте его

Он написан до перехода на GHCR (май 2026). Собирает образы на сервере, не накатывает миграции и компилирует фронтенд в каталог, который никуда не монтируется. Его запуск означает новый код на непромигрированной базе. Деплойте через CI.

Ручной production-деплой

Обычно не нужен — достаточно запушить в main. Если требуется задеплоить текущий main без нового коммита, перезапустите workflow Deploy to Production из интерфейса GitHub Actions.

Ручной деплой на сервере (только для восстановительных ситуаций):

ssh deploy@<server>
cd /opt/tryon-saas
git pull

COMPOSE="docker compose -f docker-compose.yml -f docker-compose.prod.yml"

# Образы берутся из реестра, никогда из локальных исходников
echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_ACTOR" --password-stdin
docker pull "$ADMIN_API_IMAGE" && docker pull "$ML_WORKER_IMAGE" && docker pull "$NGINX_IMAGE"

$COMPOSE up -d

# Миграции сами не применяются
$COMPOSE exec -T -e PYTHONPATH=/app admin-api alembic upgrade head

curl -sfkL http://localhost/health

DOMAIN должен быть задан в .env

docker-compose.prod.yml использует ${DOMAIN} для MEDIA_BASE_URL. Если DOMAIN не задан, URL становится https:///uploads и доставка изображений ломается.

Первый деплой (чистый сервер)

Чистый VPS требует подготовки, без которой деплой не пройдёт. Запустите скрипт bootstrap от root — он ставит Docker с Compose v2, создаёт пользователя deploy, ужесточает SSH, настраивает UFW и fail2ban, добавляет swap, готовит /opt/tryon-saas и в конце перечисляет всё, чего ещё не хватает. Скрипт идемпотентный: его можно перезапускать для проверки.

scp scripts/bootstrap-server.sh root@<server>:/tmp/
ssh root@<server> "REPO_URL=<repo-url> bash /tmp/bootstrap-server.sh"

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

ssh deploy@<server>

git clone <repo-url> /opt/tryon-saas
cd /opt/tryon-saas

# TLS: Cloudflare Origin Certificate (см. ssl/README.md)
# ssl/fullchain.pem ← origin.crt
# ssl/privkey.pem   ← origin.key
chmod 644 ssl/fullchain.pem
chmod 600 ssl/privkey.pem

bash scripts/generate-secrets.sh
cp .env.example .env
# Заполнить: FAL_API_KEY, S3_*, FIRST_ADMIN_EMAIL, FIRST_ADMIN_PASSWORD, DOMAIN

# Первый запуск должен поднять весь стек целиком — workflow деплоя рассчитан
# на уже существующие контейнеры и обновляет только отдельные сервисы
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
  exec -T -e PYTHONPATH=/app admin-api alembic upgrade head

После этого переведите секреты GitHub SERVER_HOST, SERVER_USER и SSH_PRIVATE_KEY на новую машину и переключите DNS-записи Cloudflare на её IP.


Реагирование на инциденты

Воркер недоступен

Симптомы: Задачи зависают в статусе pending. /health показывает "worker_heartbeat": "stale" или "missing". Срабатывает Telegram-алерт.

# Проверить логи воркера
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs --tail=50 ml-worker

# Проверить, работает ли контейнер
sudo docker ps | grep ml-worker

# Перезапустить воркер
docker compose -f docker-compose.yml -f docker-compose.prod.yml restart ml-worker

# Убедиться, что heartbeat восстановился (проверить в течение 90 секунд)
curl https://api.ziex-tryon.com/health | python3 -m json.tool

Автовосстановление зависших задач

admin-api запускает фоновый sweep каждые 60 секунд. Задачи, застрявшие в статусе processing более 15 минут, автоматически переводятся в failed. Задачи в pending более 30 минут — тоже. Клиентам следует повторить упавшие задачи.

Высокий процент ошибок задач

Симптомы: Срабатывает Telegram-алерт. Grafana показывает рост tryon_completed_total{status="failed"}.

# Проверить последние ошибки в логах воркера
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs ml-worker | grep '"level":"error"' | tail -20

# Возможные причины:
# 1. API-ключ fal.ai невалиден или превышен rate limit
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs ml-worker | grep "fal_api_key"

# 2. Сбой сервиса fal.ai
#    Сначала проверить их статус-страницу: https://status.fal.ai
curl https://api.ziex-tryon.com/health | python3 -m json.tool | grep fal_api

# 3. URL изображения недоступен для fal.ai (проблема MEDIA_BASE_URL)
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs admin-api | grep "media_base_url_localhost"

Устранение: - Проблема с ключом fal.ai → обновить FAL_API_KEY в .env, пересобрать и перезапустить ml-worker - Сбой fal.ai → подождать; задачи будут повторены при перезапуске воркера - Проблема MEDIA_BASE_URL → убедиться, что MEDIA_BASE_URL в .env задан как публично доступный HTTPS URL

API недоступен (admin-api не отвечает)

Симптомы: curl https://api.ziex-tryon.com/health не работает или возвращает 502.

# Проверить контейнер admin-api
sudo docker ps | grep admin-api
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs --tail=30 admin-api

# Проверить nginx
sudo docker ps | grep nginx
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs --tail=20 nginx

# Перезапустить admin-api
docker compose -f docker-compose.yml -f docker-compose.prod.yml restart admin-api

# Если 502 сохраняется, проверить доступность admin-api из nginx
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec nginx wget -qO- http://admin-api:8000/health

Частые причины сбоя запуска admin-api

  • Отсутствует обязательная переменная окружения (например, JWT_SECRET_KEY слишком короткий) → проверить логи на событие startup_failed
  • БД недоступна при запуске → сначала проверить контейнер postgres
  • Конфликт портов → проверить, не занят ли порт 8000 другим процессом

Проблемы с базой данных

Симптомы: /readiness возвращает 503. /health показывает "database": "connection_failed".

# Проверить контейнер postgres
sudo docker ps | grep postgres
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs --tail=30 postgres

# Перезапустить postgres (данные хранятся на именованном Docker-томе, перезапуск безопасен)
docker compose -f docker-compose.yml -f docker-compose.prod.yml restart postgres

# Подождать ~10 секунд, затем проверить
curl https://api.ziex-tryon.com/readiness

# Подключиться напрямую для диагностики
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec postgres psql -U postgres tryon_saas -c "\l"

Никогда не удаляйте volume postgres

База данных хранится в именованном Docker-томе. Команда docker-compose down -v уничтожит все данные. Всегда используйте docker-compose down без флага -v.

Проблемы с Redis

Симптомы: /readiness возвращает 503. /health показывает "redis": "connection_failed". Перестаёт работать rate limiting и постановка задач в очередь.

# Проверить контейнер redis
sudo docker ps | grep redis
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs --tail=20 redis

# Перезапустить redis (AOF-персистентность воспроизведётся при перезапуске — займёт несколько секунд)
docker compose -f docker-compose.yml -f docker-compose.prod.yml restart redis

# Проверить
curl https://api.ziex-tryon.com/readiness

AOF-персистентность Redis

Redis настроен с AOF (append-only file). После перезапуска Redis воспроизводит AOF для восстановления состояния очереди. Это занимает несколько секунд и безопасно. Задачи, находившиеся в очереди до перезапуска, будут доступны снова.

Проблемы с TLS/SSL

Симптомы: HTTPS возвращает ошибку сертификата. Cloudflare показывает ошибку SSL handshake.

# Проверить логи nginx на TLS-ошибки
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs nginx | grep -i "ssl\|tls\|cert"

# Убедиться, что файлы сертификатов существуют и доступны
ls -la /opt/tryon-saas/ssl/
# Ожидается: fullchain.pem (644) и privkey.pem (600)

# Проверить срок действия сертификата
openssl x509 -in /opt/tryon-saas/ssl/fullchain.pem -noout -dates

Cloudflare Origin Certificates

Платформа использует Cloudflare Origin Certificates (не Let's Encrypt). Это 15-летние сертификаты — срок истечения не является рутинной проблемой. Однако ssl_stapling должен быть выключен в nginx.prod.conf для этих сертификатов:

ssl_stapling        off;
ssl_stapling_verify off;
OCSP stapling не работает с origin-сертификатами Cloudflare и вызовет ошибку запуска nginx, если включён.

Переполнение диска

Симптомы: admin-api не может сохранять загруженные изображения. Логи показывают OSError: No space left on device.

# Проверить использование диска
df -h /
du -sh /opt/tryon-saas-backups/
du -sh /opt/tryon-saas/uploads/
du -sh /var/lib/docker/

# Удалить старые резервные копии (хранение 30 дней — автоматизировано, но можно вручную)
ls -lht /opt/tryon-saas-backups/ | tail -n +31 | awk '{print $NF}' | xargs -I{} rm /opt/tryon-saas-backups/{}

# Запустить очистку медиафайлов (удаляет WebP-файлы старше UPLOAD_RETENTION_HOURS без активных задач)
# Сервис очистки запускается автоматически по интервалу — перезапуск admin-api ускорит выполнение
docker compose -f docker-compose.yml -f docker-compose.prod.yml restart admin-api

# Очистить неиспользуемые Docker-слои
sudo docker system prune -f

Высокое потребление памяти

Симптомы: OOM kill в логах контейнера. sudo docker stats показывает сервис у предела памяти.

# Проверить использование памяти по контейнерам
sudo docker stats --no-stream

# Проверить OOM kills в журнале ядра
sudo dmesg | grep -i "oom\|kill" | tail -10

# Перезапустить затронутый сервис
docker compose -f docker-compose.yml -f docker-compose.prod.yml restart admin-api

Ограничения ресурсов

Все сервисы имеют ограничения памяти в docker-compose.yml. Если сервис стабильно достигает своего лимита, проанализируйте паттерн использования памяти перед увеличением ограничения.


Процедуры восстановления

Восстановление базы данных из резервной копии

# Список доступных резервных копий
ls -lh /opt/tryon-saas-backups/

# Восстановление из конкретного дампа (БД будет УДАЛЕНА и пересоздана)
bash /opt/tryon-saas/scripts/restore.sh /opt/tryon-saas-backups/<dump-file>.sql.gz --drop-existing --yes

# Проверить восстановление
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec postgres psql -U postgres tryon_saas -c "SELECT count(*) FROM clients;"

Восстановление удаляет существующие данные

--drop-existing удаляет текущую базу данных перед восстановлением. Всегда проверяйте правильность выбранного файла резервной копии перед использованием этого флага.

Восстановление доступа администратора (сброс пароля)

Если пароль администратора утерян и нет других способов восстановления:

# Сгенерировать новый bcrypt-хэш (использовать Python на сервере)
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec admin-api python3 -c "
import bcrypt
pw = b'YourNewPassword123!'
print(bcrypt.hashpw(pw, bcrypt.gensalt(rounds=12)).decode())
"

# Обновить пароль в базе данных
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec postgres psql -U postgres tryon_saas -c \
  "UPDATE users SET password_hash='\$2b\$12\$...' WHERE email='[email protected]';"

Эндпоинт forgot-password не реализован

Эндпоинт /auth/forgot-password отсутствует. Восстановление пароля администратора требует прямого доступа к БД, как показано выше. Самообслуживание через email запланировано для Фазы 2 (требует email-провайдера).

Исправление сломанной конфигурации nginx

# Проверить конфигурацию nginx перед применением
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec nginx nginx -t

# Если конфигурация сломана и nginx не запускается, восстановить из git
cd /opt/tryon-saas
git diff nginx/nginx.prod.conf   # посмотреть что изменилось
git checkout nginx/nginx.prod.conf  # откатить изменения
docker compose -f docker-compose.yml -f docker-compose.prod.yml restart nginx

Исправление сломанной конфигурации sshd

Если вы заблокированы из SSH из-за неверного sshd_config:

  1. Войдите в веб-консоль VPS-провайдера (xorek.cloud) через аккаунт root
  2. Через консоль, от имени root, восстановите резервную конфигурацию:
    cp /etc/ssh/sshd_config.backup.20260520 /etc/ssh/sshd_config
    systemctl reload ssh
    
  3. При необходимости добавьте ключ пользователя deploy:
    cat /home/deploy/.ssh/authorized_keys
    # Добавить публичный ключ при отсутствии
    

Восстановление после потери SSH-ключа

  1. Войдите в веб-консоль xorek.cloud
  2. Подключитесь к серверу от имени root через веб-консоль
  3. Добавьте новый публичный ключ в /home/deploy/.ssh/authorized_keys
  4. Проверьте права: chmod 700 /home/deploy/.ssh && chmod 600 /home/deploy/.ssh/authorized_keys

Справочник

Переменные окружения (критические)

Переменная Назначение Где изменить
JWT_SECRET_KEY Подпись всех JWT /opt/tryon-saas/.env
FAL_API_KEY Инференс fal.ai /opt/tryon-saas/.env
POSTGRES_PASSWORD Аутентификация БД /opt/tryon-saas/.env
DOMAIN Префикс MEDIA_BASE_URL /opt/tryon-saas/.env
MEDIA_BASE_URL Публичный URL загрузок, отправляемый в fal.ai /opt/tryon-saas/.env
UPLOAD_RETENTION_HOURS Срок хранения изображений /opt/tryon-saas/.env
ENABLE_DOCS Отключить /docs в production /opt/tryon-saas/.env

После изменения .env

Перезапустите затронутый сервис:

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --force-recreate admin-api

Брандмауэр (UFW)

Открытые порты: 22 (SSH), 80 (HTTP→HTTPS редирект), 443 (HTTPS). Все остальные порты запрещены.

sudo ufw status verbose

Доступ к Grafana (порт 3000 закрыт снаружи)

В продакшне Grafana слушает только localhost — попасть в неё можно через SSH-туннель:

ssh -L 3000:localhost:3000 deploy@<server>
# затем открыть http://localhost:3000
# учётные данные: admin / $GRAFANA_ADMIN_PASSWORD из .env

Команды Docker Compose

# В production всегда использовать оба compose-файла
COMPOSE="docker compose -f docker-compose.yml -f docker-compose.prod.yml"

$COMPOSE ps                          # статус сервисов
$COMPOSE logs -f admin-api           # следить за логами
$COMPOSE restart admin-api           # перезапустить один сервис
$COMPOSE up -d                       # запустить/обновить все
$COMPOSE exec admin-api bash         # консоль внутри контейнера
$COMPOSE exec postgres psql -U postgres tryon_saas  # консоль БД

Полезные однострочники

# Количество задач по статусам
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec postgres \
  psql -U postgres tryon_saas -c "SELECT status, count(*) FROM jobs GROUP BY status;"

# Количество активных клиентов
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec postgres \
  psql -U postgres tryon_saas -c "SELECT count(*) FROM clients WHERE status='active';"

# Глубина очереди Redis
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec redis \
  redis-cli LLEN tryon_jobs

# Возраст heartbeat воркера (секунды с последнего обновления)
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec redis \
  redis-cli TTL worker_heartbeat

# Следить за структурированными логами с форматированием JSON
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs -f admin-api | python3 -c "
import sys, json
for line in sys.stdin:
    try:
        print(json.dumps(json.loads(line.strip()), indent=2))
    except:
        print(line, end='')
"

Crontab для резервных копий

Для настройки автоматического ежедневного резервного копирования (если ещё не настроено):

crontab -e
# Добавить:
0 3 * * * cd /opt/tryon-saas && bash scripts/backup.sh >> /var/log/tryon-backup.log 2>&1

Резервные копии хранятся в /opt/tryon-saas-backups/ в виде файлов .sql.gz с временной меткой и manifest.json. Скрипт backup.sh автоматически удаляет резервные копии старше 30 дней.