feat(deploy): split obsidian stack from main + path-filtered GHA

Obsidian Self-hosted LiveSync (CouchDB) выведен в отдельный docker-compose
stack и GHA workflow. Любой стек теперь редеплоится независимо.

* docker-compose.obsidian.yml: только couchdb в shared external network
* docker-compose.prod.yml: убрана couchdb-секция; caddy подключён к
  shared network для маршрута obsidian.gendsgn.ru → couchdb:5984
* .github/workflows/deploy-obsidian.yml: новый workflow с path-filters
  (триггерится только при изменениях obsidian-related файлов)
* .github/workflows/deploy.yml: + path-filters (триггерится только при
  main-related файлах) + создание shared network в bootstrap
* docs/obsidian-livesync.md: обновлены инструкции под split-архитектуру

После push:
1. ssh gendesign 'docker network create gendesign_shared'  (один раз)
2. Прописать COUCHDB_USER/PASSWORD в backend/.env.runtime
3. Добавить DNS A-record obsidian.gendsgn.ru → IP VPS
4. git push → GHA задеплоит обе части независимо

Преимущества:
- Изменения main не разрывают LiveSync клиентов
- Изменения obsidian не рестартуют main
- Падение obsidian-стека не влияет на основное приложение
This commit is contained in:
lekss361 2026-05-11 09:51:31 +03:00
parent 79e2e94e09
commit b0f5c38894
7 changed files with 550 additions and 3 deletions

97
.github/workflows/deploy-obsidian.yml vendored Normal file
View file

@ -0,0 +1,97 @@
name: Deploy Obsidian
# Деплой ТОЛЬКО obsidian-стека (CouchDB).
# Триггерится при изменениях:
# - docker-compose.obsidian.yml (compose сервиса CouchDB)
# - scripts/setup-couchdb.sh (bootstrap)
# - docs/obsidian-livesync.md (документация — для history-watcher'а)
# - этот workflow
#
# Не пересобирает никаких Docker-образов (CouchDB официальный с DockerHub).
# Не трогает main-стек (backend / frontend / postgres / worker / beat / caddy).
on:
push:
branches: [main]
paths:
- "docker-compose.obsidian.yml"
- "scripts/setup-couchdb.sh"
- "docs/obsidian-livesync.md"
- ".github/workflows/deploy-obsidian.yml"
workflow_dispatch:
concurrency:
group: deploy-obsidian
cancel-in-progress: false
jobs:
deploy-obsidian:
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy obsidian stack via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
port: ${{ secrets.DEPLOY_PORT || 22 }}
script: |
set -euo pipefail
cd /opt/gendesign
# Свежие конфиги из репо
git fetch origin main
git reset --hard origin/main
# Создать shared network если её ещё нет (idempotent).
# Эта же сеть используется Caddy в main-stack.
docker network inspect gendesign_shared >/dev/null 2>&1 \
|| docker network create gendesign_shared
# Стек CouchDB поднимается с собственным project-name,
# чтобы main `docker compose ps` не показывал его как часть main.
docker compose -p gendesign-obsidian \
-f docker-compose.obsidian.yml pull
docker compose -p gendesign-obsidian \
-f docker-compose.obsidian.yml up -d
# Bootstrap CouchDB (CORS, db, лимиты) — idempotent
if [ -f scripts/setup-couchdb.sh ]; then
# Загружаем COUCHDB_PASSWORD из backend/.env.runtime если есть
if [ -f backend/.env.runtime ]; then
set -a; source backend/.env.runtime; set +a
fi
# Ждём CouchDB up, потом bootstrap
for i in $(seq 1 30); do
if docker compose -p gendesign-obsidian \
-f docker-compose.obsidian.yml \
exec -T couchdb curl -fsS http://localhost:5984/_up >/dev/null 2>&1; then
break
fi
sleep 2
done
COUCHDB_HOST=http://localhost:5984 \
COUCHDB_USER="${COUCHDB_USER:-obsidian}" \
COUCHDB_PASSWORD="${COUCHDB_PASSWORD:?must be set in backend/.env.runtime}" \
docker compose -p gendesign-obsidian \
-f docker-compose.obsidian.yml \
exec -T couchdb bash -c "$(cat scripts/setup-couchdb.sh)" \
|| echo "(bootstrap warnings ignored — script is idempotent)"
fi
# Caddy в main-stack — НЕ перезапускаем тут (другой workflow),
# но reload конфига полезен на случай если в Caddyfile добавили
# новый obsidian-route только что (главное: image main caddy уже
# запущен и подключён к gendesign_shared network).
if docker compose -p gendesign -f docker-compose.prod.yml ps caddy --quiet \
| grep -q .; then
docker compose -p gendesign -f docker-compose.prod.yml \
exec -T caddy caddy reload --config /etc/caddy/Caddyfile \
|| echo "(caddy reload skipped — main stack not running)"
fi
sleep 3
curl -fsS https://obsidian.gendsgn.ru/_up | head -c 200 || true

