Операционный справочник
Этот 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
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.
Что на самом деле делает конвейер:
- CI (
ci.yml) — линт, тесты, сборка фронтенда, затем публикация образовadmin-apiиml-workerв GHCR - Deploy (
deploy.yml), только если CI прошёл — собирает nginx-образ (внутрь запекается собранный фронтенд и документация) и публикует его - Подключается по SSH к серверу и там:
docker login ghcr.io→docker pullкаждого образа →docker compose up -d - Накатывает миграции:
docker compose exec -T -e PYTHONPATH=/app admin-api alembic upgrade head - Проверяет здоровье через
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 для этих сертификатов:
Переполнение диска¶
Симптомы: 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:
- Войдите в веб-консоль VPS-провайдера (xorek.cloud) через аккаунт root
- Через консоль, от имени root, восстановите резервную конфигурацию:
- При необходимости добавьте ключ пользователя
deploy:
Восстановление после потери SSH-ключа¶
- Войдите в веб-консоль xorek.cloud
- Подключитесь к серверу от имени root через веб-консоль
- Добавьте новый публичный ключ в
/home/deploy/.ssh/authorized_keys - Проверьте права:
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
Перезапустите затронутый сервис:
Брандмауэр (UFW)¶
Открытые порты: 22 (SSH), 80 (HTTP→HTTPS редирект), 443 (HTTPS). Все остальные порты запрещены.
Доступ к 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 дней.