Files
nexa/docs/09-deployment.md
T
Claude a1e14c64c3 Apply Q1–Q3 decisions: GraphDB/SPARQL, reuse Qdrant, self-host embeddings via TEI
- Q1 → Ontotext GraphDB (SPARQL). docs/08 fully rewritten with RDF schema,
  example SPARQL queries (transitive deps via property paths, time-windowed
  topic counts, cross-pillar joins via nexa:vectorId).
- Q2 → reuse qdrant_scientific with nexa_* collection prefix; docs/09 step 2
  now points there explicitly.
- Q3 → no OpenAI embeddings. Self-host on the docker host. Use TEI
  (HuggingFace text-embeddings-inference) — single Rust binary, ~500 MB image,
  OpenAI-compatible — instead of Ollama, since we only need embeddings.
- docs/09 Phase-3.4 add-on simplified to a single Ontotext compose snippet
  (Neo4j option dropped) plus repo creation curl.
- docs/11 Q3 marked resolved; new Q15 picks the model (bge-m3 vs nomic-embed)
  and adds the open question of whether SAIA already proxies an embedding
  model that would let us skip TEI entirely.
- docs/03 + CLAUDE.md updated with the new decisions so future runs don't
  re-litigate.
2026-05-04 21:26:08 +00:00

9.1 KiB
Raw Blame History

09 — Deployment

Pragmatic deployment guide that assumes the existing homelab and adds only what's missing.

What's already running (no action required)

Surveyed from Homepage / Dozzle / Proxmox / Zoraxy:

Service Host / port URL
Memos docker LXC 104 → :5230 https://memos.nuclide.systems
n8n docker LXC 104 → :5678 https://n8n.nuclide.systems
LiteLLM (SAIA gateway) docker LXC 104 → :4000 https://ai.nuclide.systems (proxies LobeHub UI :3210; API on :4000)
Nextcloud LXC 105 https://nc.nuclide.systems
ntfy docker LXC 104 → :7998 https://ntfy.nuclide.systems
Karakeep (Hoarder) docker LXC 104 → :3090 https://hoarder.nuclide.systems
Home Assistant VM 100 (HAOS) https://ha.nuclide.systems
Pocket-ID (OAuth/SSO) docker LXC 104 → :1411 https://id.nuclide.systems
Vaultwarden docker LXC 104 → :11001 https://vault.nuclide.systems
Backrest LXC 103 (internal)
AdGuard DNS LXC 102 (internal)
Zoraxy reverse proxy LXC 108 → 192.168.1.4:8000 TLS for *.nuclide.systems
qdrant_scientific (existing) docker LXC 104 reused — Nexa uses nexa_* collections in this instance

The deployment task is not "spin up the stack" — most of the stack is already up. It is wire Nexa across these services + add the small bits that are missing.

