Newer
Older
gnexus-tasks / docs / deploy.md

Деплой на 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. Предпосылки (проверить до начала)

docker --version && docker compose version   # ≥ 24, плагин compose
git --version

Домен уже указывает A-записью на сервер: dig +short <домен>. Внешний reverse-proxy с TLS уже работает (наш стек за ним, см. шаг 5). Свободный порт для WEB_PORT (по умолчанию 8135): ss -tln | grep 8135 — пусто.

1. Код

git clone git@git.gnexus.space:root/gnexus-tasks.git /srv/gntodo && cd /srv/gntodo
# либо обновление существующего клона: git pull --ff-only

2. .env

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. Запуск

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 на хосте:

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. Проверка деплоя (чеклист)

# 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 (как подключить, список тулов).

claude mcp add --transport http gntodo https://<домен>/mcp/ \
  --header "Authorization: Bearer <MCP_TOKEN>"

8. Обновление

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).