Пошаговый разворот 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, контейнер за ним.
docker --version && docker compose version # ≥ 24, плагин compose git --version
Домен уже указывает A-записью на сервер: dig +short <домен>. Внешний reverse-proxy с TLS уже работает (наш стек за ним, см. шаг 5). Свободный порт для WEB_PORT (по умолчанию 8135): ss -tln | grep 8135 — пусто.
git clone git@git.gnexus.space:root/gnexus-tasks.git /srv/gntodo && cd /srv/gntodo # либо обновление существующего клона: git pull --ff-only
.envcp .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 |
GAUTH_WEBHOOK_SECRET |
секрет webhook из панели SSO (п. 3.1); пусто — webhook от SSO выключен (эндпоинт отвечает 503) |
SESSION_SECRET |
вывод openssl rand -hex 32; не change-me-* — приложение не стартует с дефолтом |
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 не входит и не должен попадать.
В панели auth.gnexus.space зарегистрировать redirect URI https://<домен>/auth/callback для client'а gntodo. Без этого шага логин падает на этапе обмена кода. Если данных SSO нет — остановиться и спросить пользователя.
В той же панели настроить webhook для client'а gntodo (endpoint https://<домен>/auth/webhook), выбрать события профиля/статуса пользователя (user.email_changed, user.profile_updated, user.blocked, user.archived, user.deleted). Секрет подписи, показанный при создании (или webhook/rotate-secret), вписать в .env как GAUTH_WEBHOOK_SECRET. Проверка доставки — кнопка «test» в панели: gntodo ответит 200 (событие webhook.test подтверждается без действий). Пустой секрет — приём webhook выключен (503), на работу логина не влияет.
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.
Проброс 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.
# 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 (токены per-user, создаются в Настройках после логина)
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
# 4. Логин в браузере: https://<домен> → SSO → возврат в приложение
Если всё зелёное — деплой готов. Пункт 4 проверяется пользователем (нужен аккаунт SSO).
Адрес сервера — https://<домен>/mcp/. Токены per-user (ТЗ 3.10): каждый пользователь генерирует свой в Настройках → Токены MCP (plaintext показывается один раз, в БД — только sha256-хэш). Агент действует от имени владельца токена и видит только его данные. Страница-инструкция для людей — https://<домен>/mcp-help.
claude mcp add --transport http gntodo https://<домен>/mcp/ \ --header "Authorization: Bearer <токен из Настроек>"
Важно: переменная MCP_TOKEN из .env больше не используется — старые конфиги агентов получат 401, пока владелец не создаст токен в Настройках.
git pull --ff-only docker compose up -d --build # миграции применит сам api при старте docker compose ps # все healthy
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).gntodo_attachments → путь внутри api /data/attachments.docker compose stop api) на время дампа.| Симптом | Причина / что смотреть | |
|---|---|---|
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 logs api | grep -i mcp` |
| Детализация задач не приходит | `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).