diff --git a/README.md b/README.md index 7d63303..c0c1682 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,10 @@ ## Деплой (Docker Compose) +Пошаговая инструкция для разворачивания на VPS — [docs/deploy.md](docs/deploy.md) +(состав стека, настройка `.env`, SSO, внешний TLS-прокси, чеклист проверки, MCP, +обновление, бэкапы, диагностика). Кратко: + Состав стека: `db` (PostgreSQL 17) + `api` (FastAPI, миграции применяются при старте контейнера) + `web` (nginx: SPA-статика, прокси `/api/`, `/auth/`, `/mcp/` на api). TLS терминирует **внешний reverse-proxy** на VPS — контейнер `web` слушает :80. diff --git a/docs/deploy.md b/docs/deploy.md new file mode 100644 index 0000000..7e11b5e --- /dev/null +++ b/docs/deploy.md @@ -0,0 +1,168 @@ +# Деплой на VPS — инструкция для агента + +Пошаговый разворот GNexus Tasks (gntodo) на сервере. Рассчитана на агента: +каждый шаг — конкретная команда + ожидаемый результат; если результат другой — +см. «Диагностика» (в конце). Требования и состав стека — ТЗ 1.3 и 4; детали +правок — README «Деплой». + +Состав стека (`docker-compose.yml`): `db` (PostgreSQL 17) → `api` (FastAPI, при +старте сам гоняет миграции alembic) → `web` (nginx: SPA-статика + прокси `/api/`, +`/auth/`, `/mcp/`). Порты наружу: только `web` на `${WEB_PORT:-8135}`. TLS — на +**внешнем reverse-proxy** VPS, контейнер за ним. + +--- + +## 0. Предпосылки (проверить до начала) + +```bash +docker --version && docker compose version # ≥ 24, плагин compose +git --version +``` + +Домен уже указывает A-записью на сервер: `dig +short <домен>`. +Внешний reverse-proxy с TLS уже работает (наш стек за ним, см. шаг 5). +Свободный порт для `WEB_PORT` (по умолчанию 8135): `ss -tln | grep 8135` — пусто. + +## 1. Код + +```bash +git clone git@git.gnexus.space:root/gnexus-tasks.git /srv/gntodo && cd /srv/gntodo +# либо обновление существующего клона: git pull --ff-only +``` + +## 2. `.env` + +```bash +cp .env.example .env +openssl rand -hex 32 # → SESSION_SECRET +``` + +Заполнить в `.env`: + +| Переменная | Что поставить | +|---|---| +| `GAUTH_BASE_URL` | `https://auth.gnexus.space` (дефолт) | +| `GAUTH_CLIENT_ID` / `GAUTH_CLIENT_SECRET` | выдать/взять в панели SSO | +| `GAUTH_REDIRECT_URI` | `https://<домен>/auth/callback` — **обязан совпасть** с зарегистрированным в SSO; из него же выводится разрешённый Host для `/mcp` | +| `SESSION_SECRET` | вывод `openssl rand -hex 32`; **не** `change-me-*` — приложение не стартует с дефолтом | +| `MCP_TOKEN` | длинная случайная строка (`openssl rand -hex 24`); пусто — `/mcp` закрыт | +| `OLLAMA_BASE_URL` | адрес внешнего сервера Ollama, например `http://192.168.1.130:11434` | +| `OLLAMA_MODEL` | `qwen3.5:2b-q4_K_M` (дефолт) | +| `POSTGRES_PASSWORD` | `openssl rand -hex 16` (не дефолт `gntodo`) | +| `WEB_PORT` | 8135 или другой свободный | + +`.env` в git не входит и не должен попадать. + +## 3. Регистрация callback в SSO (ручной шаг) + +В панели auth.gnexus.space зарегистрировать redirect URI +`https://<домен>/auth/callback` для client'а gntodo. Без этого шага логин +падает на этапе обмена кода. Если данных SSO нет — **остановиться и спросить +пользователя**. + +## 4. Запуск + +```bash +docker compose up -d --build +docker compose ps # db — healthy, api — healthy (start_period 30s: миграции), web — Up +``` + +Миграции применяются автоматически при старте `api` (CMD контейнера: +`alembic upgrade head && uvicorn …`). Логи старта: `docker compose logs api | tail -30`. + +## 5. Внешний reverse-proxy (TLS) + +Проброс `https://<домен>` → `http://127.0.0.1:${WEB_PORT}`. Пример для nginx +на хосте: + +```nginx +server { + listen 443 ssl; + server_name <домен>; + # ssl_certificate / ssl_certificate_key — по правилам хоста (certbot и т.п.) + + client_max_body_size 12m; # вложения до 10 МБ; без этого — 413 + + location / { + proxy_pass http://127.0.0.1:8135; + proxy_set_header Host $host; # КРИТИЧНО: Host не переписывать — + # иначе /mcp ответит 421 (проверка Host) + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_http_version 1.1; + proxy_set_header Connection ""; + } + # /api/ и /mcp/ — долгоживущие SSE: буферизацию отключить на внешнем + # прокси тоже, иначе реактивность и MCP-агенты «застрянут». + # Backend шлёт X-Accel-Buffering: no — nginx уважает его и так; + # на других прокси отключить явно. +} +``` + +После правки: `nginx -t && systemctl reload nginx`. + +## 6. Проверка деплоя (чеклист) + +```bash +# 1. SPA и health +curl -s -o /dev/null -w "%{http_code} %{content_type}\n" https://<домен>/ # 200 text/html +curl -s https://<домен>/api/health # {"status":"ok"} + +# 2. SSO-редирект собран (client_id, redirect_uri — из .env) +curl -s -o /dev/null -D - https://<домен>/auth/login | grep -i ^location +# → https://auth.gnexus.space/oauth/authorize?... + +# 3. MCP: без токена 401, с токеном — 200 и mcp-session-id +curl -s -o /dev/null -w "%{http_code}\n" -X POST https://<домен>/mcp/ \ + -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","method":"initialize"}' # 401 +curl -s -D - -o /dev/null -X POST https://<домен>/mcp/ \ + -H "Authorization: Bearer $MCP_TOKEN" -H 'Content-Type: application/json' \ + -H 'Accept: application/json, text/event-stream' \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"check","version":"0"}}}' \ + | grep -iE "^(HTTP|mcp-session)" # 200 + mcp-session-id + +# 4. Логин в браузере: https://<домен> → SSO → возврат в приложение +``` + +Если всё зелёное — деплой готов. Пункт 4 проверяется пользователем (нужен аккаунт SSO). + +## 7. MCP для ИИ-агентов + +Адрес сервера — `https://<домен>/mcp/`, токен — `MCP_TOKEN` из `.env`. +Агентам раздаётся не сам токен, а инструкция; страница-инструкция для людей — +`https://<домен>/mcp-help` (как подключить, список тулов). + +```bash +claude mcp add --transport http gntodo https://<домен>/mcp/ \ + --header "Authorization: Bearer " +``` + +## 8. Обновление + +```bash +git pull --ff-only +docker compose up -d --build # миграции применит сам api при старте +docker compose ps # все healthy +``` + +## 9. Бэкапы + +- БД: `docker compose exec db pg_dump -U gntodo gntodo > backup-$(date +%F).sql` + (восстановление: `cat backup.sql | docker compose exec -T db psql -U gntodo gntodo` + в свежий volume). +- Вложения: named volume `gntodo_attachments` → путь внутри api `/data/attachments`. +- Всё вместе: остановить api (`docker compose stop api`) на время дампа. + +## 10. Диагностика + +| Симптом | Причина / что смотреть | +|---|---| +| `api` не стартует, в логах про session secret | `SESSION_SECRET` не заполнен или равен дефолту | +| Логин падает после SSO | `GAUTH_REDIRECT_URI` ≠ зарегистрированному в SSO; https vs http | +| `/mcp` → 421 Misdirected Request | внешний прокси не передаёт Host (нужен `proxy_set_header Host $host`), или Host ≠ домену из `GAUTH_REDIRECT_URI` | +| Сад/списки «живут» только после F5; тосты XP не приходят | SSE буферизуется: проверь `proxy_buffering off` на `/api/` у внешнего прокси | +| Вложение >1 МБ → 413 | `client_max_body_size` на внешнем прокси (в контейнерном nginx уже 20m) | +| MCP 401 при верном токене | токен не пробрался в контейнер: `docker compose exec api printenv MCP_TOKEN` | +| Детализация задач не приходит | `docker compose logs api | grep -i detail`; доступен ли Ollama: `curl $OLLAMA_BASE_URL/api/tags` с хоста api | + +Логи: `docker compose logs -f api` (uvicorn), `docker compose logs web` (nginx). \ No newline at end of file