Compare commits
3 Commits
04a18efc6a
...
78b824bb39
| Author | SHA1 | Date | |
|---|---|---|---|
| 78b824bb39 | |||
| a908973264 | |||
| 795908e1ef |
@@ -4,14 +4,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Repository Overview
|
## Repository Overview
|
||||||
|
|
||||||
This repository manages a **homelab infrastructure** with self-hosted services running across 3 nodes using a **hybrid orchestration** model: Docker Swarm for core infrastructure services, standalone Docker Compose for isolated services.
|
This repository manages a **homelab infrastructure** with self-hosted services running across 3 nodes using **standalone Docker Compose** on all nodes. (Docker Swarm was fully dismantled on 2026-04-01.)
|
||||||
|
|
||||||
### Cluster Architecture
|
### Cluster Architecture
|
||||||
|
|
||||||
**Nodes:**
|
**Nodes:**
|
||||||
- **hf-pdocker-01** (192.168.86.192) - Swarm Manager, core infrastructure + Frigate NVR - **RESTORED (2026-03-08)**
|
- **hf-pdocker-01** (192.168.86.192) - Core infrastructure + Frigate NVR - **RESTORED (2026-03-08)**
|
||||||
- **hf-pdocker-02** (192.168.86.100) - Standalone Compose services (dev tools, utilities)
|
- **hf-pdocker-02** (192.168.86.100) - Dev tools, utilities
|
||||||
- **bart** (192.168.86.167) - Standalone Compose services (media stack, OpenVAS)
|
- **bart** (192.168.86.167) - Media stack, OpenVAS
|
||||||
|
|
||||||
**Synology NAS** (192.168.86.30) - Centralized NFS storage for all service data
|
**Synology NAS** (192.168.86.30) - Centralized NFS storage for all service data
|
||||||
|
|
||||||
@@ -20,47 +20,47 @@ This repository manages a **homelab infrastructure** with self-hosted services r
|
|||||||
### Service Distribution
|
### Service Distribution
|
||||||
|
|
||||||
```
|
```
|
||||||
hf-pdocker-01 (Manager, 23GB RAM, i7-7700T, NVMe, HD 630 GPU) -- RESTORED (2026-03-08, hardware repaired)
|
hf-pdocker-01 (23GB RAM, i7-7700T, NVMe, HD 630 GPU) -- RESTORED (2026-03-08, hardware repaired)
|
||||||
├── [COMPOSE] Frigate NVR (:5000) -- OpenVINO GPU detector, 13 cameras active
|
├── [COMPOSE] Frigate NVR (:5000) -- OpenVINO GPU detector, 13 cameras active
|
||||||
├── [COMPOSE] Frigate-Notify v2 (:5199) -- MQTT->AI->Matrix notifications
|
├── [COMPOSE] Frigate-Notify v2 (:5199) -- MQTT->AI->Matrix notifications
|
||||||
├── [COMPOSE] clip-service (:5200) -- CLIP embeddings (ViT-L-14, CPU)
|
├── [COMPOSE] clip-service (:5200) -- CLIP embeddings (ViT-L-14, CPU)
|
||||||
├── [COMPOSE] Open WebUI (:3010) -- AI chat UI (Ollama + Anthropic backends)
|
|
||||||
├── [COMPOSE] Home Assistant (:8123)
|
├── [COMPOSE] Home Assistant (:8123)
|
||||||
├── [COMPOSE] ESPHome (:6052)
|
├── [COMPOSE] ESPHome (:6052)
|
||||||
├── [COMPOSE] Mosquitto (:1883, :8883)
|
├── [COMPOSE] Mosquitto (:1883, :8883)
|
||||||
├── [COMPOSE] Synapse/Matrix (:8008) + synapse-db (postgres) + wellknown (:8010)
|
├── [COMPOSE] Synapse/Matrix (:8008) + synapse-db (postgres) + wellknown (:8010)
|
||||||
├── [COMPOSE] Guacamole (:8199) + guacd + guacamole-db (mysql, LOCAL /var/lib/guacamole-db)
|
├── [COMPOSE] Guacamole (:8199) + guacd + guacamole-db (mysql, LOCAL /var/lib/guacamole-db)
|
||||||
├── [COMPOSE] AI Agents (margaret:18790, spirit:18795 [infra ops], clippy:18800, bob:18810, travis:18815)
|
├── [COMPOSE] AI Agents (margaret:18790, spirit:18795 [infra ops])
|
||||||
├── [COMPOSE] Traefik (reverse proxy, ports 80/443/8090)
|
├── [COMPOSE] Traefik (reverse proxy, ports 80/443/8090)
|
||||||
├── [COMPOSE] AdGuard Home (:3000, :53)
|
├── [COMPOSE] AdGuard Home (:3000, :53)
|
||||||
├── [COMPOSE] Monitoring (Grafana :3001, Prometheus :9095, Alertmanager :9093, InfluxDB :8086, Blackbox :9115, matrix-receiver :9094)
|
├── [COMPOSE] Monitoring (Grafana :3001, Prometheus :9095, Alertmanager :9093, InfluxDB :8086, Blackbox :9115, matrix-receiver :9094)
|
||||||
├── [COMPOSE] Farm Manager API (:8888) -- dashboard + API server
|
├── [COMPOSE] Farm Manager API (:8888) -- dashboard + API server
|
||||||
├── [COMPOSE] Farm Agent (:8889)
|
├── [COMPOSE] Farm Agent (:8889)
|
||||||
├── [SWARM] Telegraf (global), Node-exporter (global)
|
├── [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.
|
||||||
|
├── [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
|
||||||
├── [CRON] sync-frigate-to-nas (daily 2am), cleanup-frigate-media (weekly Sun 3am), restart-frigate (daily 4am)
|
├── [CRON] sync-frigate-to-nas (daily 2am), cleanup-frigate-media (weekly Sun 3am), restart-frigate (daily 4am)
|
||||||
├── 5TB USB disk at /mnt/frigate-recordings (Frigate recordings storage)
|
├── 5TB USB disk at /mnt/frigate-recordings (Frigate recordings storage)
|
||||||
|
|
||||||
bart (Worker, 15GB RAM, i5-4570T, HDD)
|
bart (15GB RAM, i5-4570T, HDD)
|
||||||
├── [COMPOSE] Media Stack (Jellyfin, Sonarr, Radarr, SABnzbd, NZBHydra2, Jellyseerr)
|
├── [COMPOSE] Media Stack (Jellyfin, Sonarr, Radarr, SABnzbd, NZBHydra2, Jellyseerr)
|
||||||
├── [COMPOSE] OpenVAS (vulnerability scanner)
|
├── [COMPOSE] OpenVAS (vulnerability scanner)
|
||||||
├── [COMPOSE] Onyx (:3005) -- AI document search/RAG (Ollama backend: llava:13b on 192.168.86.196)
|
|
||||||
├── [COMPOSE] Farm Agent (:8889)
|
├── [COMPOSE] Farm Agent (:8889)
|
||||||
|
├── [COMPOSE] Node-exporter (:9100) -- standalone with pid:host
|
||||||
|
├── [COMPOSE] Telegraf -- metrics collection to InfluxDB
|
||||||
├── [COMPOSE] Watchtower
|
├── [COMPOSE] Watchtower
|
||||||
├── [SWARM] Telegraf (global), Node-exporter (global)
|
|
||||||
|
|
||||||
hf-pdocker-02 (Worker, 7.6GB RAM, i7-6700T, HDD)
|
hf-pdocker-02 (7.6GB RAM, i7-6700T, HDD)
|
||||||
├── [COMPOSE] Unifi + unifi-db (MongoDB)
|
├── [COMPOSE] Unifi + unifi-db (MongoDB)
|
||||||
├── [COMPOSE] Gitea + gitea-db (PostgreSQL)
|
├── [COMPOSE] Gitea + gitea-db (PostgreSQL)
|
||||||
├── [COMPOSE] Semaphore + semaphore-db (MySQL)
|
├── [COMPOSE] Semaphore + semaphore-db (MySQL)
|
||||||
├── [COMPOSE] Wiki.js + wikijs-db (PostgreSQL)
|
├── [COMPOSE] Wiki.js + wikijs-db (PostgreSQL)
|
||||||
├── [COMPOSE] Vaultwarden, Actual Budget, MCPO, Trivy
|
├── [COMPOSE] Vaultwarden, Actual Budget, MCPO, Trivy
|
||||||
├── [COMPOSE] SearXNG (:8888) -- self-hosted meta-search engine (Onyx web search backend)
|
├── [COMPOSE] n8n + n8n-db (PostgreSQL) -- workflow automation
|
||||||
├── [COMPOSE] Unstructured API (:8000) -- document parsing for Onyx file ingestion
|
|
||||||
├── [COMPOSE] n8n + n8n-db (PostgreSQL) -- Clippy workflow automation
|
|
||||||
├── [COMPOSE] NetClaw/Aaron (:1179) -- AI network agent
|
|
||||||
├── [COMPOSE] Farm Agent (:8889)
|
├── [COMPOSE] Farm Agent (:8889)
|
||||||
|
├── [COMPOSE] Node-exporter (:9100) -- standalone with pid:host
|
||||||
|
├── [COMPOSE] Telegraf -- metrics collection to InfluxDB
|
||||||
├── [COMPOSE] Watchtower
|
├── [COMPOSE] Watchtower
|
||||||
├── [SWARM] Telegraf (global), Node-exporter (global)
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ hf-pdocker-02 (Worker, 7.6GB RAM, i7-6700T, HDD)
|
|||||||
|
|
||||||
You are working in a **WSL 2 environment** on Windows. Docker is NOT directly available in WSL - all Docker operations must be performed by connecting to the remote nodes via SSH.
|
You are working in a **WSL 2 environment** on Windows. Docker is NOT directly available in WSL - all Docker operations must be performed by connecting to the remote nodes via SSH.
|
||||||
|
|
||||||
**Critical:** Use the `docker-swarm-ops` agent (via Task tool) for ALL Docker operations. Do not attempt to run docker commands directly in bash - they will fail.
|
**Critical:** Docker is not available locally in WSL. All Docker operations must be performed by connecting to the remote nodes via SSH.
|
||||||
|
|
||||||
### Storage Architecture
|
### Storage Architecture
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ All service data lives on the **Synology NAS** via NFS, mounted at `/mnt/docker-
|
|||||||
├── traefik/ # Let's Encrypt certs
|
├── traefik/ # Let's Encrypt certs
|
||||||
└── compose/ # Standalone compose files per node
|
└── compose/ # Standalone compose files per node
|
||||||
├── hf-pdocker-01/{frigate,frigate-notify,clip-service,matrix,smarthome,guacamole,ai-agents,traefik,adguard}/
|
├── hf-pdocker-01/{frigate,frigate-notify,clip-service,matrix,smarthome,guacamole,ai-agents,traefik,adguard}/
|
||||||
├── hf-pdocker-02/{unifi,gitea,semaphore,wikijs,vaultwarden,actual-budget,mcpo,trivy,netclaw,watchtower}/
|
├── hf-pdocker-02/{unifi,gitea,semaphore,wikijs,vaultwarden,actual-budget,mcpo,trivy,watchtower}/
|
||||||
└── bart/{media,openvas,watchtower}/
|
└── bart/{media,openvas,watchtower}/
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -95,13 +95,10 @@ All service data lives on the **Synology NAS** via NFS, mounted at `/mnt/docker-
|
|||||||
### Repository File Structure
|
### Repository File Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
stacks/ # Swarm stack definitions (deployed on hf-pdocker-01)
|
stacks/ # LEGACY Swarm stack definitions (Swarm dismantled 2026-04-01, kept for reference)
|
||||||
├── {service-name}/
|
|
||||||
│ ├── docker-compose.yml
|
|
||||||
│ └── .env (if needed)
|
|
||||||
|
|
||||||
compose/ # Standalone Compose definitions (deployed per-node)
|
compose/ # Standalone Compose definitions (deployed per-node)
|
||||||
├── hf-pdocker-01/ # Frigate
|
├── hf-pdocker-01/ # Core infrastructure + Frigate
|
||||||
├── hf-pdocker-02/ # Dev tools & utilities
|
├── hf-pdocker-02/ # Dev tools & utilities
|
||||||
│ ├── {service}/docker-compose.yml
|
│ ├── {service}/docker-compose.yml
|
||||||
│ └── {service}/.env
|
│ └── {service}/.env
|
||||||
@@ -110,20 +107,15 @@ compose/ # Standalone Compose definitions (deployed per-node)
|
|||||||
|
|
||||||
### Deployment Workflow
|
### Deployment Workflow
|
||||||
|
|
||||||
**Swarm services (stacks/):**
|
**All services (compose/):**
|
||||||
1. Edit stack file in `stacks/{service-name}/docker-compose.yml`
|
|
||||||
2. SCP to hf-pdocker-01 and redeploy: `docker stack deploy -c <file> <stack-name>`
|
|
||||||
|
|
||||||
**Standalone Compose services (compose/):**
|
|
||||||
1. Edit compose file in `compose/{node}/{service}/docker-compose.yml`
|
1. Edit compose file in `compose/{node}/{service}/docker-compose.yml`
|
||||||
2. SCP to NAS path or node, then: `cd /mnt/docker-data/compose/{node}/{service} && docker compose up -d`
|
2. SCP to NAS path or node, then: `cd /mnt/docker-data/compose/{node}/{service} && docker compose up -d`
|
||||||
|
|
||||||
### Service Architecture Patterns
|
### Service Architecture Patterns
|
||||||
|
|
||||||
**Network Patterns:**
|
**Network Patterns:**
|
||||||
- **Swarm services**: Overlay networks for inter-service communication
|
|
||||||
- **Compose services**: Bridge networks for service isolation
|
- **Compose services**: Bridge networks for service isolation
|
||||||
- **Public services** (Traefik): Connect to `traefik-public` overlay + Traefik labels
|
- **Public services** (Traefik): Traefik labels + dynamic config files in `/mnt/docker-data/traefik/dynamic/`
|
||||||
|
|
||||||
**Database Patterns:**
|
**Database Patterns:**
|
||||||
- PostgreSQL: `postgres:15-alpine` or `postgres:16-alpine`
|
- PostgreSQL: `postgres:15-alpine` or `postgres:16-alpine`
|
||||||
@@ -146,8 +138,8 @@ compose/ # Standalone Compose definitions (deployed per-node)
|
|||||||
- Home Assistant: `yoda.hobbs.farm`
|
- Home Assistant: `yoda.hobbs.farm`
|
||||||
- Vaultwarden: `vault.hobbs.farm`
|
- Vaultwarden: `vault.hobbs.farm`
|
||||||
- Gitea: `git.hobbs.farm`
|
- Gitea: `git.hobbs.farm`
|
||||||
- Onyx: `onyx.hobbs.farm`
|
- VoiceCoach: `coach.hobbs.farm` (LiveKit WebRTC: `voice.hobbs.farm`)
|
||||||
- Open WebUI: `open.hobbs.farm`
|
- Rongorongo: `rongorongo.hobbs.farm`
|
||||||
|
|
||||||
### Internal Services by Node
|
### Internal Services by Node
|
||||||
|
|
||||||
@@ -155,7 +147,6 @@ compose/ # Standalone Compose definitions (deployed per-node)
|
|||||||
- Frigate: :5000
|
- Frigate: :5000
|
||||||
- Frigate Alerts (v2): :5199 (web UI + REST API)
|
- Frigate Alerts (v2): :5199 (web UI + REST API)
|
||||||
- clip-service: :5200 (CLIP embeddings, ViT-L-14, CPU)
|
- clip-service: :5200 (CLIP embeddings, ViT-L-14, CPU)
|
||||||
- Open WebUI: :3010 (AI chat UI, public at open.hobbs.farm)
|
|
||||||
- Home Assistant: :8123
|
- Home Assistant: :8123
|
||||||
- ESPHome: :6052
|
- ESPHome: :6052
|
||||||
- Mosquitto: :1883
|
- Mosquitto: :1883
|
||||||
@@ -170,9 +161,11 @@ compose/ # Standalone Compose definitions (deployed per-node)
|
|||||||
- InfluxDB: :8086
|
- InfluxDB: :8086
|
||||||
- Blackbox Exporter: :9115
|
- Blackbox Exporter: :9115
|
||||||
- matrix-alertmanager-receiver: :9094
|
- matrix-alertmanager-receiver: :9094
|
||||||
- AI Agents (margaret :18790, spirit :18795 [infra ops — monitors #notifications, auto-remediates alerts], clippy :18800, bob :18810, travis :18815) -- LAN-accessible (bind=lan, 0.0.0.0)
|
- AI Agents (margaret :18790, spirit :18795 [infra ops — monitors #notifications, auto-remediates alerts]) -- LAN-accessible (bind=lan, 0.0.0.0)
|
||||||
- Farm Manager: :8888 (dashboard + API)
|
- Farm Manager: :8888 (dashboard + API)
|
||||||
- Farm Agent: :8889
|
- 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
|
||||||
|
|
||||||
**hf-pdocker-02 (192.168.86.100):**
|
**hf-pdocker-02 (192.168.86.100):**
|
||||||
- Farm Agent: :8889
|
- Farm Agent: :8889
|
||||||
@@ -184,9 +177,6 @@ compose/ # Standalone Compose definitions (deployed per-node)
|
|||||||
- Actual Budget: :5006
|
- Actual Budget: :5006
|
||||||
- MCPO: :8001
|
- MCPO: :8001
|
||||||
- Trivy: :4954
|
- Trivy: :4954
|
||||||
- SearXNG: :8888 (meta-search, Onyx web search backend)
|
|
||||||
- Unstructured API: :8000 (document parsing for Onyx file ingestion)
|
|
||||||
- NetClaw/Aaron: :1179 (internal only)
|
|
||||||
|
|
||||||
**bart (192.168.86.167):**
|
**bart (192.168.86.167):**
|
||||||
- Farm Agent: :8889
|
- Farm Agent: :8889
|
||||||
@@ -197,19 +187,27 @@ compose/ # Standalone Compose definitions (deployed per-node)
|
|||||||
- NZBHydra2: :5076
|
- NZBHydra2: :5076
|
||||||
- Jellyseerr: :5055
|
- Jellyseerr: :5055
|
||||||
- OpenVAS: :9392
|
- OpenVAS: :9392
|
||||||
- Onyx: :3005 (AI document search/RAG)
|
|
||||||
|
|
||||||
**GPU Machine (192.168.86.196, RTX 3090):**
|
**GPU Machine (192.168.86.150, RTX 3090, 15GB RAM):**
|
||||||
- Ollama: :11434 (vision LLM, llava:13b)
|
- 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
|
||||||
|
|
||||||
### Port Conflict Notes
|
### Port Conflict Notes
|
||||||
|
|
||||||
Port conflicts already resolved:
|
Port conflicts already resolved:
|
||||||
- Port 8000: Now used by Unstructured API on hf-pdocker-02 (MCPO moved to 8001, Portainer moved elsewhere)
|
- Port 8000: Freed (MCPO moved to 8001, Portainer moved elsewhere)
|
||||||
- Port 3000: AdGuard Home (Gitea moved to 3003)
|
- Port 3000: AdGuard Home (Gitea moved to 3003)
|
||||||
- Port 3001: Grafana (Semaphore moved to 3004)
|
- Port 3001: Grafana (Semaphore moved to 3004)
|
||||||
- Port 5000: Frigate uses host networking (registry container stopped)
|
- Port 5000: Frigate uses host networking (registry container stopped)
|
||||||
- AI Agent ports re-spaced (2026-03-15): openclaw 2026.3.13 uses gateway+2 and gateway+3 for browser/chrome-relay. Agents now spaced 5 apart: margaret=18790, spirit=18795, clippy=18800, bob=18810, travis=18815
|
- AI Agent ports re-spaced (2026-03-15): openclaw 2026.3.13 uses gateway+2 and gateway+3 for browser/chrome-relay. Agents spaced 5 apart: margaret=18790, spirit=18795
|
||||||
|
|
||||||
## Critical Configuration Preservation
|
## Critical Configuration Preservation
|
||||||
|
|
||||||
@@ -235,20 +233,11 @@ Do NOT wait for the user to ask. Do NOT skip "because the change was small." Eve
|
|||||||
|
|
||||||
## Common Operations
|
## Common Operations
|
||||||
|
|
||||||
### Deploy/Update a Swarm Service
|
|
||||||
|
|
||||||
1. Edit `stacks/{service}/docker-compose.yml`
|
|
||||||
2. Use docker-swarm-ops agent to SCP and deploy:
|
|
||||||
```bash
|
|
||||||
docker stack deploy -c <compose-file> <stack-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deploy/Update a Compose Service
|
### Deploy/Update a Compose Service
|
||||||
|
|
||||||
1. Edit `compose/{node}/{service}/docker-compose.yml`
|
1. Edit `compose/{node}/{service}/docker-compose.yml`
|
||||||
2. Use docker-swarm-ops agent to:
|
2. SSH to the target node and run:
|
||||||
```bash
|
```bash
|
||||||
# On the target node:
|
|
||||||
cd /mnt/docker-data/compose/{node}/{service}
|
cd /mnt/docker-data/compose/{node}/{service}
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
@@ -256,11 +245,7 @@ Do NOT wait for the user to ask. Do NOT skip "because the change was small." Eve
|
|||||||
### Troubleshoot
|
### Troubleshoot
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Swarm services (on hf-pdocker-01):
|
# On target node:
|
||||||
docker service ls
|
|
||||||
docker service logs {stack}_{service} --tail 100
|
|
||||||
|
|
||||||
# Compose services (on target node):
|
|
||||||
docker compose -f /mnt/docker-data/compose/{node}/{service}/docker-compose.yml logs --tail 100
|
docker compose -f /mnt/docker-data/compose/{node}/{service}/docker-compose.yml logs --tail 100
|
||||||
docker ps --format "table {{.Names}}\t{{.Status}}"
|
docker ps --format "table {{.Names}}\t{{.Status}}"
|
||||||
```
|
```
|
||||||
@@ -268,13 +253,8 @@ docker ps --format "table {{.Names}}\t{{.Status}}"
|
|||||||
### Check Cluster Health
|
### Check Cluster Health
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# On hf-pdocker-01:
|
|
||||||
docker node ls # Swarm node status
|
|
||||||
docker service ls # Swarm services
|
|
||||||
docker stack ls # Deployed stacks
|
|
||||||
|
|
||||||
# On each node:
|
# On each node:
|
||||||
docker ps # Local containers (Compose + Swarm)
|
docker ps # Local containers
|
||||||
```
|
```
|
||||||
|
|
||||||
## Data Management
|
## Data Management
|
||||||
@@ -296,9 +276,6 @@ All persistent data on NAS at `/mnt/docker-data/`:
|
|||||||
|
|
||||||
## Specialized Agents
|
## Specialized Agents
|
||||||
|
|
||||||
### docker-swarm-ops
|
|
||||||
**Use for:** All Docker operations (Swarm + Compose) on any node
|
|
||||||
|
|
||||||
### vault-credential-manager
|
### vault-credential-manager
|
||||||
**Use for:** Storing/retrieving credentials from Vaultwarden
|
**Use for:** Storing/retrieving credentials from Vaultwarden
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY app.py .
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health').status==200 else 1)" || exit 1
|
||||||
|
|
||||||
|
CMD ["gunicorn", "-w", "2", "-t", "60", "-b", "0.0.0.0:8000", "app:app"]
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
"""
|
||||||
|
Rongorongo state-sync backend.
|
||||||
|
|
||||||
|
Single-user. Stores state as one JSON file and images as content-addressed
|
||||||
|
files on disk. No DB, no auth — auth is enforced by Traefik basicAuth in
|
||||||
|
front of the whole app.
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from flask import Flask, abort, jsonify, request, send_file
|
||||||
|
|
||||||
|
DATA_DIR = Path(os.environ.get("RONGORONGO_DATA_DIR", "/data"))
|
||||||
|
STATE_FILE = DATA_DIR / "state.json"
|
||||||
|
IMAGES_DIR = DATA_DIR / "images"
|
||||||
|
MAX_IMAGE_BYTES = 25 * 1024 * 1024 # 25 MB per image
|
||||||
|
|
||||||
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
IMAGES_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config["MAX_CONTENT_LENGTH"] = MAX_IMAGE_BYTES + 1024
|
||||||
|
|
||||||
|
|
||||||
|
def _atomic_write(path: Path, data: bytes) -> None:
|
||||||
|
fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=".tmp-", suffix=".part")
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, "wb") as fh:
|
||||||
|
fh.write(data)
|
||||||
|
os.replace(tmp, path)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
os.unlink(tmp)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _ext_for(mime: str) -> str:
|
||||||
|
if not mime:
|
||||||
|
return ".bin"
|
||||||
|
guess = mimetypes.guess_extension(mime) or ".bin"
|
||||||
|
# mimetypes returns .jpe for image/jpeg on some systems
|
||||||
|
return ".jpg" if guess == ".jpe" else guess
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health():
|
||||||
|
return ("ok\n", 200, {"Content-Type": "text/plain"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/health")
|
||||||
|
def api_health():
|
||||||
|
return ("ok\n", 200, {"Content-Type": "text/plain"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/state")
|
||||||
|
def get_state():
|
||||||
|
if not STATE_FILE.exists():
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"images": [],
|
||||||
|
"glyphs": [],
|
||||||
|
"anchors": [],
|
||||||
|
"analyses": [],
|
||||||
|
"feasibilityRuns": [],
|
||||||
|
"confidenceHistory": {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return send_file(STATE_FILE, mimetype="application/json")
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/state")
|
||||||
|
def put_state():
|
||||||
|
if not request.is_json:
|
||||||
|
abort(415, description="expected application/json")
|
||||||
|
payload = request.get_json(silent=False)
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
abort(400, description="state must be a JSON object")
|
||||||
|
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
||||||
|
_atomic_write(STATE_FILE, body)
|
||||||
|
return jsonify({"ok": True, "bytes": len(body)})
|
||||||
|
|
||||||
|
|
||||||
|
def _find_image(image_hash: str) -> Path | None:
|
||||||
|
for p in IMAGES_DIR.glob(f"{image_hash}.*"):
|
||||||
|
return p
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/images")
|
||||||
|
def upload_image():
|
||||||
|
f = request.files.get("file")
|
||||||
|
if f is None or f.filename == "":
|
||||||
|
abort(400, description="missing file")
|
||||||
|
data = f.read()
|
||||||
|
if len(data) == 0:
|
||||||
|
abort(400, description="empty file")
|
||||||
|
if len(data) > MAX_IMAGE_BYTES:
|
||||||
|
abort(413, description="image too large")
|
||||||
|
digest = hashlib.sha256(data).hexdigest()
|
||||||
|
mime = f.mimetype or "application/octet-stream"
|
||||||
|
existing = _find_image(digest)
|
||||||
|
if existing is None:
|
||||||
|
target = IMAGES_DIR / f"{digest}{_ext_for(mime)}"
|
||||||
|
_atomic_write(target, data)
|
||||||
|
return jsonify(
|
||||||
|
{"hash": digest, "name": f.filename, "mimeType": mime, "size": len(data)}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/images/<image_hash>")
|
||||||
|
def get_image(image_hash: str):
|
||||||
|
if not all(c in "0123456789abcdef" for c in image_hash) or len(image_hash) != 64:
|
||||||
|
abort(400, description="invalid hash")
|
||||||
|
p = _find_image(image_hash)
|
||||||
|
if p is None:
|
||||||
|
abort(404)
|
||||||
|
mime, _ = mimetypes.guess_type(str(p))
|
||||||
|
return send_file(p, mimetype=mime or "application/octet-stream")
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/images/<image_hash>")
|
||||||
|
def delete_image(image_hash: str):
|
||||||
|
if not all(c in "0123456789abcdef" for c in image_hash) or len(image_hash) != 64:
|
||||||
|
abort(400, description="invalid hash")
|
||||||
|
p = _find_image(image_hash)
|
||||||
|
if p is None:
|
||||||
|
return ("", 204)
|
||||||
|
p.unlink()
|
||||||
|
return ("", 204)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="0.0.0.0", port=8000)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
flask==3.0.3
|
||||||
|
gunicorn==23.0.0
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
services:
|
||||||
|
rongorongo-api:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: rongorongo-api:latest
|
||||||
|
container_name: rongorongo-api
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- rongorongo
|
||||||
|
volumes:
|
||||||
|
- /mnt/docker-data/configs/rongorongo/data:/data
|
||||||
|
mem_limit: 256M
|
||||||
|
memswap_limit: 256M
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health').status==200 else 1)"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
labels:
|
||||||
|
- "com.centurylinklabs.watchtower.enable=false"
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
rongorongo-web:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: rongorongo-web:latest
|
||||||
|
container_name: rongorongo-web
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- rongorongo-api
|
||||||
|
networks:
|
||||||
|
- rongorongo
|
||||||
|
ports:
|
||||||
|
- "8181:80"
|
||||||
|
mem_limit: 128M
|
||||||
|
memswap_limit: 128M
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
labels:
|
||||||
|
- "com.centurylinklabs.watchtower.enable=false"
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
rongorongo:
|
||||||
|
driver: bridge
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
FROM nginx:1.25-alpine
|
||||||
|
|
||||||
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
|
||||||
|
COPY index.html /usr/share/nginx/html/index.html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
RUN chmod 644 /usr/share/nginx/html/index.html
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD wget -qO- http://127.0.0.1/health || exit 1
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
client_max_body_size 30m;
|
||||||
|
|
||||||
|
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 on;
|
||||||
|
gzip_types text/html text/css application/javascript application/json;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 "ok\n";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
resolver 127.0.0.11 valid=10s;
|
||||||
|
set $backend "http://rongorongo-api:8000";
|
||||||
|
proxy_pass $backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
expires -1;
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|woff2)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
services:
|
services:
|
||||||
gitea:
|
gitea:
|
||||||
image: gitea/gitea:latest
|
image: gitea/gitea:1.26.1
|
||||||
container_name: gitea
|
container_name: gitea
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "com.centurylinklabs.watchtower.enable=false"
|
||||||
ports:
|
ports:
|
||||||
- "3003:3000"
|
- "3003:3000"
|
||||||
- "2222:22"
|
- "2222:22"
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ scrape_configs:
|
|||||||
- http://192.168.86.192:5000 # Frigate NVR (standalone, host network)
|
- http://192.168.86.192:5000 # Frigate NVR (standalone, host network)
|
||||||
- http://192.168.86.192:5199/health # Frigate Notify
|
- http://192.168.86.192:5199/health # Frigate Notify
|
||||||
- http://192.168.86.192:5200/health # clip-service (CLIP embeddings)
|
- http://192.168.86.192:5200/health # clip-service (CLIP embeddings)
|
||||||
- http://192.168.86.192:3010 # Open WebUI
|
|
||||||
- http://192.168.86.192:8888/health # Farm Manager API
|
- http://192.168.86.192:8888/health # Farm Manager API
|
||||||
- http://192.168.86.192:8889/health # Farm Agent
|
- http://192.168.86.192:8889/health # Farm Agent
|
||||||
- http://192.168.86.192:8123 # Home Assistant
|
- http://192.168.86.192:8123 # Home Assistant
|
||||||
@@ -69,8 +68,10 @@ scrape_configs:
|
|||||||
- http://192.168.86.192:3001 # Grafana
|
- http://192.168.86.192:3001 # Grafana
|
||||||
- http://192.168.86.192:9095 # Prometheus
|
- http://192.168.86.192:9095 # Prometheus
|
||||||
- http://192.168.86.192:9093 # Alertmanager
|
- http://192.168.86.192:9093 # Alertmanager
|
||||||
- http://192.168.86.192:18815 # Travis (NemoClaw agent)
|
|
||||||
- http://192.168.86.192:18795 # Spirit (infra ops agent)
|
- http://192.168.86.192:18795 # Spirit (infra ops agent)
|
||||||
|
- http://192.168.86.192:8085 # VoiceCoach web client
|
||||||
|
- http://192.168.86.192:8181/health # Rongorongo (probe internal — Traefik basicAuth blocks external)
|
||||||
|
- http://192.168.86.192:7880 # VoiceCoach LiveKit server
|
||||||
relabel_configs:
|
relabel_configs:
|
||||||
- source_labels: [__address__]
|
- source_labels: [__address__]
|
||||||
target_label: __param_target
|
target_label: __param_target
|
||||||
@@ -93,9 +94,6 @@ scrape_configs:
|
|||||||
- http://192.168.86.100:5006 # Actual Budget
|
- http://192.168.86.100:5006 # Actual Budget
|
||||||
- http://192.168.86.100:8001/openapi.json # MCPO
|
- http://192.168.86.100:8001/openapi.json # MCPO
|
||||||
- http://192.168.86.100:8889/health # Farm Agent
|
- http://192.168.86.100:8889/health # Farm Agent
|
||||||
- http://192.168.86.100:8888 # SearXNG
|
|
||||||
- http://192.168.86.100:8000/healthcheck # Unstructured API
|
|
||||||
- http://192.168.86.100:9000 # Portainer
|
|
||||||
relabel_configs:
|
relabel_configs:
|
||||||
- source_labels: [__address__]
|
- source_labels: [__address__]
|
||||||
target_label: __param_target
|
target_label: __param_target
|
||||||
@@ -135,10 +133,6 @@ scrape_configs:
|
|||||||
- http://192.168.86.167:5055 # Jellyseerr
|
- http://192.168.86.167:5055 # Jellyseerr
|
||||||
- http://192.168.86.167:9392 # OpenVAS
|
- http://192.168.86.167:9392 # OpenVAS
|
||||||
- http://192.168.86.167:8889/health # Farm Agent
|
- http://192.168.86.167:8889/health # Farm Agent
|
||||||
- targets:
|
|
||||||
- http://192.168.86.167:3005 # Onyx
|
|
||||||
labels:
|
|
||||||
name: onyx
|
|
||||||
relabel_configs:
|
relabel_configs:
|
||||||
- source_labels: [__address__]
|
- source_labels: [__address__]
|
||||||
target_label: __param_target
|
target_label: __param_target
|
||||||
@@ -159,7 +153,7 @@ scrape_configs:
|
|||||||
- https://yoda.hobbs.farm # Home Assistant
|
- https://yoda.hobbs.farm # Home Assistant
|
||||||
- https://vault.hobbs.farm # Vaultwarden
|
- https://vault.hobbs.farm # Vaultwarden
|
||||||
- https://git.hobbs.farm # Gitea
|
- https://git.hobbs.farm # Gitea
|
||||||
- https://open.hobbs.farm # Open WebUI
|
- https://coach.hobbs.farm # VoiceCoach web client
|
||||||
relabel_configs:
|
relabel_configs:
|
||||||
- source_labels: [__address__]
|
- source_labels: [__address__]
|
||||||
target_label: __param_target
|
target_label: __param_target
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
http:
|
||||||
|
routers:
|
||||||
|
rongorongo:
|
||||||
|
rule: "Host(`rongorongo.hobbs.farm`)"
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- rongorongo-auth
|
||||||
|
- security-headers
|
||||||
|
service: rongorongo
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
rongorongo-http:
|
||||||
|
rule: "Host(`rongorongo.hobbs.farm`)"
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
middlewares:
|
||||||
|
- rongorongo-https-redirect
|
||||||
|
service: rongorongo
|
||||||
|
middlewares:
|
||||||
|
rongorongo-https-redirect:
|
||||||
|
redirectScheme:
|
||||||
|
scheme: https
|
||||||
|
permanent: true
|
||||||
|
rongorongo-auth:
|
||||||
|
basicAuth:
|
||||||
|
realm: "rongorongo.hobbs.farm"
|
||||||
|
# users entries are appended at deploy time as: <user>:<bcrypt-hash>
|
||||||
|
users:
|
||||||
|
- "REPLACE_AT_DEPLOY"
|
||||||
|
services:
|
||||||
|
rongorongo:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: "http://192.168.86.192:8181"
|
||||||
Reference in New Issue
Block a user