chore: sync accumulated infra changes and docs
Removals: openwebui and netclaw compose + Traefik routes (both decommissioned). Updates: litellm, n8n, vaultwarden, ai-agents, media, traefik routes, CLAUDE.md (nodes/ports/known-issues), agent definitions. Additions: Rongorongo tree, LinkedStorm plan, rename-bart spec, vllm brief, tls-options and security-headers Traefik config. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -142,6 +142,7 @@ When creating documentation:
|
||||
2. Full content in markdown format
|
||||
3. Mermaid diagrams embedded within the documentation
|
||||
4. Summary of what was documented and any follow-up actions needed
|
||||
5. Update the change logs
|
||||
|
||||
When providing information to other agents:
|
||||
1. The specific information requested
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
"Bash(docker version:*)",
|
||||
"Bash(/tmp/mcpo-config.json << 'EOF'\n{\n \"servers\": []\n}\nEOF)",
|
||||
"Bash(/tmp/mcpo-config.json << 'EOF'\n{\n \"mcpServers\": {}\n}\nEOF)",
|
||||
"Bash(/tmp/mcpo-config.json << 'EOF'\n{\n \"mcpServers\": {\n \"example\": {\n \"command\": \"npx\",\n \"args\": [\"-y\", \"@modelcontextprotocol/server-everything\"],\n \"env\": {}\n }\n }\n}\nEOF)"
|
||||
"Bash(/tmp/mcpo-config.json << 'EOF'\n{\n \"mcpServers\": {\n \"example\": {\n \"command\": \"npx\",\n \"args\": [\"-y\", \"@modelcontextprotocol/server-everything\"],\n \"env\": {}\n }\n }\n}\nEOF)",
|
||||
"Bash(sleep 20 && sshpass -p '&Rip8a115' ssh -o StrictHostKeyChecking=no glyptic@192.168.86.100 \"docker ps --filter name=n8n --format 'table {{.Names}}\\\\t{{.Status}}' && echo '---' && docker logs n8n --tail 15 2>&1\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ hf-pdocker-01 (23GB RAM, i7-7700T, NVMe, HD 630 GPU) -- RESTORED (2026-03-08, ha
|
||||
├── [COMPOSE] Farm Manager API (:8888) -- dashboard + API server
|
||||
├── [COMPOSE] Farm Agent (:8889)
|
||||
├── [COMPOSE] Rongorongo decipherment engine -- 2 containers (rongorongo-web :8181, rongorongo-api). Server-synced state at /mnt/docker-data/configs/rongorongo/data. Traefik basicAuth on rongorongo.hobbs.farm.
|
||||
├── [COMPOSE] LinkedStorm (:8300 pipeline, :8310 api, cron no-port) -- whole-home audio assistant. VAD+Whisper STT+speaker-ID. Deployed 2026-05-28. Public: linkedstorm.hobbs.farm -> :8310.
|
||||
├── [REMOVED] VoiceCoach -- moved to GPU machine (192.168.86.150) 2026-04-04
|
||||
├── [COMPOSE] Node-exporter (:9100) -- standalone with pid:host
|
||||
├── [COMPOSE] Telegraf -- metrics collection to InfluxDB
|
||||
@@ -58,6 +59,7 @@ hf-pdocker-02 (7.6GB RAM, i7-6700T, HDD)
|
||||
├── [COMPOSE] Wiki.js + wikijs-db (PostgreSQL)
|
||||
├── [COMPOSE] Vaultwarden, Actual Budget, MCPO, Trivy
|
||||
├── [COMPOSE] n8n + n8n-db (PostgreSQL) -- workflow automation
|
||||
├── [COMPOSE] LifeOS (:8095) -- personal life/goals/habits tracker (PWA). Local mode: Flask+SQLite, no Supabase. LAN-only (no auth in backend). AI companion -> Ollama on .150.
|
||||
├── [COMPOSE] Farm Agent (:8889)
|
||||
├── [COMPOSE] Node-exporter (:9100) -- standalone with pid:host
|
||||
├── [COMPOSE] Telegraf -- metrics collection to InfluxDB
|
||||
@@ -140,8 +142,9 @@ compose/ # Standalone Compose definitions (deployed per-node)
|
||||
- Home Assistant: `yoda.hobbs.farm`
|
||||
- Vaultwarden: `vault.hobbs.farm`
|
||||
- Gitea: `git.hobbs.farm`
|
||||
- VoiceCoach: `coach.hobbs.farm` (LiveKit WebRTC: `voice.hobbs.farm`)
|
||||
- FRIDAY Voice: `coach.hobbs.farm` (replaced VoiceCoach 2026-07-05; runs on .150 :8320)
|
||||
- Rongorongo: `rongorongo.hobbs.farm`
|
||||
- LinkedStorm: `linkedstorm.hobbs.farm`
|
||||
|
||||
### Internal Services by Node
|
||||
|
||||
@@ -167,7 +170,8 @@ compose/ # Standalone Compose definitions (deployed per-node)
|
||||
- Farm Manager: :8888 (dashboard + API)
|
||||
- Farm Agent: :8889
|
||||
- Rongorongo: :8181 (rongorongo-web nginx; reverse-proxies /api/* to rongorongo-api Flask backend on the internal `rongorongo` bridge net). State + images at /mnt/docker-data/configs/rongorongo/data/. Public URL gated by Traefik basicAuth (creds in Vaultwarden).
|
||||
- ~~VoiceCoach~~ -- moved to GPU machine (192.168.86.150) 2026-04-04
|
||||
- LinkedStorm: :8300 (pipeline/VAD+STT+speaker-ID), :8310 (API/dashboard), cron (no port). Public: linkedstorm.hobbs.farm -> :8310. Source: /opt/linkedstorm (rsync from WSL). Data: /mnt/docker-data/configs/linkedstorm/. STT via Speaches GPU machine :8000, LLM via Ollama :11434 (qwen2.5:14b).
|
||||
- ~~VoiceCoach~~ -- fully decommissioned 2026-07-05, replaced by FRIDAY Voice on .150 (see GPU Machine section + memory `friday-voice`)
|
||||
|
||||
**hf-pdocker-02 (192.168.86.100):**
|
||||
- Farm Agent: :8889
|
||||
@@ -179,6 +183,7 @@ compose/ # Standalone Compose definitions (deployed per-node)
|
||||
- Actual Budget: :5006
|
||||
- MCPO: :8001
|
||||
- Trivy: :4954
|
||||
- LifeOS: :8095 (host) -> :8080 (container). LAN-only, deliberately NOT behind Traefik: the Flask backend is single-user with NO auth. Image `lifeos:local` built on-node from /opt/lifeos (upstream https://github.com/lifeos-app/lifeos, pinned 3009bf6). SQLite at /var/lib/lifeos/data.db on LOCAL disk (NFS breaks SQLite WAL locking). Compose: compose/hf-pdocker-02/lifeos/. Note: host 8080 is taken by unifi-os-server.
|
||||
|
||||
**bart (192.168.86.167):**
|
||||
- Farm Agent: :8889
|
||||
@@ -190,17 +195,14 @@ compose/ # Standalone Compose definitions (deployed per-node)
|
||||
- Jellyseerr: :5055
|
||||
- OpenVAS: :9392
|
||||
|
||||
**GPU Machine (192.168.86.150, RTX 3090, 15GB RAM):**
|
||||
- Ollama: :11434 (llama3.1:8b for VoiceCoach LLM, qwen2.5:14b, others)
|
||||
- [COMPOSE] VoiceCoach (all services co-located):
|
||||
- voicecoach-livekit: :7880/:7881, UDP 50000-50100 (public at voice.hobbs.farm)
|
||||
- voicecoach-speaches-gpu: :8000 (GPU Whisper STT + Kokoro TTS, CUDA float16)
|
||||
- voicecoach-speechbrain: :8001 (emotion analysis)
|
||||
- voicecoach-token-server: :8080 (token + settings API)
|
||||
- voicecoach-pipecat: pipeline agent (LLM via local Ollama llama3.1:8b)
|
||||
- voicecoach-web: :8085 (public at coach.hobbs.farm)
|
||||
- voicecoach-openclaw: :3100 (idle, kept for future Claude API routing)
|
||||
- Compose: ~/voicecoach-speaches/docker-compose.yml
|
||||
**GPU Machine (192.168.86.150, RTX 3090):**
|
||||
- **NOTE (2026-07-05):** .150 is actually the user's **Windows workstation** (runs the WSL/Claude Code session), not a headless Linux server. It runs **Docker Desktop** (WSL2 backend, RTX 3090 GPU passthrough working) alongside **UniFi OS Server** (Podman). Docker Desktop panics if UniFi/Podman claims `\\.\pipe\docker_engine` first — start Docker Desktop before UniFi OS Server. See memory `docker-desktop-unifi-podman-conflict`.
|
||||
- **FRIDAY Voice replaced VoiceCoach (2026-07-05).** VoiceCoach fully decommissioned (v1+v2 removed from hf-pdocker-01; voice.hobbs.farm route removed). See memory `friday-voice`.
|
||||
- [COMPOSE] **FRIDAY Voice stack (all on .150 Docker Desktop, via `docker.exe -c desktop-linux`):**
|
||||
- **voicecoach-speaches-gpu**: :8000 — GPU Whisper large-v3-turbo (STT) + Kokoro-82M af_nova (TTS), CUDA float16. Compose: `compose/gpu-machine/voicecoach-speaches/`.
|
||||
- **hermes**: `nousresearch/hermes-agent:latest` gateway — FRIDAY's reasoning agent. Settings transferred from WSL `~/.hermes` into named volume `hermes-data`. Compose: `compose/gpu-machine/hermes/`.
|
||||
- **friday-voice**: :8320 — push-to-talk console. Calls speaches (`host.docker.internal:8000`) + hermes (`docker exec hermes hermes -Q` via wrapper). Public: **coach.hobbs.farm** → :8320. Compose: `compose/gpu-machine/friday-voice/`.
|
||||
- Ollama: :11434 (qwen2.5:14b, Hermes-4.3-36B-GGUF, others)
|
||||
|
||||
### Port Conflict Notes
|
||||
|
||||
@@ -297,3 +299,7 @@ All persistent data on NAS at `/mnt/docker-data/`:
|
||||
9. **Guacamole schema**: App image pinned to `guacamole/guacamole:1.6.0` + `guacd:1.6.0` with Watchtower disabled. DB schema was reset to fresh 1.6.0 on 2026-05-20 (old schema was non-standard). Recreate connections after any DB reset.
|
||||
10. **Gitea image**: Pinned to `gitea/gitea:1.26.1` with Watchtower disabled (2026-05-20). Watchtower previously pulled broken `:latest` images causing missing git binary errors.
|
||||
11. **Home Assistant reverse proxy**: Config at `/mnt/docker-data/configs/homeassistant/configuration.yaml` (no hyphen). Must have `http: use_x_forwarded_for: true` + `trusted_proxies: [172.16.0.0/12, 127.0.0.1]` or proxied requests return 400.
|
||||
12. **hf-pdocker-01 memory pressure (2026-07-05)**: Node runs hot (~15/23GB, chronic swap). Mitigations applied: `vm.swappiness` lowered 60→10 (persisted in `/etc/sysctl.d/99-swappiness.conf`), and VoiceCoach (v1+v2, ~10 containers) fully removed (replaced by FRIDAY Voice on .150). Frigate is the main consumer (~9GB/12GB). Frigate has no semantic-search/face/LPR features enabled.
|
||||
13. **Docker Desktop vs UniFi OS Server pipe conflict (Windows box .150, 2026-07-05)**: Both fight over `\\.\pipe\docker_engine`; Docker Desktop's backend nil-panics ("backend exited before becoming ready") if Podman wins. Fix: let Docker Desktop (auto-starts at login) claim the pipe first, launch UniFi OS Server manually after. Full detail in memory `docker-desktop-unifi-podman-conflict`.
|
||||
14. **MySQL InnoDB corruption on NFS**: `semaphore-db` (mysql:8.0 on NFS) hit system-tablespace page corruption and crash-looped 3789× (2026-07-11), same class as the Guacamole-on-NFS issue (#9). Recovered in-place via `--innodb-force-recovery=1` → mysqldump → fresh volume → reimport (DB was empty, no data lost); admin recreated with `semaphore user add`. Any MySQL DB on NFS is at risk — prefer LOCAL storage like guacamole-db. See memory `semaphore-db-nfs-corruption`.
|
||||
15. **litellm hangs after DB blip**: litellm does NOT self-recover from a transient `litellm-db` disconnect — it loops on "Prisma DB reconnect failed" forever; needs manual `docker restart litellm`. Its healthcheck was fixed from `curl` (absent from image, permanent false-unhealthy) to a python urllib check (2026-07-11).
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
# RONGORONGO_DEPLOY.md
|
||||
# Rongorongo Decipherment Engine — Claude Code Deployment Guide
|
||||
# Target: hobbs.farm Docker Swarm infrastructure
|
||||
# Subdomain: rongorongo.hobbs.farm
|
||||
|
||||
---
|
||||
|
||||
## CONTEXT
|
||||
|
||||
This document instructs Claude Code to build, push, and deploy the Rongorongo
|
||||
Decipherment Engine as a containerized static web app to the hobbs.farm Swarm.
|
||||
|
||||
The app is a single-file HTML/JS application served by nginx. It calls the
|
||||
Anthropic API directly from the browser. No backend, no database. All session
|
||||
state is stored in browser localStorage.
|
||||
|
||||
Infrastructure assumptions (from existing hobbs.farm stack):
|
||||
- Docker Swarm: nodes hf-pdocker-01, hf-pdocker-02, bart
|
||||
- Traefik reverse proxy with wildcard *.hobbs.farm TLS via Cloudflare DNS
|
||||
- External overlay network: traefik-public
|
||||
- Registry: registry.hobbs.farm (if local) OR use Docker Hub as fallback
|
||||
- Portainer CE available for stack management
|
||||
- DNS: AdGuard Home with hairpin NAT for *.hobbs.farm
|
||||
|
||||
---
|
||||
|
||||
## REPOSITORY STRUCTURE
|
||||
|
||||
After cloning or creating the project, verify this structure exists:
|
||||
|
||||
```
|
||||
rongorongo/
|
||||
├── Dockerfile
|
||||
├── nginx.conf
|
||||
├── stack.yml
|
||||
├── frontend/
|
||||
│ └── index.html
|
||||
└── RONGORONGO_DEPLOY.md (this file)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## STEP 1 — VERIFY PREREQUISITES
|
||||
|
||||
Run these checks before proceeding. Stop and report any failures.
|
||||
|
||||
```bash
|
||||
# Confirm Docker and Swarm context
|
||||
docker info | grep -E "Swarm:|Manager:|Nodes:"
|
||||
|
||||
# Confirm traefik-public network exists
|
||||
docker network ls | grep traefik-public
|
||||
|
||||
# Confirm Traefik is running
|
||||
docker service ls | grep traefik
|
||||
|
||||
# Test registry connectivity (if using local registry)
|
||||
curl -s https://registry.hobbs.farm/v2/_catalog || echo "Registry unreachable — will use build-and-load method"
|
||||
|
||||
# Confirm you are on a Swarm manager node
|
||||
docker node ls
|
||||
```
|
||||
|
||||
If registry.hobbs.farm is unreachable, use the ALTERNATIVE DEPLOYMENT method
|
||||
at the bottom of this document.
|
||||
|
||||
---
|
||||
|
||||
## STEP 2 — BUILD THE IMAGE
|
||||
|
||||
```bash
|
||||
cd rongorongo/
|
||||
|
||||
# Build image
|
||||
docker build -t registry.hobbs.farm/rongorongo:latest .
|
||||
|
||||
# Verify image was created
|
||||
docker images | grep rongorongo
|
||||
```
|
||||
|
||||
Expected output: image tagged `registry.hobbs.farm/rongorongo:latest`, size
|
||||
should be ~25–40MB (nginx:alpine base + small HTML file).
|
||||
|
||||
---
|
||||
|
||||
## STEP 3 — PUSH TO REGISTRY
|
||||
|
||||
```bash
|
||||
# If registry requires auth:
|
||||
docker login registry.hobbs.farm
|
||||
|
||||
# Push
|
||||
docker push registry.hobbs.farm/rongorongo:latest
|
||||
```
|
||||
|
||||
If the push fails due to registry issues, fall through to ALTERNATIVE DEPLOYMENT.
|
||||
|
||||
---
|
||||
|
||||
## STEP 4 — DEPLOY STACK
|
||||
|
||||
```bash
|
||||
# Deploy or update the stack
|
||||
docker stack deploy -c stack.yml rongorongo --with-registry-auth
|
||||
|
||||
# Watch rollout
|
||||
docker service ps rongorongo_rongorongo --no-trunc
|
||||
```
|
||||
|
||||
Wait for the service to show `Running` status. This typically takes 15–30 seconds.
|
||||
|
||||
```bash
|
||||
# Confirm healthy
|
||||
docker service ls | grep rongorongo
|
||||
curl -s http://localhost/health # or via container exec
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## STEP 5 — DNS VERIFICATION
|
||||
|
||||
The stack.yml assumes `rongorongo.hobbs.farm` resolves to the Traefik entrypoint.
|
||||
|
||||
```bash
|
||||
# Check AdGuard Home has the record (or wildcard covers it)
|
||||
nslookup rongorongo.hobbs.farm
|
||||
|
||||
# Test internal access
|
||||
curl -sk https://rongorongo.hobbs.farm/health
|
||||
```
|
||||
|
||||
If using AdGuard Home for internal DNS and the wildcard *.hobbs.farm is already
|
||||
configured for hairpin NAT, no additional DNS entry is needed.
|
||||
|
||||
---
|
||||
|
||||
## STEP 6 — SMOKE TEST
|
||||
|
||||
Open https://rongorongo.hobbs.farm in a browser and verify:
|
||||
1. App loads with "Rongorongo · Decipherment Engine" title
|
||||
2. Sidebar navigation works (Image Corpus, Glyph Catalog, etc.)
|
||||
3. Enter an Anthropic API key in the sidebar footer input
|
||||
4. Upload a test image to the corpus
|
||||
5. Navigate to Pattern Analyzer and run an analysis
|
||||
6. Confirm AI response appears in the result box
|
||||
|
||||
---
|
||||
|
||||
## UPDATING THE APP
|
||||
|
||||
When index.html is modified:
|
||||
|
||||
```bash
|
||||
cd rongorongo/
|
||||
|
||||
# Rebuild
|
||||
docker build -t registry.hobbs.farm/rongorongo:latest .
|
||||
|
||||
# Push
|
||||
docker push registry.hobbs.farm/rongorongo:latest
|
||||
|
||||
# Force service update (pull new image)
|
||||
docker service update --image registry.hobbs.farm/rongorongo:latest --force rongorongo_rongorongo
|
||||
|
||||
# Watch update
|
||||
docker service ps rongorongo_rongorongo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ROLLBACK
|
||||
|
||||
If something breaks after an update:
|
||||
|
||||
```bash
|
||||
docker service rollback rongorongo_rongorongo
|
||||
docker service ps rongorongo_rongorongo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TEARDOWN
|
||||
|
||||
To remove the service without destroying data (there is no persistent volume):
|
||||
|
||||
```bash
|
||||
docker stack rm rongorongo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ALTERNATIVE DEPLOYMENT (no registry)
|
||||
|
||||
Use this if registry.hobbs.farm is unavailable. This method builds the image
|
||||
directly on the Swarm manager and uses `docker service create` instead of
|
||||
`stack deploy`.
|
||||
|
||||
```bash
|
||||
# On the Swarm manager node:
|
||||
cd rongorongo/
|
||||
|
||||
# Build locally
|
||||
docker build -t rongorongo:latest .
|
||||
|
||||
# Create service manually (adjust Traefik labels as needed)
|
||||
docker service create \
|
||||
--name rongorongo_rongorongo \
|
||||
--network traefik-public \
|
||||
--label "traefik.enable=true" \
|
||||
--label "traefik.docker.network=traefik-public" \
|
||||
--label "traefik.http.routers.rongorongo.rule=Host(\`rongorongo.hobbs.farm\`)" \
|
||||
--label "traefik.http.routers.rongorongo.entrypoints=websecure" \
|
||||
--label "traefik.http.routers.rongorongo.tls=true" \
|
||||
--label "traefik.http.routers.rongorongo.tls.certresolver=cloudflare" \
|
||||
--label "traefik.http.services.rongorongo.loadbalancer.server.port=80" \
|
||||
--replicas 1 \
|
||||
--restart-condition on-failure \
|
||||
rongorongo:latest
|
||||
|
||||
# Monitor
|
||||
docker service ps rongorongo_rongorongo
|
||||
```
|
||||
|
||||
NOTE: With this method, to update you must rebuild locally and run:
|
||||
```bash
|
||||
docker service update --image rongorongo:latest --force rongorongo_rongorongo
|
||||
```
|
||||
|
||||
This only works if your current node is the one running the replica. If you
|
||||
have a multi-node swarm, you should set up the registry to avoid placement issues.
|
||||
|
||||
---
|
||||
|
||||
## PORTAINER DEPLOYMENT (optional GUI method)
|
||||
|
||||
If you prefer Portainer CE:
|
||||
|
||||
1. Navigate to https://portainer.hobbs.farm (or your Portainer URL)
|
||||
2. Select the Swarm endpoint
|
||||
3. Go to Stacks → Add Stack
|
||||
4. Name: `rongorongo`
|
||||
5. Paste the contents of stack.yml into the web editor
|
||||
6. Set `registry.hobbs.farm/rongorongo:latest` as the image
|
||||
(you still need to build and push first)
|
||||
7. Click Deploy the stack
|
||||
|
||||
---
|
||||
|
||||
## ENVIRONMENT NOTES
|
||||
|
||||
The app does NOT use environment variables or Docker secrets. The Anthropic API
|
||||
key is entered by the user at runtime and stored in their browser's localStorage.
|
||||
This is intentional — the app is designed to be used by a single user (Jeremy)
|
||||
and no server-side key storage is needed or appropriate.
|
||||
|
||||
If you want to pre-populate the API key for convenience (only if the deployment
|
||||
is strictly private/internal), you can inject it at build time by modifying
|
||||
frontend/index.html before building:
|
||||
|
||||
Find this line:
|
||||
```javascript
|
||||
function getApiKey() {
|
||||
return document.getElementById('apiKeyInput').value.trim()
|
||||
```
|
||||
|
||||
And prepend a hardcoded fallback (NOT recommended for any externally accessible URL).
|
||||
|
||||
---
|
||||
|
||||
## TRAEFIK ENTRYPOINT NAMES
|
||||
|
||||
The stack.yml assumes:
|
||||
- HTTP entrypoint name: `web`
|
||||
- HTTPS entrypoint name: `websecure`
|
||||
- Cert resolver name: `cloudflare`
|
||||
|
||||
These match the standard hobbs.farm Traefik configuration. If your Traefik uses
|
||||
different names, edit the labels in stack.yml before deploying.
|
||||
|
||||
---
|
||||
|
||||
## TROUBLESHOOTING
|
||||
|
||||
**Service won't start:**
|
||||
```bash
|
||||
docker service ps rongorongo_rongorongo --no-trunc
|
||||
docker inspect $(docker ps -q --filter name=rongorongo) 2>/dev/null | grep -A5 '"State"'
|
||||
```
|
||||
|
||||
**Traefik not routing:**
|
||||
```bash
|
||||
# Check Traefik logs for rongorongo
|
||||
docker service logs traefik_traefik 2>&1 | grep -i rongorongo | tail -20
|
||||
# Confirm labels are applied
|
||||
docker service inspect rongorongo_rongorongo | grep -A2 Labels
|
||||
```
|
||||
|
||||
**TLS cert not issuing:**
|
||||
```bash
|
||||
# Wildcard cert should already cover rongorongo.hobbs.farm
|
||||
# If not, check Cloudflare DNS API token has Zone:Read and DNS:Edit
|
||||
docker service logs traefik_traefik 2>&1 | grep -i "acme\|certificate\|cloudflare" | tail -20
|
||||
```
|
||||
|
||||
**API calls blocked (CORS/CSP):**
|
||||
The nginx.conf includes a Content-Security-Policy header that allows
|
||||
`connect-src https://api.anthropic.com`. If you see CORS errors in the browser
|
||||
console, verify the CSP header is present:
|
||||
```bash
|
||||
curl -I https://rongorongo.hobbs.farm/ | grep -i content-security
|
||||
```
|
||||
|
||||
**App loads but AI calls fail:**
|
||||
- Check API key is entered correctly in the sidebar
|
||||
- Anthropic API key must start with `sk-ant-`
|
||||
- Confirm outbound HTTPS to api.anthropic.com is not blocked by firewall rules
|
||||
- The app sends `anthropic-dangerous-direct-browser-access: true` header which
|
||||
is required for direct browser API access
|
||||
|
||||
---
|
||||
|
||||
## END OF DEPLOYMENT GUIDE
|
||||
@@ -0,0 +1,18 @@
|
||||
FROM nginx:1.25-alpine
|
||||
|
||||
# Remove default nginx static content
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
|
||||
# Copy app
|
||||
COPY frontend/index.html /usr/share/nginx/html/index.html
|
||||
|
||||
# Copy nginx config
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Healthcheck
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget -qO- http://localhost/health || exit 1
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -0,0 +1,322 @@
|
||||
# RONGORONGO_DEPLOY.md
|
||||
# Rongorongo Decipherment Engine — Claude Code Deployment Guide
|
||||
# Target: hobbs.farm Docker Swarm infrastructure
|
||||
# Subdomain: rongorongo.hobbs.farm
|
||||
|
||||
---
|
||||
|
||||
## CONTEXT
|
||||
|
||||
This document instructs Claude Code to build, push, and deploy the Rongorongo
|
||||
Decipherment Engine as a containerized static web app to the hobbs.farm Swarm.
|
||||
|
||||
The app is a single-file HTML/JS application served by nginx. It calls the
|
||||
Anthropic API directly from the browser. No backend, no database. All session
|
||||
state is stored in browser localStorage.
|
||||
|
||||
Infrastructure assumptions (from existing hobbs.farm stack):
|
||||
- Docker Swarm: nodes hf-pdocker-01, hf-pdocker-02, bart
|
||||
- Traefik reverse proxy with wildcard *.hobbs.farm TLS via Cloudflare DNS
|
||||
- External overlay network: traefik-public
|
||||
- Registry: registry.hobbs.farm (if local) OR use Docker Hub as fallback
|
||||
- Portainer CE available for stack management
|
||||
- DNS: AdGuard Home with hairpin NAT for *.hobbs.farm
|
||||
|
||||
---
|
||||
|
||||
## REPOSITORY STRUCTURE
|
||||
|
||||
After cloning or creating the project, verify this structure exists:
|
||||
|
||||
```
|
||||
rongorongo/
|
||||
├── Dockerfile
|
||||
├── nginx.conf
|
||||
├── stack.yml
|
||||
├── frontend/
|
||||
│ └── index.html
|
||||
└── RONGORONGO_DEPLOY.md (this file)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## STEP 1 — VERIFY PREREQUISITES
|
||||
|
||||
Run these checks before proceeding. Stop and report any failures.
|
||||
|
||||
```bash
|
||||
# Confirm Docker and Swarm context
|
||||
docker info | grep -E "Swarm:|Manager:|Nodes:"
|
||||
|
||||
# Confirm traefik-public network exists
|
||||
docker network ls | grep traefik-public
|
||||
|
||||
# Confirm Traefik is running
|
||||
docker service ls | grep traefik
|
||||
|
||||
# Test registry connectivity (if using local registry)
|
||||
curl -s https://registry.hobbs.farm/v2/_catalog || echo "Registry unreachable — will use build-and-load method"
|
||||
|
||||
# Confirm you are on a Swarm manager node
|
||||
docker node ls
|
||||
```
|
||||
|
||||
If registry.hobbs.farm is unreachable, use the ALTERNATIVE DEPLOYMENT method
|
||||
at the bottom of this document.
|
||||
|
||||
---
|
||||
|
||||
## STEP 2 — BUILD THE IMAGE
|
||||
|
||||
```bash
|
||||
cd rongorongo/
|
||||
|
||||
# Build image
|
||||
docker build -t registry.hobbs.farm/rongorongo:latest .
|
||||
|
||||
# Verify image was created
|
||||
docker images | grep rongorongo
|
||||
```
|
||||
|
||||
Expected output: image tagged `registry.hobbs.farm/rongorongo:latest`, size
|
||||
should be ~25–40MB (nginx:alpine base + small HTML file).
|
||||
|
||||
---
|
||||
|
||||
## STEP 3 — PUSH TO REGISTRY
|
||||
|
||||
```bash
|
||||
# If registry requires auth:
|
||||
docker login registry.hobbs.farm
|
||||
|
||||
# Push
|
||||
docker push registry.hobbs.farm/rongorongo:latest
|
||||
```
|
||||
|
||||
If the push fails due to registry issues, fall through to ALTERNATIVE DEPLOYMENT.
|
||||
|
||||
---
|
||||
|
||||
## STEP 4 — DEPLOY STACK
|
||||
|
||||
```bash
|
||||
# Deploy or update the stack
|
||||
docker stack deploy -c stack.yml rongorongo --with-registry-auth
|
||||
|
||||
# Watch rollout
|
||||
docker service ps rongorongo_rongorongo --no-trunc
|
||||
```
|
||||
|
||||
Wait for the service to show `Running` status. This typically takes 15–30 seconds.
|
||||
|
||||
```bash
|
||||
# Confirm healthy
|
||||
docker service ls | grep rongorongo
|
||||
curl -s http://localhost/health # or via container exec
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## STEP 5 — DNS VERIFICATION
|
||||
|
||||
The stack.yml assumes `rongorongo.hobbs.farm` resolves to the Traefik entrypoint.
|
||||
|
||||
```bash
|
||||
# Check AdGuard Home has the record (or wildcard covers it)
|
||||
nslookup rongorongo.hobbs.farm
|
||||
|
||||
# Test internal access
|
||||
curl -sk https://rongorongo.hobbs.farm/health
|
||||
```
|
||||
|
||||
If using AdGuard Home for internal DNS and the wildcard *.hobbs.farm is already
|
||||
configured for hairpin NAT, no additional DNS entry is needed.
|
||||
|
||||
---
|
||||
|
||||
## STEP 6 — SMOKE TEST
|
||||
|
||||
Open https://rongorongo.hobbs.farm in a browser and verify:
|
||||
1. App loads with "Rongorongo · Decipherment Engine" title
|
||||
2. Sidebar navigation works (Image Corpus, Glyph Catalog, etc.)
|
||||
3. Enter an Anthropic API key in the sidebar footer input
|
||||
4. Upload a test image to the corpus
|
||||
5. Navigate to Pattern Analyzer and run an analysis
|
||||
6. Confirm AI response appears in the result box
|
||||
|
||||
---
|
||||
|
||||
## UPDATING THE APP
|
||||
|
||||
When index.html is modified:
|
||||
|
||||
```bash
|
||||
cd rongorongo/
|
||||
|
||||
# Rebuild
|
||||
docker build -t registry.hobbs.farm/rongorongo:latest .
|
||||
|
||||
# Push
|
||||
docker push registry.hobbs.farm/rongorongo:latest
|
||||
|
||||
# Force service update (pull new image)
|
||||
docker service update --image registry.hobbs.farm/rongorongo:latest --force rongorongo_rongorongo
|
||||
|
||||
# Watch update
|
||||
docker service ps rongorongo_rongorongo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ROLLBACK
|
||||
|
||||
If something breaks after an update:
|
||||
|
||||
```bash
|
||||
docker service rollback rongorongo_rongorongo
|
||||
docker service ps rongorongo_rongorongo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TEARDOWN
|
||||
|
||||
To remove the service without destroying data (there is no persistent volume):
|
||||
|
||||
```bash
|
||||
docker stack rm rongorongo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ALTERNATIVE DEPLOYMENT (no registry)
|
||||
|
||||
Use this if registry.hobbs.farm is unavailable. This method builds the image
|
||||
directly on the Swarm manager and uses `docker service create` instead of
|
||||
`stack deploy`.
|
||||
|
||||
```bash
|
||||
# On the Swarm manager node:
|
||||
cd rongorongo/
|
||||
|
||||
# Build locally
|
||||
docker build -t rongorongo:latest .
|
||||
|
||||
# Create service manually (adjust Traefik labels as needed)
|
||||
docker service create \
|
||||
--name rongorongo_rongorongo \
|
||||
--network traefik-public \
|
||||
--label "traefik.enable=true" \
|
||||
--label "traefik.docker.network=traefik-public" \
|
||||
--label "traefik.http.routers.rongorongo.rule=Host(\`rongorongo.hobbs.farm\`)" \
|
||||
--label "traefik.http.routers.rongorongo.entrypoints=websecure" \
|
||||
--label "traefik.http.routers.rongorongo.tls=true" \
|
||||
--label "traefik.http.routers.rongorongo.tls.certresolver=cloudflare" \
|
||||
--label "traefik.http.services.rongorongo.loadbalancer.server.port=80" \
|
||||
--replicas 1 \
|
||||
--restart-condition on-failure \
|
||||
rongorongo:latest
|
||||
|
||||
# Monitor
|
||||
docker service ps rongorongo_rongorongo
|
||||
```
|
||||
|
||||
NOTE: With this method, to update you must rebuild locally and run:
|
||||
```bash
|
||||
docker service update --image rongorongo:latest --force rongorongo_rongorongo
|
||||
```
|
||||
|
||||
This only works if your current node is the one running the replica. If you
|
||||
have a multi-node swarm, you should set up the registry to avoid placement issues.
|
||||
|
||||
---
|
||||
|
||||
## PORTAINER DEPLOYMENT (optional GUI method)
|
||||
|
||||
If you prefer Portainer CE:
|
||||
|
||||
1. Navigate to https://portainer.hobbs.farm (or your Portainer URL)
|
||||
2. Select the Swarm endpoint
|
||||
3. Go to Stacks → Add Stack
|
||||
4. Name: `rongorongo`
|
||||
5. Paste the contents of stack.yml into the web editor
|
||||
6. Set `registry.hobbs.farm/rongorongo:latest` as the image
|
||||
(you still need to build and push first)
|
||||
7. Click Deploy the stack
|
||||
|
||||
---
|
||||
|
||||
## ENVIRONMENT NOTES
|
||||
|
||||
The app does NOT use environment variables or Docker secrets. The Anthropic API
|
||||
key is entered by the user at runtime and stored in their browser's localStorage.
|
||||
This is intentional — the app is designed to be used by a single user (Jeremy)
|
||||
and no server-side key storage is needed or appropriate.
|
||||
|
||||
If you want to pre-populate the API key for convenience (only if the deployment
|
||||
is strictly private/internal), you can inject it at build time by modifying
|
||||
frontend/index.html before building:
|
||||
|
||||
Find this line:
|
||||
```javascript
|
||||
function getApiKey() {
|
||||
return document.getElementById('apiKeyInput').value.trim()
|
||||
```
|
||||
|
||||
And prepend a hardcoded fallback (NOT recommended for any externally accessible URL).
|
||||
|
||||
---
|
||||
|
||||
## TRAEFIK ENTRYPOINT NAMES
|
||||
|
||||
The stack.yml assumes:
|
||||
- HTTP entrypoint name: `web`
|
||||
- HTTPS entrypoint name: `websecure`
|
||||
- Cert resolver name: `cloudflare`
|
||||
|
||||
These match the standard hobbs.farm Traefik configuration. If your Traefik uses
|
||||
different names, edit the labels in stack.yml before deploying.
|
||||
|
||||
---
|
||||
|
||||
## TROUBLESHOOTING
|
||||
|
||||
**Service won't start:**
|
||||
```bash
|
||||
docker service ps rongorongo_rongorongo --no-trunc
|
||||
docker inspect $(docker ps -q --filter name=rongorongo) 2>/dev/null | grep -A5 '"State"'
|
||||
```
|
||||
|
||||
**Traefik not routing:**
|
||||
```bash
|
||||
# Check Traefik logs for rongorongo
|
||||
docker service logs traefik_traefik 2>&1 | grep -i rongorongo | tail -20
|
||||
# Confirm labels are applied
|
||||
docker service inspect rongorongo_rongorongo | grep -A2 Labels
|
||||
```
|
||||
|
||||
**TLS cert not issuing:**
|
||||
```bash
|
||||
# Wildcard cert should already cover rongorongo.hobbs.farm
|
||||
# If not, check Cloudflare DNS API token has Zone:Read and DNS:Edit
|
||||
docker service logs traefik_traefik 2>&1 | grep -i "acme\|certificate\|cloudflare" | tail -20
|
||||
```
|
||||
|
||||
**API calls blocked (CORS/CSP):**
|
||||
The nginx.conf includes a Content-Security-Policy header that allows
|
||||
`connect-src https://api.anthropic.com`. If you see CORS errors in the browser
|
||||
console, verify the CSP header is present:
|
||||
```bash
|
||||
curl -I https://rongorongo.hobbs.farm/ | grep -i content-security
|
||||
```
|
||||
|
||||
**App loads but AI calls fail:**
|
||||
- Check API key is entered correctly in the sidebar
|
||||
- Anthropic API key must start with `sk-ant-`
|
||||
- Confirm outbound HTTPS to api.anthropic.com is not blocked by firewall rules
|
||||
- The app sends `anthropic-dangerous-direct-browser-access: true` header which
|
||||
is required for direct browser API access
|
||||
|
||||
---
|
||||
|
||||
## END OF DEPLOYMENT GUIDE
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# CSP — allows Anthropic API calls and Google Fonts
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self' https://api.anthropic.com; worker-src blob:;" always;
|
||||
|
||||
# Gzip
|
||||
gzip on;
|
||||
gzip_types text/html text/css application/javascript;
|
||||
gzip_min_length 1024;
|
||||
|
||||
# Health endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "ok\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
expires -1;
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||
}
|
||||
|
||||
# Cache static assets aggressively if you add any
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|woff2)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
rongorongo:
|
||||
image: registry.hobbs.farm/rongorongo:latest
|
||||
networks:
|
||||
- traefik-public
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 10s
|
||||
max_attempts: 3
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
failure_action: rollback
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
|
||||
# HTTP → HTTPS redirect
|
||||
- "traefik.http.routers.rongorongo-http.rule=Host(`rongorongo.hobbs.farm`)"
|
||||
- "traefik.http.routers.rongorongo-http.entrypoints=web"
|
||||
- "traefik.http.routers.rongorongo-http.middlewares=redirect-to-https"
|
||||
|
||||
# HTTPS router
|
||||
- "traefik.http.routers.rongorongo.rule=Host(`rongorongo.hobbs.farm`)"
|
||||
- "traefik.http.routers.rongorongo.entrypoints=websecure"
|
||||
- "traefik.http.routers.rongorongo.tls=true"
|
||||
- "traefik.http.routers.rongorongo.tls.certresolver=cloudflare"
|
||||
|
||||
# Service
|
||||
- "traefik.http.services.rongorongo.loadbalancer.server.port=80"
|
||||
|
||||
# Middlewares
|
||||
- "traefik.http.routers.rongorongo.middlewares=rongorongo-headers"
|
||||
- "traefik.http.middlewares.rongorongo-headers.headers.customResponseHeaders.X-Robots-Tag=noindex,nofollow"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://localhost/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -114,7 +114,7 @@ services:
|
||||
- /mnt/docker-data/configs/nzbhydra:/config
|
||||
ports:
|
||||
- "5076:5076"
|
||||
mem_limit: 512M
|
||||
mem_limit: 768M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:5076/ || exit 1"]
|
||||
interval: 30s
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Overlay on the friday-voice app image that adds the docker CLI + a wrapper so
|
||||
# the container can reach the hermes agent via `docker exec hermes hermes ...`.
|
||||
# Runs as root to access the mounted /var/run/docker.sock.
|
||||
FROM friday-voice:latest
|
||||
|
||||
USER root
|
||||
|
||||
# Static docker *client* only (~small), for `docker exec` into the hermes container.
|
||||
RUN curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-27.5.1.tgz \
|
||||
| tar -xz --strip-components=1 -C /usr/local/bin docker/docker \
|
||||
&& chmod +x /usr/local/bin/docker \
|
||||
&& docker --version
|
||||
|
||||
COPY hermes-wrap /usr/local/bin/hermes-wrap
|
||||
RUN chmod +x /usr/local/bin/hermes-wrap
|
||||
|
||||
# Keep the base image's CMD (uvicorn friday_voice.app:app :8320).
|
||||
@@ -0,0 +1,49 @@
|
||||
# FRIDAY Voice on .150 (Docker Desktop) — replaces VoiceCoach.
|
||||
# STT/TTS via local GPU speaches (:8000). Reasoning via the hermes container
|
||||
# (invoked through the docker-exec wrapper). Runs as root for socket access.
|
||||
services:
|
||||
friday-voice:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.hermes
|
||||
image: friday-voice-hermes:latest
|
||||
container_name: friday-voice
|
||||
restart: unless-stopped
|
||||
user: "0:0"
|
||||
ports:
|
||||
- "8320:8320"
|
||||
volumes:
|
||||
- friday-voice-data:/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- DATA_DIR=/data
|
||||
- SPEACHES_URL=http://host.docker.internal:8000
|
||||
- HERMES_COMMAND=/usr/local/bin/hermes-wrap
|
||||
- HERMES_SOURCE=friday-voice
|
||||
- WHISPER_MODEL=deepdml/faster-whisper-large-v3-turbo-ct2
|
||||
- TTS_MODEL=speaches-ai/Kokoro-82M-v1.0-ONNX
|
||||
- TTS_VOICE=af_nova
|
||||
- SPEAKER_CONFIRMED_THRESHOLD=0.85
|
||||
- SPEAKER_PROBABLE_THRESHOLD=0.70
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8320/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "20m"
|
||||
max-file: "3"
|
||||
|
||||
volumes:
|
||||
friday-voice-data:
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: friday-voice
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
# friday-voice invokes: hermes-wrap chat -q "<text>" --source friday-voice
|
||||
# We forward into the hermes container and add -Q (quiet) so only the final
|
||||
# response text is returned (no banner/spinner/box art) for clean TTS.
|
||||
exec docker exec hermes hermes "$@" -Q
|
||||
@@ -36,6 +36,7 @@ services:
|
||||
- HOME=/home/glyptic
|
||||
- USER=glyptic
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||
- WIKIJS_API_KEY=${WIKIJS_API_KEY}
|
||||
- OPENCLAW_STATE_DIR=/home/glyptic/.spirit
|
||||
- OPENCLAW_CONFIG_PATH=/home/glyptic/.spirit/openclaw.json
|
||||
@@ -43,6 +44,8 @@ services:
|
||||
- /mnt/infrastructure/spirit:/home/glyptic/.spirit
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /mnt/infrastructure/spirit/ssh:/root/.ssh:ro
|
||||
- /mnt/docker-data/configs/prometheus:/prometheus-config
|
||||
- /mnt/docker-data/configs/alertmanager:/alertmanager-config
|
||||
command: ["sh", "-c", "mkdir -p /home/glyptic && node --max-old-space-size=1500 /usr/local/lib/node_modules/openclaw/dist/index.js gateway --allow-unconfigured"]
|
||||
mem_limit: 3G
|
||||
memswap_limit: 3G
|
||||
@@ -52,74 +55,3 @@ services:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
bob:
|
||||
image: margaret-openclaw:latest
|
||||
container_name: bob
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
user: "0:0"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- HOME=/home/glyptic
|
||||
- USER=glyptic
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- OPENCLAW_STATE_DIR=/home/glyptic/.bob
|
||||
- OPENCLAW_CONFIG_PATH=/home/glyptic/.bob/openclaw.json
|
||||
volumes:
|
||||
- /mnt/infrastructure/bob:/home/glyptic/.bob
|
||||
command: ["sh", "-c", "mkdir -p /home/glyptic && node --max-old-space-size=1500 /usr/local/lib/node_modules/openclaw/dist/index.js gateway --allow-unconfigured"]
|
||||
mem_limit: 2G
|
||||
memswap_limit: 2G
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
clippy:
|
||||
image: margaret-openclaw:latest
|
||||
container_name: clippy
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
user: "0:0"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- HOME=/home/glyptic
|
||||
- USER=glyptic
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- OPENCLAW_STATE_DIR=/home/glyptic/.clippy
|
||||
- OPENCLAW_CONFIG_PATH=/home/glyptic/.clippy/openclaw.json
|
||||
volumes:
|
||||
- /mnt/infrastructure/clippy:/home/glyptic/.clippy
|
||||
command: ["sh", "-c", "mkdir -p /home/glyptic && node --max-old-space-size=1500 /usr/local/lib/node_modules/openclaw/dist/index.js gateway --allow-unconfigured"]
|
||||
mem_limit: 2G
|
||||
memswap_limit: 2G
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
travis:
|
||||
image: travis-nemoclaw:latest
|
||||
container_name: travis
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
user: "0:0"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- HOME=/home/glyptic
|
||||
- USER=glyptic
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- OPENCLAW_STATE_DIR=/home/glyptic/.travis
|
||||
- OPENCLAW_CONFIG_PATH=/home/glyptic/.travis/openclaw.json
|
||||
volumes:
|
||||
- /mnt/infrastructure/travis:/home/glyptic/.travis
|
||||
command: ["sh", "-c", "mkdir -p /home/glyptic && openclaw plugins install /opt/nemoclaw 2>/dev/null; node --max-old-space-size=1500 /usr/local/lib/node_modules/openclaw/dist/index.js gateway --allow-unconfigured"]
|
||||
mem_limit: 2G
|
||||
memswap_limit: 2G
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
@@ -17,7 +17,7 @@ services:
|
||||
litellm-db:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:4000/health || exit 1"]
|
||||
test: ["CMD-SHELL", "python3 -c \"import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:4000/health/liveliness',timeout=5).status==200 else 1)\""]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
services:
|
||||
open-webui:
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
container_name: open-webui
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3010:8080"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- TZ=America/Chicago
|
||||
- OLLAMA_BASE_URL=http://192.168.86.196:11434
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/openwebui:/app/backend/data
|
||||
mem_limit: 1G
|
||||
memswap_limit: 1G
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -sf http://localhost:8080/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
@@ -6,24 +6,13 @@ services:
|
||||
ports:
|
||||
- "5678:5678"
|
||||
environment:
|
||||
- DB_TYPE=postgresdb
|
||||
- DB_POSTGRESDB_HOST=n8n-db
|
||||
- DB_POSTGRESDB_PORT=5432
|
||||
- DB_POSTGRESDB_DATABASE=n8n
|
||||
- DB_POSTGRESDB_USER=n8n
|
||||
- DB_POSTGRESDB_PASSWORD=${N8N_DB_PASSWORD}
|
||||
- N8N_PORT=5678
|
||||
- GENERIC_TIMEZONE=America/Chicago
|
||||
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
|
||||
- N8N_SECURE_COOKIE=false
|
||||
- NODE_OPTIONS=--max-old-space-size=1500
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/n8n:/home/node/.n8n
|
||||
depends_on:
|
||||
n8n-db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- n8n-internal
|
||||
mem_limit: 512M
|
||||
memswap_limit: 512M
|
||||
- n8n_data:/home/node/.n8n
|
||||
mem_limit: 2g
|
||||
memswap_limit: 2g
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost:5678/healthz || exit 1"]
|
||||
interval: 30s
|
||||
@@ -36,35 +25,6 @@ services:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
n8n-db:
|
||||
image: postgres:15-alpine
|
||||
container_name: n8n-db
|
||||
restart: unless-stopped
|
||||
user: "1024:100"
|
||||
environment:
|
||||
- POSTGRES_DB=n8n
|
||||
- POSTGRES_USER=n8n
|
||||
- POSTGRES_PASSWORD=${N8N_DB_PASSWORD}
|
||||
volumes:
|
||||
- /mnt/docker-data/databases/n8n-db:/var/lib/postgresql/data
|
||||
networks:
|
||||
- n8n-internal
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
mem_limit: 256M
|
||||
memswap_limit: 256M
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
n8n-internal:
|
||||
driver: bridge
|
||||
n8n_data:
|
||||
external: true
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
ANTHROPIC_API_KEY=sk-ant-...
|
||||
MATRIX_ACCESS_TOKEN=syt_...
|
||||
@@ -1,38 +0,0 @@
|
||||
FROM node:20-bookworm
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 python3-pip python3-venv python3-full \
|
||||
git curl jq openssh-client iputils-ping net-tools \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install uv (Python package manager used by some MCP servers)
|
||||
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
# Install OpenClaw globally
|
||||
RUN npm install -g openclaw@latest
|
||||
|
||||
# Clone NetClaw
|
||||
WORKDIR /opt/netclaw
|
||||
RUN git clone https://github.com/automateyournetwork/netclaw.git .
|
||||
|
||||
# Create openclaw home directory
|
||||
RUN mkdir -p /root/.openclaw/workspace
|
||||
|
||||
# Copy NetClaw workspace config into openclaw home
|
||||
RUN cp -r workspace/skills /root/.openclaw/workspace/ 2>/dev/null || true
|
||||
RUN cp config/openclaw.json /root/.openclaw/openclaw.json 2>/dev/null || true
|
||||
|
||||
# Install Python dependencies for core MCP servers
|
||||
RUN python3 -m venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
RUN pip install --no-cache-dir pyats genie netmiko paramiko requests pyyaml jinja2 2>/dev/null || true
|
||||
|
||||
# Create entrypoint script
|
||||
COPY entrypoint.sh /opt/entrypoint.sh
|
||||
RUN chmod +x /opt/entrypoint.sh
|
||||
|
||||
EXPOSE 1179
|
||||
|
||||
ENTRYPOINT ["/opt/entrypoint.sh"]
|
||||
@@ -1,20 +0,0 @@
|
||||
services:
|
||||
netclaw:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: netclaw-aaron
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- MATRIX_ACCESS_TOKEN=${MATRIX_ACCESS_TOKEN}
|
||||
- MATRIX_HOMESERVER=http://192.168.86.192:8008
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/netclaw:/root/.openclaw/data
|
||||
mem_limit: 2G
|
||||
memswap_limit: 2G
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "3"
|
||||
@@ -1,72 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Configure OpenClaw environment
|
||||
mkdir -p /root/.openclaw
|
||||
|
||||
# Write .env with API key
|
||||
cat > /root/.openclaw/.env << EOF
|
||||
ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
EOF
|
||||
|
||||
# Write openclaw config for gateway mode
|
||||
cat > /root/.openclaw/openclaw.json << 'OCEOF'
|
||||
{
|
||||
"mode": "gateway",
|
||||
"provider": "anthropic",
|
||||
"model": "claude-sonnet-4-6",
|
||||
"fallbackModel": "claude-haiku-4-5-20251001",
|
||||
"name": "Aaron",
|
||||
"identity": "Aaron - NetClaw AI Network Engineering Agent"
|
||||
}
|
||||
OCEOF
|
||||
|
||||
# Copy workspace skills if they exist
|
||||
if [ -d /opt/netclaw/workspace/skills ]; then
|
||||
cp -r /opt/netclaw/workspace/skills /root/.openclaw/workspace/ 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Copy personality/identity docs
|
||||
for doc in SOUL.md AGENTS.md IDENTITY.md USER.md TOOLS.md; do
|
||||
if [ -f /opt/netclaw/$doc ]; then
|
||||
cp /opt/netclaw/$doc /root/.openclaw/ 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
# Send Matrix DM to Jeremy to announce availability
|
||||
if [ -n "$MATRIX_ACCESS_TOKEN" ] && [ -n "$MATRIX_HOMESERVER" ]; then
|
||||
echo "Sending Matrix DM to Jeremy..."
|
||||
|
||||
# Create a direct message room with Jeremy
|
||||
ROOM_RESPONSE=$(curl -sf -X POST "${MATRIX_HOMESERVER}/_matrix/client/r0/createRoom" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${MATRIX_ACCESS_TOKEN}" \
|
||||
-d "{
|
||||
\"is_direct\": true,
|
||||
\"invite\": [\"@jeremy:hobbs.farm\"],
|
||||
\"preset\": \"trusted_private_chat\"
|
||||
}" 2>/dev/null || echo "")
|
||||
|
||||
ROOM_ID=$(echo "$ROOM_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('room_id',''))" 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$ROOM_ID" ]; then
|
||||
# Send the message
|
||||
curl -sf -X PUT "${MATRIX_HOMESERVER}/_matrix/client/r0/rooms/${ROOM_ID}/send/m.room.message/$(date +%s%N)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer ${MATRIX_ACCESS_TOKEN}" \
|
||||
-d "{
|
||||
\"msgtype\": \"m.text\",
|
||||
\"body\": \"Hey Jeremy - Aaron here. I'm online and ready to help with network engineering tasks. I'm running NetClaw with CCIE-level networking skills. Ask me about device health checks, routing analysis, security audits, or anything network-related.\"
|
||||
}" 2>/dev/null || echo "Failed to send DM"
|
||||
echo "DM sent to @jeremy:hobbs.farm in room $ROOM_ID"
|
||||
else
|
||||
echo "Warning: Could not create DM room with Jeremy. Response: $ROOM_RESPONSE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Start OpenClaw gateway
|
||||
echo "Starting NetClaw (Aaron) gateway..."
|
||||
exec openclaw gateway 2>&1 || {
|
||||
echo "OpenClaw gateway failed to start, keeping container alive for debugging..."
|
||||
tail -f /dev/null
|
||||
}
|
||||
@@ -5,6 +5,8 @@ http:
|
||||
service: yoda
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- security-headers-allow-frames
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -13,6 +15,8 @@ http:
|
||||
service: mess
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- security-headers
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -21,6 +25,8 @@ http:
|
||||
service: mess
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- security-headers
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -29,6 +35,8 @@ http:
|
||||
service: wellknown
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- security-headers
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -37,6 +45,8 @@ http:
|
||||
service: solo
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- security-headers-allow-frames
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -45,6 +55,8 @@ http:
|
||||
service: vault
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- security-headers
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -53,9 +65,41 @@ http:
|
||||
service: git
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- security-headers
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
middlewares:
|
||||
security-headers:
|
||||
headers:
|
||||
stsSeconds: 31536000
|
||||
stsIncludeSubdomains: true
|
||||
stsPreload: true
|
||||
contentTypeNosniff: true
|
||||
frameDeny: true
|
||||
referrerPolicy: "strict-origin-when-cross-origin"
|
||||
permissionsPolicy: "camera=(), microphone=(), geolocation=(), payment=()"
|
||||
browserXssFilter: true
|
||||
customResponseHeaders:
|
||||
X-Powered-By: ""
|
||||
Server: ""
|
||||
|
||||
security-headers-allow-frames:
|
||||
headers:
|
||||
stsSeconds: 31536000
|
||||
stsIncludeSubdomains: true
|
||||
stsPreload: true
|
||||
contentTypeNosniff: true
|
||||
frameDeny: false
|
||||
customFrameOptionsValue: "SAMEORIGIN"
|
||||
referrerPolicy: "strict-origin-when-cross-origin"
|
||||
permissionsPolicy: "camera=(), microphone=(), geolocation=(), payment=()"
|
||||
browserXssFilter: true
|
||||
customResponseHeaders:
|
||||
X-Powered-By: ""
|
||||
Server: ""
|
||||
|
||||
services:
|
||||
yoda:
|
||||
loadBalancer:
|
||||
|
||||
@@ -6,6 +6,7 @@ services:
|
||||
environment:
|
||||
- DOMAIN=https://vault.hobbs.farm
|
||||
- SIGNUPS_ALLOWED=false
|
||||
- ADMIN_TOKEN=${ADMIN_TOKEN}
|
||||
volumes:
|
||||
- /mnt/docker-data/configs/vaultwarden:/data
|
||||
ports:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
# Rename bart → hf-pdocker-03 Design
|
||||
|
||||
**Date:** 2026-03-11
|
||||
**Server:** 192.168.86.167 (currently "bart", becoming "hf-pdocker-03")
|
||||
|
||||
## Overview
|
||||
|
||||
Rename the server "bart" to "hf-pdocker-03" across the entire infrastructure — hostname, repository files, NAS paths, Docker Swarm node, monitoring, and documentation. The IP address 192.168.86.167 does not change.
|
||||
|
||||
## Scope
|
||||
|
||||
### Server-Level
|
||||
|
||||
- Change hostname from `bart` to `hf-pdocker-03` via `hostnamectl`
|
||||
- Update `/etc/hosts` on the server itself
|
||||
- Docker Swarm: drain → remove → change hostname → re-join with new name
|
||||
- Swarm global services (Telegraf, Node-exporter) auto-reschedule on new node name
|
||||
|
||||
### NAS Path Rename
|
||||
|
||||
- `/mnt/docker-data/compose/bart/` → `/mnt/docker-data/compose/hf-pdocker-03/`
|
||||
- **Onyx exception:** `compose/hf-pdocker-03/onyx/docker-compose.yml` has hardcoded volume mounts referencing `/mnt/docker-data/compose/bart/onyx`. These must be updated in the compose file AND the Onyx containers redeployed after the NAS rename.
|
||||
- All other running containers are unaffected (they don't reference the compose file path at runtime)
|
||||
|
||||
### Repository Files
|
||||
|
||||
**Directory rename:**
|
||||
- `compose/bart/` → `compose/hf-pdocker-03/` (14 files across 8 subdirectories)
|
||||
- Active services: `media/`, `openvas/`, `watchtower/`, `onyx/`
|
||||
- Stale failover dirs (preserved for reference): `smarthome/`, `matrix/`, `guacamole/`, `ai-agents/`
|
||||
|
||||
**File inventory (all files containing "bart"):**
|
||||
|
||||
| File | Type | Action |
|
||||
|------|------|--------|
|
||||
| `CLAUDE.md` | Config | Must update — service distribution, storage, internal services |
|
||||
| `stacks/monitoring/prometheus.yml` | Config | Must update — hostname label, job name `http-bart` → `http-hf-pdocker-03` |
|
||||
| `stacks/monitoring/alerts.yml` | Config | Must update — PromQL `hostname="bart"` matchers. Review: elevated 90% CPU threshold was for Frigate, which is now on hf-pdocker-01, not this node. Consider reverting to standard 80% threshold. |
|
||||
| `stacks/monitoring/matrix-receiver-config.yml` | Config | Must update — "bart" in comment. Note: file appears stale (still references old IPs). Update comment only. |
|
||||
| `stacks/frigate/docker-compose.yml` | Stale Swarm def | Update for consistency — has `node.hostname == bart` placement constraint. This is an unused Swarm stack (Frigate runs as standalone Compose on hf-pdocker-01). |
|
||||
| `compose/bart/onyx/docker-compose.yml` | Active compose | **CRITICAL** — has hardcoded volume mount `/mnt/docker-data/compose/bart/onyx:/mnt/gitea_patch:ro`. Must update path AND redeploy Onyx containers. |
|
||||
| `.claude/agents/gitea-backup.md` | Agent def | Must update |
|
||||
| `.claude/agents/alert-manager.md` | Agent def | Must update |
|
||||
| `.claude/agents/documentation-keeper.md` | Agent def | Must update |
|
||||
| `DEPLOYMENT_SUMMARY.md` | Doc | Must update |
|
||||
| `docs/plans/2026-03-08-hf-pdocker-01-phase2-migration.md` | Historical | Update for consistency |
|
||||
| `docs/plans/2026-03-08-hf-pdocker-01-rebuild.md` | Historical | Update for consistency |
|
||||
| `docs/plans/2026-03-07-frigate-notify-v2-plan.md` | Historical | Update for consistency |
|
||||
| `docs/plans/2026-03-05-farm-manager.md` | Historical | Update for consistency |
|
||||
| `docs/plans/2026-03-05-farm-manager-design.md` | Historical | Update for consistency |
|
||||
|
||||
**Memory/auto-memory files:**
|
||||
- `MEMORY.md` — ~30 references to "bart"
|
||||
- `memory/migration-details.md` — bart references
|
||||
|
||||
### Documentation
|
||||
|
||||
- Wiki pages — infrastructure overview, service access, changelog
|
||||
|
||||
## Order of Operations
|
||||
|
||||
1. **Repo file updates** — rename `compose/bart/` directory, update all file contents (including Onyx compose volume path). No service impact yet.
|
||||
2. **SCP updated Onyx compose file** to NAS at the current path (`/mnt/docker-data/compose/bart/onyx/docker-compose.yml`) with the NEW volume path (`/mnt/docker-data/compose/hf-pdocker-03/onyx`). This prepares for the rename.
|
||||
3. **NAS path rename** — `mv /mnt/docker-data/compose/bart /mnt/docker-data/compose/hf-pdocker-03` (via SSH). Atomic on NFS.
|
||||
4. **Redeploy Onyx** — `cd /mnt/docker-data/compose/hf-pdocker-03/onyx && docker compose up -d` to pick up the new volume mount path.
|
||||
5. **Server hostname change** — `sudo hostnamectl set-hostname hf-pdocker-03` + update `/etc/hosts` on 192.168.86.167.
|
||||
6. **Docker Swarm re-join** — on hf-pdocker-01: drain old bart node, remove it. On 192.168.86.167: leave swarm, re-join with new hostname. Global services auto-schedule. ~30s gap.
|
||||
7. **Prometheus + alerts** — SCP updated prometheus.yml and alerts.yml, hot-reload.
|
||||
8. **Update /etc/hosts on other nodes** — if hf-pdocker-01 or hf-pdocker-02 have "bart" in /etc/hosts, update to hf-pdocker-03.
|
||||
9. **MEMORY.md + Wiki** — update all documentation.
|
||||
10. **Commit + push to Gitea**.
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Low risk:** Running Compose services (Jellyfin, Sonarr, Radarr, etc.) are unaffected — containers keep running.
|
||||
- **Onyx restart required:** Onyx containers need redeployment after NAS rename to pick up new volume mount path. Brief downtime for Onyx (~30s).
|
||||
- **Low risk:** Swarm global services have ~30s gap during node re-join.
|
||||
- **No IP changes:** All service-to-service communication uses IPs, not hostnames. Nothing breaks at the network level.
|
||||
- **NAS path rename is atomic:** `mv` on NFS is atomic for same-filesystem renames.
|
||||
|
||||
## Verification
|
||||
|
||||
After completion:
|
||||
- `hostname` on 192.168.86.167 returns `hf-pdocker-03`
|
||||
- `docker node ls` on hf-pdocker-01 shows `hf-pdocker-03` instead of `bart`
|
||||
- All containers on 192.168.86.167 still running (`docker ps`)
|
||||
- Onyx containers running with new volume mount path
|
||||
- Prometheus targets for hf-pdocker-03 are UP
|
||||
- `grep -r bart` in repo returns zero matches (excluding git history)
|
||||
@@ -4,6 +4,8 @@ http:
|
||||
rule: "Host(`git.hobbs.farm`)"
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- security-headers
|
||||
service: gitea
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
http:
|
||||
routers:
|
||||
openwebui:
|
||||
rule: "Host(`open.hobbs.farm`)"
|
||||
entryPoints:
|
||||
- websecure
|
||||
service: openwebui
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
openwebui-http:
|
||||
rule: "Host(`open.hobbs.farm`)"
|
||||
entryPoints:
|
||||
- web
|
||||
middlewares:
|
||||
- openwebui-https-redirect
|
||||
service: openwebui
|
||||
middlewares:
|
||||
openwebui-https-redirect:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
permanent: true
|
||||
services:
|
||||
openwebui:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://192.168.86.192:3010"
|
||||
@@ -0,0 +1,60 @@
|
||||
http:
|
||||
middlewares:
|
||||
security-headers:
|
||||
headers:
|
||||
# HSTS - force browsers to use HTTPS for 1 year, include subdomains
|
||||
stsSeconds: 31536000
|
||||
stsIncludeSubdomains: true
|
||||
stsPreload: true
|
||||
# Prevent MIME-type sniffing
|
||||
contentTypeNosniff: true
|
||||
# Block rendering in iframes from other origins
|
||||
frameDeny: true
|
||||
# Referrer policy - send origin only on cross-origin requests
|
||||
referrerPolicy: "strict-origin-when-cross-origin"
|
||||
# Permissions policy - restrict browser features
|
||||
permissionsPolicy: "camera=(), microphone=(), geolocation=(), payment=()"
|
||||
# Prevent browsers from caching sensitive responses
|
||||
browserXssFilter: true
|
||||
# Remove server identity header
|
||||
customResponseHeaders:
|
||||
X-Powered-By: ""
|
||||
Server: ""
|
||||
|
||||
# Variant that allows microphone (for VoiceCoach)
|
||||
security-headers-allow-mic:
|
||||
headers:
|
||||
stsSeconds: 31536000
|
||||
stsIncludeSubdomains: true
|
||||
stsPreload: true
|
||||
contentTypeNosniff: true
|
||||
frameDeny: true
|
||||
referrerPolicy: "strict-origin-when-cross-origin"
|
||||
permissionsPolicy: "camera=(), microphone=(self), geolocation=(), payment=()"
|
||||
browserXssFilter: true
|
||||
customResponseHeaders:
|
||||
X-Powered-By: ""
|
||||
Server: ""
|
||||
|
||||
# BasicAuth for sensitive services (Guacamole)
|
||||
solo-basicauth:
|
||||
basicAuth:
|
||||
users:
|
||||
- "glyptic:$2y$05$6AcmRIjMX6YrxY3fACeDhui4ghBGZtjmiPHbzWDb.qZ.8Fdip4GM."
|
||||
realm: "Restricted"
|
||||
|
||||
# Variant that allows framing (for Guacamole, Home Assistant embeds)
|
||||
security-headers-allow-frames:
|
||||
headers:
|
||||
stsSeconds: 31536000
|
||||
stsIncludeSubdomains: true
|
||||
stsPreload: true
|
||||
contentTypeNosniff: true
|
||||
frameDeny: false
|
||||
customFrameOptionsValue: "SAMEORIGIN"
|
||||
referrerPolicy: "strict-origin-when-cross-origin"
|
||||
permissionsPolicy: "camera=(), microphone=(), geolocation=(), payment=()"
|
||||
browserXssFilter: true
|
||||
customResponseHeaders:
|
||||
X-Powered-By: ""
|
||||
Server: ""
|
||||
@@ -0,0 +1,18 @@
|
||||
tls:
|
||||
options:
|
||||
default:
|
||||
minVersion: VersionTLS12
|
||||
cipherSuites:
|
||||
# TLS 1.3 ciphers are always enabled and not configurable
|
||||
# These are the strong TLS 1.2 ciphers
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
|
||||
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
||||
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
|
||||
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
|
||||
curvePreferences:
|
||||
- X25519
|
||||
- CurveP256
|
||||
- CurveP384
|
||||
sniStrict: true
|
||||
@@ -4,6 +4,8 @@ http:
|
||||
rule: "Host(`vault.hobbs.farm`)"
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- security-headers
|
||||
service: vault
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
# Deployment Brief: vLLM serving Hermes 4.3 for OpenClaw/Pluto
|
||||
|
||||
**Audience:** Claude Code, executing autonomously on Jeremy's homelab.
|
||||
**Goal:** Stand up a vLLM OpenAI-compatible inference server hosting NousResearch Hermes 4.3, with native Hermes tool-call parsing, and wire it into the existing OpenClaw gateway as Pluto's reasoning model.
|
||||
|
||||
---
|
||||
|
||||
## 0. Why this exists (context you need)
|
||||
|
||||
We tried serving Hermes 4.3 36B (Q4_K_M GGUF) through Ollama on the Windows box. The model loads and runs at ~98% GPU after forcing `num_gpu 64`, but **structured tool calling fails** — Ollama returns empty `content` and no `tool_calls` array, because Ollama's parser doesn't extract Hermes's `<tool_call>` XML format into the OpenAI-structured field on a self-imported GGUF. This is a known, recurring Ollama sharp edge.
|
||||
|
||||
vLLM solves this directly: the `--tool-call-parser hermes` flag parses the XML `<tool_call>` format natively into the OpenAI-compatible response schema, no post-processing. That is the entire reason for this migration.
|
||||
|
||||
**Do not** spend effort trying to fix the Ollama path. The decision to move to vLLM is final.
|
||||
|
||||
---
|
||||
|
||||
## 1. Hard constraints — read before doing anything
|
||||
|
||||
1. **The GPU is in the Windows desktop, NOT the Linux swarm.**
|
||||
- Host: `DESKTOP-5AHPRKR` — ASUS Z790 (TUF GAMING Z790-PLUS WIFI), i7-13700KF, 32GB RAM, **RTX 3090 (24GB VRAM)**.
|
||||
- The Docker Swarm nodes (`hf-pdocker-01`, `hf-pdocker-02`, `bart`) are CPU-only and **cannot** run a 36B model usefully. Do not attempt to schedule vLLM onto the swarm.
|
||||
- vLLM is **Linux/CUDA only**. On Windows this means running inside **WSL2** (Ubuntu) with CUDA passthrough, using the official vLLM Docker image via Docker Desktop's WSL2 backend, OR a native WSL2 Python install. Docker image is preferred for reproducibility.
|
||||
|
||||
2. **24GB VRAM is the binding limit.** Hermes 4.3 36B in FP16 is ~72GB — impossible here. Options, in order of preference:
|
||||
- **(A)** A pre-quantized **AWQ or GPTQ 4-bit** build of Hermes 4.3 36B (~18-20GB weights). Tight but workable with `--gpu-memory-utilization 0.95`, `--max-model-len 8192`, and `--enforce-eager` (skip CUDA graph capture to save VRAM). **First choice if a quant exists.**
|
||||
- **(B)** If no 36B AWQ/GPTQ exists, fall back to **Hermes 4.3 in a smaller parameter size** if Nous published one (e.g. a 14B), served in FP16 or AWQ — fits comfortably, full context, fast. **Strongly preferred over fighting the 36B on 24GB.**
|
||||
- **(C)** Last resort: serve the **GGUF Q4** through vLLM (vLLM supports GGUF). Weaker than AWQ in vLLM and partially defeats the point, but the hermes parser still works. Only if A and B are unavailable.
|
||||
|
||||
3. **WSL2 will fight you for RAM.** Jeremy's box already had `VmmemWSL` consuming ~13GB. There may now be a `C:\Users\Jeremy\.wslconfig` capping it. Check it. vLLM's model loading stages through system RAM, so ensure WSL2's memory cap is high enough (≥24GB) for the load, OR raise it temporarily. Coordinate: Docker Desktop restarts WSL2 automatically, so don't rely on `wsl --shutdown` to free RAM permanently.
|
||||
|
||||
4. **Stop the Ollama server before testing** to avoid port/VRAM contention. Ollama holds VRAM while a model is resident. `ollama stop hermes` and confirm `ollama ps` is empty, or quit the Ollama app entirely. vLLM and Ollama cannot both hold the 3090 at once.
|
||||
|
||||
---
|
||||
|
||||
## 2. Pre-flight verification (do these first, report results)
|
||||
|
||||
Run inside WSL2 (Ubuntu):
|
||||
|
||||
```bash
|
||||
# Confirm CUDA reaches WSL2 and sees the 3090
|
||||
nvidia-smi
|
||||
# Expect: "NVIDIA GeForce RTX 3090", 24576 MiB total, driver present.
|
||||
# If this fails, CUDA-on-WSL2 isn't set up — STOP and report.
|
||||
|
||||
# Confirm Docker can see the GPU
|
||||
docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi
|
||||
# Expect same output from inside a container.
|
||||
```
|
||||
|
||||
If `nvidia-smi` works on the Windows side but not in WSL2, the fix is installing the WSL2 CUDA toolkit (NOT a full driver — the Windows driver provides the WSL2 GPU stub). Report before proceeding so Jeremy can confirm.
|
||||
|
||||
Also check current RAM headroom and the WSL config:
|
||||
|
||||
```bash
|
||||
cat /mnt/c/Users/Jeremy/.wslconfig 2>/dev/null || echo "no .wslconfig"
|
||||
free -h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Model selection (decide, then download)
|
||||
|
||||
**Step 3a — Find the best-fitting quant.** Search HuggingFace for, in priority order:
|
||||
1. `Hermes-4.3-36B` AWQ (search orgs: NousResearch, and community quantizers like `casperhansen`, `solidrust`, `bartowski` for AWQ/GPTQ variants)
|
||||
2. A smaller Hermes 4.3 (14B-class) if one exists — this is genuinely the easier, faster path on 24GB
|
||||
3. GGUF Q4 as fallback (we already have `hf.co/NousResearch/Hermes-4.3-36B-GGUF:Q4_K_M`)
|
||||
|
||||
**Report what you find and which option (A/B/C from §1.2) you're proceeding with before downloading 20GB.** If a 36B AWQ exists, confirm its repo name. If the only realistic fit is a smaller Hermes 4.3, say so — Jeremy explicitly prefers a model that runs 100% on-GPU with full context over grinding a 36B into a too-small card.
|
||||
|
||||
**Step 3b — Download to a persistent WSL2 path** (not the container, so it survives restarts):
|
||||
|
||||
```bash
|
||||
mkdir -p ~/models
|
||||
pip install -U "huggingface_hub[cli]"
|
||||
huggingface-cli download <RESOLVED_REPO_ID> --local-dir ~/models/hermes-4.3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Launch vLLM (Docker, OpenAI-compatible)
|
||||
|
||||
Create `~/vllm-hermes/docker-compose.yml`. Template for **Option A (36B AWQ)** — adjust `--quantization`, `--model`, and `--max-model-len` per the resolved model:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
vllm-hermes:
|
||||
image: vllm/vllm-openai:latest
|
||||
container_name: vllm-hermes
|
||||
runtime: nvidia
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ~/models/hermes-4.3:/models/hermes-4.3
|
||||
ipc: host
|
||||
command: >
|
||||
--model /models/hermes-4.3
|
||||
--served-model-name hermes
|
||||
--quantization awq
|
||||
--gpu-memory-utilization 0.95
|
||||
--max-model-len 8192
|
||||
--enforce-eager
|
||||
--enable-auto-tool-choice
|
||||
--tool-call-parser hermes
|
||||
--host 0.0.0.0
|
||||
--port 8000
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Key flag rationale:
|
||||
- `--enable-auto-tool-choice --tool-call-parser hermes` — **the whole point.** Native Hermes XML tool-call parsing into OpenAI schema.
|
||||
- `--quantization awq` — set to `gptq` for GPTQ, omit entirely for GGUF (use `--quantization gguf` and point `--model` at the .gguf file) or for an unquantized smaller model.
|
||||
- `--enforce-eager` — disables CUDA graph capture, saving ~1-3GB VRAM. **Critical on the tight 24GB 36B path.** Remove it if you have headroom (smaller model) for a latency improvement.
|
||||
- `--max-model-len 8192` — keep KV cache small to fit. Raise cautiously if VRAM allows; watch for OOM on startup.
|
||||
- `--gpu-memory-utilization 0.95` — aggressive, appropriate for a dedicated inference box. Drop to 0.90 if the desktop also drives a display off the 3090 (it does — there's a monitor at 1680x1050 on it; budget ~0.5-1GB for the desktop, so 0.90 may be safer).
|
||||
|
||||
Launch and watch logs:
|
||||
|
||||
```bash
|
||||
cd ~/vllm-hermes
|
||||
docker compose up -d
|
||||
docker compose logs -f vllm-hermes
|
||||
# Wait for: "Application startup complete" and "Uvicorn running on http://0.0.0.0:8000"
|
||||
```
|
||||
|
||||
**If it OOMs on startup:** lower `--max-model-len` to 4096, confirm `--enforce-eager` is present, drop `--gpu-memory-utilization` to 0.90. If it still won't fit, that's the signal Option A is not viable on this card — fall back to Option B (smaller Hermes 4.3) and report.
|
||||
|
||||
---
|
||||
|
||||
## 5. Verification — the test that actually matters
|
||||
|
||||
This is the exact test that FAILED on Ollama. It must pass on vLLM.
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8000/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "hermes",
|
||||
"messages": [{"role": "user", "content": "What is 8347 multiplied by 291? Use the calculator tool."}],
|
||||
"tools": [{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calculator",
|
||||
"description": "Multiply two numbers",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"a": {"type": "number"}, "b": {"type": "number"}},
|
||||
"required": ["a", "b"]
|
||||
}
|
||||
}
|
||||
}],
|
||||
"tool_choice": "auto"
|
||||
}' | python3 -m json.tool
|
||||
```
|
||||
|
||||
**PASS criteria:** the response `choices[0].message` has a populated `tool_calls` array containing `function.name == "calculator"` and `function.arguments` parsing to `{"a": 8347, "b": 291}` (or equivalent), with `content` empty/null. That is structured tool calling working.
|
||||
|
||||
**FAIL:** if `tool_calls` is null/absent and the JSON is dumped in `content` as a string, the parser flag didn't take — verify `--tool-call-parser hermes` is in the running command (`docker compose logs` shows the args at boot) and that the model's chat template actually emits `<tool_call>` tags. Report the raw response.
|
||||
|
||||
Also sanity-check throughput:
|
||||
|
||||
```bash
|
||||
# Plain generation latency
|
||||
curl -s http://localhost:8000/v1/chat/completions -H "Content-Type: application/json" \
|
||||
-d '{"model":"hermes","messages":[{"role":"user","content":"Say hello in one sentence."}]}' \
|
||||
| python3 -m json.tool
|
||||
nvidia-smi # confirm VRAM occupancy and that it's the vLLM process
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Wire into OpenClaw (Pluto's brain)
|
||||
|
||||
OpenClaw is model-agnostic and speaks the OpenAI API. Point it at the vLLM endpoint. The vLLM server is on the Windows box at `http://<WINDOWS_LAN_IP>:8000/v1` (or `http://localhost:8000/v1` if OpenClaw runs on the same Windows box; confirm where Pluto's gateway actually runs — it may be on the swarm).
|
||||
|
||||
In OpenClaw's model/provider config:
|
||||
- **Base URL:** `http://<WINDOWS_LAN_IP>:8000/v1`
|
||||
- **Model name:** `hermes` (matches `--served-model-name`)
|
||||
- **API key:** any non-empty string (vLLM ignores it by default; use `EMPTY` or `sk-noauth`)
|
||||
|
||||
**Networking notes:**
|
||||
- vLLM in the WSL2 container binds `0.0.0.0:8000`, but WSL2 networking means the port must be reachable from the Windows host and the LAN. WSL2 usually auto-forwards localhost; for LAN access you may need a `netsh interface portproxy` rule on Windows forwarding the host's LAN IP:8000 to the WSL2 IP:8000, plus a Windows Firewall inbound rule for port 8000 **scoped to the LAN subnet only**. Set this up and verify from another machine with `curl http://<WINDOWS_LAN_IP>:8000/v1/models`.
|
||||
- Do **not** expose port 8000 to the internet. If remote access is wanted later, that goes behind Traefik with forward-auth on the swarm, tunneled to the Windows box — a separate task, not part of this brief.
|
||||
|
||||
After wiring, restart the OpenClaw gateway and run a Pluto interaction that should trigger a tool (whatever real tools Pluto has registered) to confirm end-to-end tool execution, not just the synthetic calculator test.
|
||||
|
||||
---
|
||||
|
||||
## 7. Operational notes / gotchas
|
||||
|
||||
- **Display + inference share the 3090.** The desktop's monitor runs off the same card. Under VRAM pressure this can cause display glitches or vLLM OOM. The `0.90` utilization setting accounts for this; if Jeremy ever runs the box headless (RDP only, which the earlier session showed — there was a Remote Display Adapter), more VRAM frees up and utilization can go to 0.95+.
|
||||
- **vLLM holds VRAM for its lifetime.** It does not unload like Ollama. That's expected and desirable for an always-on Pluto brain, but it means the 3090 is committed — no gaming / other GPU work while it runs.
|
||||
- **Auto-start on boot:** `restart: unless-stopped` handles container restarts, but WSL2 + Docker Desktop must themselves start on Windows login. For true always-on, enable Docker Desktop "Start on login" and consider WSL2 boot via Task Scheduler, OR (better long-term) move this whole stack to a dedicated headless Linux box with the 3090 if one becomes available. Flag this to Jeremy as a follow-up if always-on reliability matters for VoiceCoach.
|
||||
- **Context length is capped at 8192 here** for VRAM reasons. Hermes 4.3 supports far more (trained to 512K), but you can't afford the KV cache on 24GB. If Pluto workflows need longer context, that's a hardware conversation (bigger card / second GPU), not a config tweak.
|
||||
- **Version pinning:** `vllm/vllm-openai:latest` is convenient but pin to a specific tag for production stability once verified working, so an image update doesn't silently break the tool parser.
|
||||
|
||||
---
|
||||
|
||||
## 8. Report back to Jeremy with
|
||||
|
||||
1. `nvidia-smi` output from inside WSL2 (GPU visible y/n).
|
||||
2. Which model option (A/B/C) you proceeded with and the exact repo ID.
|
||||
3. The full JSON from the §5 calculator tool-call test (the pass/fail evidence).
|
||||
4. Measured tokens/sec and VRAM occupancy from `nvidia-smi`.
|
||||
5. Whether OpenClaw end-to-end tool execution worked with the real Pluto toolset.
|
||||
6. Any networking steps needed for LAN access (portproxy/firewall rules added).
|
||||
|
||||
Do not mark this complete until the §5 tool-call test passes with a structured `tool_calls` array. That single result is the definition of done for the inference layer.
|
||||
Reference in New Issue
Block a user