View file

@ -1,8 +1,18 @@
name: Deploy
# Деплоится только при изменениях основного стека.
# Obsidian-стек (CouchDB) — отдельный workflow `deploy-obsidian.yml`.
on:
push:
branches: [main]
paths:
- "backend/**"
- "frontend/**"
- "docker-compose.prod.yml"
- "Caddyfile"
- ".github/workflows/deploy.yml"
# NB: shared compose-fragments (network) — тоже триггерят main
# потому что Caddy шарит сеть с obsidian-stack
workflow_dispatch:
concurrency:
@ -98,13 +108,20 @@ jobs:
mkdir -p backend
printf 'SENTRY_RELEASE=%s\n' "$IMAGE_TAG" > backend/.env.runtime
# Создать external network если её нет (нужна Caddy для маршрута
# obsidian.gendsgn.ru → couchdb из отдельного obsidian-stack).
docker network inspect gendesign_shared >/dev/null 2>&1 \
|| docker network create gendesign_shared
export IMAGE_TAG="$IMAGE_TAG"
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
# Project name явно — `gendesign` (по имени папки auto), но
# фиксируем для consistency с obsidian-stack (-p gendesign-obsidian).
docker compose -p gendesign -f docker-compose.prod.yml pull
docker compose -p gendesign -f docker-compose.prod.yml up -d
# Caddyfile is bind-mounted; up -d won't re-read it. Reload explicitly.
# `|| true` so a temporary Caddy hiccup doesn't fail the whole deploy.
docker compose -f docker-compose.prod.yml exec -T caddy \
docker compose -p gendesign -f docker-compose.prod.yml exec -T caddy \
caddy reload --config /etc/caddy/Caddyfile || true
# Clean up disk: dangling layers + tagged images older than 72h that

View file