What's missing for Nexa

  1. Qdrant collection for Nexa (nexa_knowledge) inside the existing qdrant_scientific instance — vector dim follows Q15 (1024 for bge-m3, 768 for nomic-embed-text).
  2. TEI (HF text-embeddings-inference) on the docker host for self-hosted embeddings (LiteLLM key is not authorised for OpenAI embeddings — see 11/Q3+Q15). Lighter than Ollama: single Rust binary, ~500 MB image, no LLM runtime.
  3. n8n workflows (./nexa-core/n8n-workflows/) imported into the running n8n.
  4. Nextcloud lists & calendars for Work / Personal / Shopping / Wishes (auto-discovered via #nexa:config).
  5. Memos webhook → n8n wired through the Memos config.
  6. LiteLLM virtual key for the nexa user with chat-only access (no embeddings — handled by Ollama).
  7. A Zoraxy host entry is not needed — Memos / n8n / LiteLLM are already proxied.
  8. (Phase 3.4) Ontotext GraphDB for the SPARQL pillar — see add-on at the bottom of this doc.

Step 1 — Secrets

Copy nexa-core/.env.examplenexa-core/.env and fill only the secrets:

cd nexa-core
cp .env.example .env
$EDITOR .env       # MEMOS_API_KEY, SAIA_API_KEY, NC_APP_PASSWORD, QDRANT_API_KEY

The .env is only used at bootstrap time. Everything else (list IDs, calendar IDs, collection sizes) is discovered at runtime via #nexa:config (see 05). No secrets should ever live in n8n workflow JSON — use n8n credentials instead.

Step 2 — Qdrant collection

# adjust QDRANT_HOST in .env first
source nexa-core/.env

curl -X PUT "$QDRANT_HOST/collections/nexa_knowledge" \
  -H "Content-Type: application/json" \
  -H "api-key: $QDRANT_API_KEY" \
  -d @nexa-core/config/qdrant_schema.json

Targets the existing qdrant_scientific instance — just an extra collection, no new container. The schema file's vectors.size must match the embedding model picked in Q15: 1024 for bge-m3, 768 for nomic-embed-text-v1.5.

Step 3 — Self-hosted embeddings (TEI)

Use HuggingFace text-embeddings-inference — single Rust binary, ~500 MB image, OpenAI-compatible API, loads exactly one model. Lighter than Ollama because there's no LLM runtime, no GGUF loader, no model registry.

# on the docker host (LXC 104)
docker run -d --name nexa-embed \
  --restart unless-stopped \
  -p 127.0.0.1:8080:80 \
  -v tei-data:/data \
  ghcr.io/huggingface/text-embeddings-inference:cpu-1.5 \
  --model-id BAAI/bge-m3

Memory budget: ~1.1 GB resident (bge-m3 is ~1 GB + ~100 MB overhead). First start downloads the model into the named volume; subsequent restarts are instant.

Register it inside LiteLLM (admin UI → Models) with the OpenAI-compatible adapter:

  • model name: nexa-embed
  • provider: openai
  • model: bge-m3
  • api_base: http://nexa-embed:80/v1
  • api_key: any non-empty string (TEI ignores it)

Now n8n only ever talks to LiteLLM and the model is swappable without touching workflows.

Step 4 — LiteLLM virtual key

In the LiteLLM admin UI (ai.nuclide.systems):

  1. Create user nexa.
  2. Issue a virtual key with access to:
    • one chat model (already-available model from your SAIA gateway).
    • the nexa-embed model from Step 3.
  3. Paste the key into SAIA_API_KEY in .env.

Step 4 — n8n workflows

Import the JSON exports — credentials are filled inside n8n, not in the JSON:

# n8n personal access token from the n8n UI: Settings → API
N8N_URL=https://n8n.nuclide.systems
N8N_TOKEN=...    # from the n8n UI

for f in nexa-core/n8n-workflows/phase-1/*.json \
         nexa-core/n8n-workflows/phase-2/*.json; do
  curl -X POST "$N8N_URL/api/v1/workflows" \
    -H "X-N8N-API-KEY: $N8N_TOKEN" \
    -H "Content-Type: application/json" \
    --data-binary "@$f"
done

Inside n8n, attach credentials to the imported nodes:

  • Memos → HTTP header Authorization: Bearer $MEMOS_API_KEY
  • LiteLLM → header Authorization: Bearer $SAIA_API_KEY
  • Nextcloud → app password
  • Qdrant → header api-key: $QDRANT_API_KEY

Activate each workflow individually after smoke-test.

Step 5 — Memos webhook

In the Memos admin UI, set the webhook URL to the production address of the discovery workflow:

https://n8n.nuclide.systems/webhook/memos

The same URL is the one the workflow exposes; verify with:

curl -i https://n8n.nuclide.systems/webhook/memos
# expect 200 / 405, never 404

Step 6 — Bootstrap commands via Memos

Create a memo with body #nexa:config — the discovery workflow:

  1. Lists Nextcloud Tasks lists, picks Work / Personal / Shopping / Wishes by name.
  2. Counts existing Qdrant points in nexa_knowledge.
  3. Verifies LiteLLM reachability + lists available models.
  4. Replies as a comment with a runtime-config snapshot that's stored as Qdrant metadata (_config namespace) and as nexa-core/config/runtime_config.json (gitignored).

After this point, .env is read-once. Subsequent runs read config from Qdrant.

Step 7 — Smoke tests

# (1) Memos round-trip — should produce a comment within ~5 s
curl -X POST https://memos.nuclide.systems/api/v1/memos \
  -H "Authorization: Bearer $MEMOS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content":"- [ ] testing the router #nexa"}'

# (2) Classification dry-run
curl -X POST https://memos.nuclide.systems/api/v1/memos \
  -H "Authorization: Bearer $MEMOS_API_KEY" \
  -d '{"content":"#nexa:route-test buy milk"}'

# (3) RAG test (requires at least one indexed memo/note)
curl -X POST https://memos.nuclide.systems/api/v1/memos \
  -H "Authorization: Bearer $MEMOS_API_KEY" \
  -d '{"content":"#nexa:ask what is the goal of nexa?"}'

Step 8 — Reverse proxy

Already done — Zoraxy at 192.168.1.4:8000 terminates TLS for *.nuclide.systems and forwards to docker LXC 104 (192.168.1.40). No new entry is required for Nexa: every service Nexa talks to already has a host entry.

Step 9 — Backups

Already covered by Backrest (LXC 103). Add:

  • n8n workflowsnexa-core/scripts/backup_workflows.sh (already present) into a Backrest schedule.
  • Qdrant snapshots → schedule a daily POST /collections/nexa_knowledge/snapshots and rsync to S3 (s3.nuclide.systems). Add as a Backrest pre-hook on the docker host.

For deeper detail: 10 — Operations.


Phase add-on: Ontotext GraphDB (Phase 3.4)

Defer until 3.13.3 ship.

# nexa-core/docker-compose.graph.yml
services:
  graphdb:
    image: ontotext/graphdb:10.7.0
    container_name: nexa-graphdb
    ports: ["127.0.0.1:7200:7200"]
    environment:
      GDB_JAVA_OPTS: "-Xmx4g -Xms1g"
    volumes:
      - ./data/graphdb:/opt/graphdb/home
    restart: unless-stopped

After first start, create the repository (one-time):

curl -X POST http://localhost:7200/rest/repositories \
  -H 'Content-Type: application/json' \
  -d '{
    "id": "nexa_knowledge",
    "title": "Nexa Knowledge Graph",
    "type": "graphdb",
    "params": {
      "ruleset":     {"value": "rdfsplus-optimized"},
      "baseURL":     {"value": "https://nuclide.systems/nexa/"}
    }
  }'

Optional Zoraxy entry graph.nuclide.systems192.168.1.40:7200 if you want the SPARQL Workbench in a browser; otherwise n8n talks to it on the docker network at http://nexa-graphdb:7200.

For schema and example queries: 08-graphrag-architecture.


Step-back / rollback

  • Disable any Nexa workflow in n8n — deactivates the side effect immediately, Memos webhooks become no-ops.
  • Drop a Qdrant collectioncurl -X DELETE $QDRANT_HOST/collections/nexa_knowledge -H "api-key: $QDRANT_API_KEY".
  • Re-discover#nexa:reset-config then #nexa:config.