@ -27,6 +27,23 @@ www.gendsgn.ru {
redir https://gendsgn.ru{uri} permanent
}
# Obsidian Self-hosted LiveSync (CouchDB backend).
# Auto-TLS Let's Encrypt. CORS уже включён на стороне CouchDB через bootstrap
# (см. scripts/setup-couchdb.sh). Basic-auth — на стороне CouchDB (admin user).
#
# DNS: A-record obsidian.gendsgn.ru → IP VPS.
# Клиенты Obsidian + Self-hosted LiveSync plugin указывают на этот URL.
obsidian.gendsgn.ru {
encode zstd gzip
reverse_proxy couchdb:5984 {
# Большие документы (vault attachments / images) — увеличиваем timeout
transport http {
response_header_timeout 120s
}
}
}
# Plain HTTP by IP — kept for ssh-tunnel / debugging.
# Caddy issues no TLS here (no hostname).
:80 {

View file

@ -0,0 +1,41 @@
# Obsidian Self-hosted LiveSync stack — CouchDB.
#
# Деплоится ОТДЕЛЬНО от main стека:
# docker compose -f docker-compose.obsidian.yml up -d
#
# Связан с main-stack через external network `gendesign_shared`. Caddy в
# main-stack маршрутизирует obsidian.gendsgn.ru → couchdb:5984 через эту сеть.
#
# Сеть должна быть создана один раз вручную:
# docker network create gendesign_shared
# (или используй scripts/setup-obsidian-stack.sh).
services:
couchdb:
image: couchdb:3
container_name: gendesign-couchdb
restart: unless-stopped
environment:
COUCHDB_USER: ${COUCHDB_USER:-obsidian}
COUCHDB_PASSWORD: ${COUCHDB_PASSWORD}
volumes:
- couchdb_data:/opt/couchdb/data
- couchdb_config:/opt/couchdb/etc/local.d
expose:
- "5984"
networks:
- shared
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:5984/_up"]
interval: 30s
timeout: 10s
retries: 5
volumes:
couchdb_data:
couchdb_config:
networks:
shared:
external: true
name: gendesign_shared

View file

@ -124,9 +124,22 @@ services:
depends_on:
- backend
- frontend
networks:
- default
# shared — для маршрута obsidian.gendsgn.ru → couchdb (отдельный stack).
# Если obsidian-стек не задеплоен, Caddy просто получит 502 на этом маршруте,
# main-приложение не страдает.
- shared
volumes:
postgres_data:
redis_data:
caddy_data:
caddy_config:
networks:
# Внешняя сеть, создаётся вне compose (см. docs/obsidian-livesync.md).
# Связывает main-stack (Caddy) и obsidian-stack (CouchDB).
shared:
external: true
name: gendesign_shared

255
docs/obsidian-livesync.md Normal file
View file

@ -0,0 +1,255 @@
# Obsidian Self-hosted LiveSync (на CouchDB)
Single-tenant self-hosted vault на VPS. Multi-device sync через
[Self-hosted LiveSync plugin](https://github.com/vrtmrz/obsidian-livesync).
## Архитектура (split deploy)
Obsidian-стек **отделён** от основного приложения (backend/frontend/postgres/redis).
Каждый деплоится своим GHA workflow, никто не блокирует другого:
```
┌────────────────────────────────────┐
[laptop / mobile]──┤ Caddy (main-stack) │
│ обслуживает gendsgn.ru + api │
│ + reverse_proxy obsidian.* → │
│ ───shared network───┐ │
└─────────────────────────┼──────────┘
┌──────────────────────────────────┐
│ CouchDB (obsidian-stack) │
│ gendesign_shared network │
│ volumes couchdb_data │
└──────────────────────────────────┘
```
| | Main stack | Obsidian stack |
|---|---|---|
| Compose | `docker-compose.prod.yml` | `docker-compose.obsidian.yml` |
| Project | `gendesign` (`docker compose -p gendesign`) | `gendesign-obsidian` |
| Workflow | `.github/workflows/deploy.yml` | `.github/workflows/deploy-obsidian.yml` |
| Триггер push paths | `backend/**`, `frontend/**`, `Caddyfile`, main compose | `docker-compose.obsidian.yml`, `scripts/setup-couchdb.sh`, docs |
| Сервисы | backend, frontend, postgres, redis, worker, beat, caddy | couchdb |
| Восстанавливается отдельно | ✓ | ✓ |
**Связь**: внешняя docker-сеть `gendesign_shared` (создаётся вручную или
в первом деплое). Caddy в main-stack подключён к ней + к default —
маршрут `obsidian.gendsgn.ru` ходит в `couchdb:5984`.
При отсутствии obsidian-стека main работает без проблем (Caddy просто
вернёт 502 на obsidian-route).
## Deploy
### 1. DNS
A-запись:
```
obsidian.gendsgn.ru. A <VPS_IP>
```
(тот же IP что `gendsgn.ru`)
### 2. Secrets на VPS
Добавить в `backend/.env.runtime` (НЕ в репо!):
```bash
COUCHDB_USER=obsidian
COUCHDB_PASSWORD=<сгенерированный 32-байтный пароль>
```
Сгенерировать пароль:
```bash
openssl rand -base64 32
```
### 3. Первое создание shared network на VPS (один раз)
```bash
ssh gendesign
docker network create gendesign_shared
```
(автоматически создаётся в каждом из двух workflow если её нет, но
для первого ручного запуска удобнее самому).
### 4. Деплой
Изменения в `docker-compose.obsidian.yml` / `scripts/setup-couchdb.sh` /
`docs/obsidian-livesync.md` → GHA триггерит **`deploy-obsidian.yml`**
(не трогает main!).
Изменения в `backend/**`, `frontend/**`, `docker-compose.prod.yml`,
`Caddyfile` → триггерит **`deploy.yml`** (main).
Для ручного старта впервые:
```bash
git push # любой из 2 workflow задеплоит свою часть
```
Или вручную с VPS:
```bash
cd /opt/gendesign
# main:
docker compose -p gendesign -f docker-compose.prod.yml up -d
# obsidian:
docker compose -p gendesign-obsidian -f docker-compose.obsidian.yml up -d
```
Caddy автоматически получит Let's Encrypt сертификат для `obsidian.gendsgn.ru`
при первом обращении (если DNS уже резолвится).
### 5. Bootstrap CouchDB (один раз, идемпотентный)
`deploy-obsidian.yml` запускает bootstrap автоматически после `up -d`.
Если нужно вручную:
```bash
cd /opt/gendesign
source backend/.env.runtime # подгружаем COUCHDB_PASSWORD
COUCHDB_HOST=http://localhost:5984 \
COUCHDB_PASSWORD="$COUCHDB_PASSWORD" \
docker compose -p gendesign-obsidian -f docker-compose.obsidian.yml \
exec -T couchdb bash -c "$(cat scripts/setup-couchdb.sh)"
```
Скрипт:
- конвертирует CouchDB в "configured single-node" mode
- создаёт database `gendesign-vault`
- включает CORS (нужно для мобильных Obsidian)
- увеличивает лимиты на размер документов (для крупных attachments)
- настраивает auth-timeout (для длинных sync-сессий)
Идемпотентен — повторный запуск безопасен.
### 5. Проверка работы
```bash
# из любой точки интернета:
curl https://obsidian.gendsgn.ru/_up
# → {"status":"ok",...}
curl -u obsidian:<password> https://obsidian.gendsgn.ru/gendesign-vault
# → {"db_name":"gendesign-vault","doc_count":0,...}
```
## Настройка клиента (Obsidian Desktop)
### 1. Установить Obsidian
- https://obsidian.md/download
### 2. Создать vault
- "Create new vault" → название "gendesign" → выбрать локальную папку
### 3. Установить Self-hosted LiveSync plugin
- Settings → Community plugins → "Browse"
- Search: "Self-hosted LiveSync" (автор: vrtmrz)
- Install → Enable
### 4. Настроить подключение
Settings → Self-hosted LiveSync → **Setup wizard** (рекомендуется первый раз):
| Поле | Значение |
|---|---|
| URI | `https://obsidian.gendsgn.ru` |
| Username | `obsidian` (или то что в `COUCHDB_USER`) |
| Password | `<COUCHDB_PASSWORD>` |
| Database name | `gendesign-vault` |
| End-to-End Encryption | **OFF** (см. ниже про trade-off) |
Затем:
- "Test Database Connection" → должно быть зелёное ✓
- "Check database configuration" → если требует — выполнить "Fix all"
- "Apply settings"
- "LiveSync" → "Enable LiveSync"
### 5. Первичный sync
При первом подключении vault пустой и в БД ничего нет. После первой
правки .md файла LiveSync запушит изменение в CouchDB.
С другого устройства/у другого пользователя:
- Та же установка
- При first sync клиент скачает всё содержимое БД в локальный vault
### 6. Mobile (iOS / Android)
- Установить **Obsidian** из App Store / Google Play
- Создать **пустой** vault
- Установить Self-hosted LiveSync (как на desktop)
- Те же настройки → первый sync скачает всё с сервера
⚠️ На iOS: включить **Background App Refresh** для Obsidian, иначе sync только при
открытом приложении.
## End-to-End Encryption (E2EE)
| | E2EE ON | E2EE OFF (default у нас) |
|---|---|---|
| Контент в БД | Зашифрован (admin сервера не может прочитать) | Plain text |
| Производительность | Тяжелее на клиенте | Быстрее |
| Claude (через MCP/HTTP) сможет писать извне? | Нет (нужен passphrase у Claude) | Да |
| Восстановление при потере passphrase | Невозможно | Возможно (через CouchDB admin) |
**Рекомендую OFF** — vault это рабочие заметки, не банковская тайна.
HTTPS + basic auth + закрытый DNS защищают от внешнего читателя.
Если позже потребуется E2EE — переключить в plugin, ребутнуть БД (replicate from scratch).
## Multi-user
CouchDB поддерживает per-user permissions, но для простого MVP:
- Один shared `gendesign-vault` database
- Все пользователи логинятся как `obsidian` (один аккаунт)
Для разделения (read-only для одних, write — для других) — нужно добавить
users в `_users` DB и настроить security object базы:
```bash
curl -X PUT -u obsidian:PWD https://obsidian.gendsgn.ru/_users/org.couchdb.user:viewer \
-H "Content-Type: application/json" \
-d '{"name":"viewer","password":"VIEWER_PWD","roles":[],"type":"user"}'
```
Затем в vault DB:
```json
PUT /gendesign-vault/_security
{
"admins": {"names": ["obsidian"], "roles": []},
"members": {"names": ["viewer"], "roles": []}
}
```
## Backup
```bash
# Snapshot DB → tar.gz на VPS (можно поставить в cron)
docker compose exec couchdb tar czf /tmp/vault-backup-$(date +%F).tar.gz /opt/couchdb/data
docker cp gendesign-couchdb:/tmp/vault-backup-*.tar.gz /opt/gendesign-backups/
```
Опционально — `_replicate` через CouchDB API на S3-совместимое хранилище.
## Troubleshooting
### `Failed to connect` в LiveSync
- Проверить DNS: `dig obsidian.gendsgn.ru`
- Проверить HTTPS: `curl https://obsidian.gendsgn.ru/_up` (должен вернуть `{"status":"ok"}`)
- Проверить пароль: `curl -u obsidian:PWD https://obsidian.gendsgn.ru/gendesign-vault`
### CORS errors на mobile
- Запустить `setup-couchdb.sh` ещё раз (или проверить через `_config/cors/origins`)
### Большие attachments не синкаются
- В LiveSync plugin: "Customization & advanced" → "Chunk size" = 100 KB
- На сервере: `couchdb/max_document_size` уже = 50 MB
### Конфликты при одновременной правке
- LiveSync создаёт conflict-маркер в файле
- Plugin показывает UI для merge ("Show conflict resolution dialog")
## Что дальше
После того как multi-device sync работает:
- Решить как Claude пишет в vault: MCP filesystem + bridge / самописный CouchDB-MCP / headless Obsidian
- Мигрировать существующий `memory-gendesign.jsonl` knowledge graph → .md файлы в vault

107
scripts/setup-couchdb.sh Normal file
View file

@ -0,0 +1,107 @@
#!/usr/bin/env bash
# Bootstrap CouchDB для Obsidian Self-hosted LiveSync.
#
# Запускается ОДИН РАЗ после первого `docker compose up -d couchdb`.
# Идемпотентен — повторный запуск безопасен.
#
# Что делает:
# 1. Конвертирует single-node setup в "single-node configured" (требование CouchDB 3.x)
# 2. Включает CORS (нужно для Obsidian мобильного / browser-client)
# 3. Увеличивает max_http_request_size до 4 GiB (для крупных attachments)
# 4. Создаёт database `gendesign-vault` для самого vault
# 5. Создаёт database `_users` (системная, для multi-user в будущем)
#
# Использование:
# COUCHDB_PASSWORD=xxx ./scripts/setup-couchdb.sh
# # или на VPS:
# docker compose exec couchdb bash -c 'curl ...' # ручной путь
#
# Требует: curl. Запускать с хоста VPS (couchdb должен быть на 127.0.0.1:5984).
set -euo pipefail
COUCHDB_HOST="${COUCHDB_HOST:-http://localhost:5984}"
COUCHDB_USER="${COUCHDB_USER:-obsidian}"
COUCHDB_PASSWORD="${COUCHDB_PASSWORD:?COUCHDB_PASSWORD env var is required}"
VAULT_DB="${VAULT_DB:-gendesign-vault}"
AUTH="-u ${COUCHDB_USER}:${COUCHDB_PASSWORD}"
echo "=== 0. Wait for CouchDB ready ==="
for i in $(seq 1 30); do
if curl -fsS "${COUCHDB_HOST}/_up" >/dev/null 2>&1; then
echo " CouchDB up after ${i}s"
break
fi
sleep 1
done
echo "=== 1. Convert to 'configured single-node' (idempotent) ==="
curl -fsS ${AUTH} -X POST "${COUCHDB_HOST}/_cluster_setup" \
-H "Content-Type: application/json" \
-d '{"action":"enable_single_node","bind_address":"0.0.0.0","username":"'"${COUCHDB_USER}"'","password":"'"${COUCHDB_PASSWORD}"'"}' \
|| echo " (уже configured, OK)"
echo "=== 2. Системные базы ==="
for db in _users _replicator _global_changes; do
curl -fsS ${AUTH} -X PUT "${COUCHDB_HOST}/${db}" 2>&1 \
| grep -q '"ok":true\|"file_exists"' && echo " ${db}: OK"
done
echo "=== 3. Vault database '${VAULT_DB}' ==="
curl -fsS ${AUTH} -X PUT "${COUCHDB_HOST}/${VAULT_DB}" 2>&1 \
| grep -q '"ok":true\|"file_exists"' && echo " ${VAULT_DB}: OK"
echo "=== 4. CORS (для мобильных клиентов) ==="
cors_settings=(
"httpd/enable_cors true"
"cors/origins app://obsidian.md,capacitor://localhost,http://localhost"
"cors/credentials true"
"cors/methods GET,PUT,POST,HEAD,DELETE"
"cors/headers accept,authorization,content-type,origin,referer,x-csrf-token"
)
for kv in "${cors_settings[@]}"; do
key="${kv%% *}"
value="${kv#* }"
curl -fsS ${AUTH} -X PUT \
"${COUCHDB_HOST}/_node/_local/_config/${key}" \
-H "Content-Type: application/json" \
-d "\"${value}\"" >/dev/null
echo " ${key} = ${value}"
done
echo "=== 5. Размер запросов / chunks (для больших документов LiveSync) ==="
curl -fsS ${AUTH} -X PUT \
"${COUCHDB_HOST}/_node/_local/_config/chttpd/max_http_request_size" \
-H "Content-Type: application/json" \
-d '"4294967296"' >/dev/null
echo " chttpd/max_http_request_size = 4 GiB"
curl -fsS ${AUTH} -X PUT \
"${COUCHDB_HOST}/_node/_local/_config/couchdb/max_document_size" \
-H "Content-Type: application/json" \
-d '"50000000"' >/dev/null
echo " couchdb/max_document_size = 50 MB"
echo "=== 6. Auth settings (CouchDB 3.x требует session timeout для LiveSync) ==="
curl -fsS ${AUTH} -X PUT \
"${COUCHDB_HOST}/_node/_local/_config/chttpd_auth/require_valid_user" \
-H "Content-Type: application/json" \
-d '"true"' >/dev/null
curl -fsS ${AUTH} -X PUT \
"${COUCHDB_HOST}/_node/_local/_config/chttpd_auth/timeout" \
-H "Content-Type: application/json" \
-d '"7200"' >/dev/null
echo " chttpd_auth: require_valid_user=true, timeout=7200s"
echo ""
echo "=== DONE ==="
echo "CouchDB готова к Self-hosted LiveSync."
echo ""
echo "Клиенту Obsidian → Settings → Self-hosted LiveSync → Remote DB:"
echo " URL: https://obsidian.gendsgn.ru"
echo " Database: ${VAULT_DB}"
echo " User: ${COUCHDB_USER}"
echo " Password: <COUCHDB_PASSWORD>"
echo ""
echo "Дальше: 'Test Database Connection' → 'Apply Settings' → 'Replicate Now'."