- gitea: LevelDB queues -> local /var/lib/gitea-queues (stale NFS lock, 7,972-restart loop) - semaphore-db: data -> local /var/lib/semaphore-db (stale ibdata1 NFS lock, 975 restarts) - sonarr: config -> local /var/lib/sonarr (SQLite-on-NFS migration crash) - nzbhydra2: mem_limit 768M -> 1G (was pinned at limit, unhealthy) - jellyfin: deploy pending VAAPI device passthrough (i965/renderD128) - openvas: disable Watchtower (latest major pull forced pg_upgrade crash-loop) - onyx: fully decommissioned (down since ~June 8 unnoticed) - remove duplicate monitoring/traefik/guacamole stacks from .100, failover guacamole from bart - synapse: raise rc_message (alertmanager receiver was 429-limited; alerts silently dropped for weeks) - prometheus: UniFi probe 8443 -> 11443, solo.hobbs.farm via 401-tolerant module, drop onyx target - frigate restart cron: unwritable /var/log path meant it never ran; now 4x daily to ~glyptic - CLAUDE.md: document all of the above + AdGuard broken/unconfigured since 2026-05-17 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
21 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Repository Overview
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
Nodes:
- hf-pdocker-01 (192.168.86.192) - Core infrastructure + Frigate NVR - RESTORED (2026-03-08)
- hf-pdocker-02 (192.168.86.100) - Dev tools, utilities
- bart (192.168.86.167) - Media stack, OpenVAS
Synology NAS (192.168.86.30) - Centralized NFS storage for all service data
Access: Username glyptic, password &Rip8a115 (has sudo access on all nodes)
Service Distribution
hf-pdocker-01 (23GB RAM, i7-7700T, NVMe, HD 630 GPU) -- RESTORED (2026-03-08, hardware repaired)
├── [COMPOSE] Frigate NVR (:5000) -- Coral USB TPU (edgetpu), ~11ms inference, 13 cameras active
├── [COMPOSE] Frigate-Notify v2 (:5199) -- MQTT->AI->Matrix notifications
├── [COMPOSE] clip-service (:5200) -- CLIP embeddings (ViT-L-14, CPU)
├── [COMPOSE] Home Assistant (:8123)
├── [COMPOSE] ESPHome (:6052)
├── [COMPOSE] Mosquitto (:1883, :8883)
├── [COMPOSE] Synapse/Matrix (:8008) + synapse-db (postgres) + wellknown (:8010)
├── [COMPOSE] Guacamole (:8199) + guacd + guacamole-db (mysql, LOCAL /var/lib/guacamole-db)
├── [COMPOSE] AI Agents (margaret:18790, spirit:18795 [infra ops])
├── [COMPOSE] Traefik (reverse proxy, ports 80/443/8090)
├── [COMPOSE] AdGuard Home (:3000, :53) -- **BROKEN since 2026-05-17**: conf dir empty, stuck in first-run wizard, DNS not answering. No backup found; needs manual reconfigure via :3000/install.html.
├── [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 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] litellm (:4000?) + litellm-db -- LLM proxy (see Known Issue #15)
├── [COMPOSE] synapse-admin -- Synapse admin UI
├── [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 (4x daily 4am/10am/4pm/10pm, log ~/frigate-restart.log)
├── 5TB USB disk at /mnt/frigate-recordings (Frigate recordings storage)
bart (15GB RAM, i5-4570T, HDD)
├── [COMPOSE] Media Stack (Jellyfin, Sonarr, Radarr, SABnzbd, NZBHydra2, Jellyseerr) -- Sonarr config on LOCAL /var/lib/sonarr (2026-07-13, SQLite-on-NFS crash)
├── [REMOVED] Onyx -- fully decommissioned 2026-07-13 (had been down since ~2026-06-08 unnoticed; containers, volumes, images removed)
├── [COMPOSE] OpenVAS (vulnerability scanner) -- Watchtower disabled 2026-07-13 after :latest major pull forced pg_upgrade crash-loop
├── [COMPOSE] Farm Agent (:8889)
├── [COMPOSE] Node-exporter (:9100) -- standalone with pid:host
├── [COMPOSE] Telegraf -- metrics collection to InfluxDB
├── [COMPOSE] Watchtower
hf-pdocker-02 (7.6GB RAM, i7-6700T, HDD)
├── [COMPOSE] UniFi OS Server (container `unifi-os-server`, HTTPS on host :11443)
├── [COMPOSE] Gitea + gitea-db (PostgreSQL) -- LevelDB queues on LOCAL /var/lib/gitea-queues (2026-07-13, stale NFS lock caused 7,972-restart loop)
├── [COMPOSE] Semaphore + semaphore-db (MySQL, LOCAL /var/lib/semaphore-db since 2026-07-13)
├── [COMPOSE] Wiki.js + wikijs-db (PostgreSQL)
├── [COMPOSE] Vaultwarden, Actual Budget, MCPO, Trivy
├── [COMPOSE] n8n + n8n-db (PostgreSQL) -- workflow automation
├── [COMPOSE] CouchDB (:5984) -- Obsidian LiveSync backend for the Second Brain vault. LAN-only. LOCAL disk (NOT NFS).
├── [COMPOSE] Farm Agent (:8889)
├── [COMPOSE] Node-exporter (:9100) -- standalone with pid:host
├── [COMPOSE] Telegraf -- metrics collection to InfluxDB
├── [COMPOSE] Watchtower
Working with This Repository
Environment Context
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: Docker is not available locally in WSL. All Docker operations must be performed by connecting to the remote nodes via SSH.
Storage Architecture
All service data lives on the Synology NAS via NFS, mounted at /mnt/docker-data on all 3 nodes.
/mnt/docker-data/ (NAS: 192.168.86.30:/volume1/Docker)
├── configs/ # Application configs
├── databases/ # Database data directories
├── media/ # Frigate recordings
├── traefik/ # Let's Encrypt certs
└── compose/ # Standalone compose files per node
├── 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,watchtower}/
└── bart/{media,openvas,watchtower}/
NFS mount (all nodes): 192.168.86.30:/volume1/Docker /mnt/docker-data nfs rw,soft,timeo=300,intr,_netdev,nfsvers=3,nofail,x-systemd.after=network-online.target,x-systemd.mount-timeout=60s 0 0
Note (hf-pdocker-01): Both NFS mounts hardened with nofail,x-systemd.after=network-online.target,x-systemd.mount-timeout=60s on 2026-05-20 to prevent boot-time race (network not up when NFS tried to mount, cascading ~10 container failures).
NFS + Databases: Database containers on NFS require user: "1024:100" to avoid chown failures from NFS root_squash. UID 1024 is the NAS anonymous user.
Repository File Structure
stacks/ # LEGACY Swarm stack definitions (Swarm dismantled 2026-04-01, kept for reference)
compose/ # Standalone Compose definitions (deployed per-node)
├── hf-pdocker-01/ # Core infrastructure + Frigate
├── hf-pdocker-02/ # Dev tools & utilities
│ ├── {service}/docker-compose.yml
│ └── {service}/.env
└── bart/ # Media stack & OpenVAS
Deployment Workflow
All services (compose/):
- Edit compose file in
compose/{node}/{service}/docker-compose.yml - SCP to NAS path or node, then:
cd /mnt/docker-data/compose/{node}/{service} && docker compose up -d
Service Architecture Patterns
Network Patterns:
- Compose services: Bridge networks for service isolation
- Public services (Traefik): Traefik labels + dynamic config files in
/mnt/docker-data/traefik/dynamic/
Database Patterns:
- PostgreSQL:
postgres:15-alpineorpostgres:16-alpine - MySQL:
mysql:8.0(Guacamole, Semaphore) - MongoDB:
mongo:7.0(Unifi) - Always add
com.centurylinklabs.watchtower.enable=falselabel - Always add
user: "1024:100"for NFS-backed database volumes
Volume Patterns:
- Application data:
/mnt/docker-data/configs/{service-name}/ - Database data:
/mnt/docker-data/databases/{service-name}-db/ - Traefik certs:
/mnt/docker-data/traefik/letsencrypt/
Key Services & Access
Public Services (via Traefik on hf-pdocker-01)
- Matrix:
mess.hobbs.farm - Guacamole:
solo.hobbs.farm - Home Assistant:
yoda.hobbs.farm - Vaultwarden:
vault.hobbs.farm - Gitea:
git.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
hf-pdocker-01 (192.168.86.192):
- Frigate: :5000
- Frigate Alerts (v2): :5199 (web UI + REST API)
- clip-service: :5200 (CLIP embeddings, ViT-L-14, CPU)
- Home Assistant: :8123
- ESPHome: :6052
- Mosquitto: :1883
- Matrix/Synapse: :8008
- Well-known (Matrix federation): :8010
- Guacamole: :8199
- AdGuard Home: :3000 (DNS: :53)
- Traefik Dashboard: :8090 (HTTP: :80, HTTPS: :443)
- Grafana: :3001
- Prometheus: :9095
- Alertmanager: :9093
- InfluxDB: :8086
- Blackbox Exporter: :9115
- matrix-alertmanager-receiver: :9094
- 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 Agent: :8889
- Rongorongo: :8181 (rongorongo-web nginx; reverse-proxies /api/* to rongorongo-api Flask backend on the internal
rongorongobridge net). State + images at /mnt/docker-data/configs/rongorongo/data/. Public URL gated by Traefik basicAuth (creds in Vaultwarden). - 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 + memoryfriday-voice)
hf-pdocker-02 (192.168.86.100):
- Farm Agent: :8889
- UniFi OS Server: :11443 (HTTPS, container 443)
- Gitea: :3003 (web), :2222 (SSH)
- Semaphore: :3004
- Wiki.js: :3002
- Vaultwarden: :26226
- Actual Budget: :5006
- MCPO: :8001
- Trivy: :4954
- CouchDB: :5984 -- Obsidian LiveSync backend. DB
secondbrain; admin creds in Vaultwarden ("CouchDB - Obsidian LiveSync"). LAN-only (no Traefik route). Data on LOCAL disk/var/lib/couchdb(NFS breaks CouchDB file locking).local.inimust be mounted READ-WRITE -- with:roCouchDB exits 1 and logs nothing. Auth required on everything except/_up(left open for the blackbox probe). Vault repo: git.hobbs.farm/jeremy/second-brain (private); vault lives atC:\Users\Jeremy\second-brainon the Windows box.
bart (192.168.86.167):
- Farm Agent: :8889
- Jellyfin: :8096
- Sonarr: :8989
- Radarr: :7878
- SABnzbd: :8082
- NZBHydra2: :5076
- Jellyseerr: :5055
- OpenVAS: :9392
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_enginefirst — start Docker Desktop before UniFi OS Server. See memorydocker-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:latestgateway — FRIDAY's reasoning agent. Settings transferred from WSL~/.hermesinto named volumehermes-data. Compose:compose/gpu-machine/hermes/. - friday-voice: :8320 — push-to-talk console. Calls speaches (
host.docker.internal:8000) + hermes (docker exec hermes hermes -Qvia wrapper). Public: coach.hobbs.farm → :8320. Compose:compose/gpu-machine/friday-voice/.
- voicecoach-speaches-gpu: :8000 — GPU Whisper large-v3-turbo (STT) + Kokoro-82M af_nova (TTS), CUDA float16. Compose:
- Ollama: :11434 (qwen2.5:14b, Hermes-4.3-36B-GGUF, others)
Port Conflict Notes
Port conflicts already resolved:
- Port 8000: Freed (MCPO moved to 8001, Portainer moved elsewhere)
- Port 3000: AdGuard Home (Gitea moved to 3003)
- Port 3001: Grafana (Semaphore moved to 3004)
- 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 spaced 5 apart: margaret=18790, spirit=18795
Critical Configuration Preservation
Encryption Keys & Passwords
- Semaphore:
SEMAPHORE_ACCESS_KEY_ENCRYPTIONencrypts sensitive data - Always check for existing encryption keys before generating new ones
Database Version Matching
Match the PostgreSQL/MySQL version to existing data directory format. Check logs for version mismatch errors.
Post-Change Checklist (MANDATORY)
After ANY infrastructure change (deploy, remove, reconfigure, add service, change ports, modify Traefik routes), you MUST complete ALL of these before reporting done:
- CLAUDE.md — Update service lists, ports, node assignments if changed
- MEMORY.md — Add/update relevant memory entries
- Prometheus probes — Add/remove blackbox targets, hot-reload Prometheus
- Wiki — Launch documentation-keeper agent to update wiki pages (run in background)
Do NOT wait for the user to ask. Do NOT skip "because the change was small." Every change gets documented.
Common Operations
Deploy/Update a Compose Service
- Edit
compose/{node}/{service}/docker-compose.yml - SSH to the target node and run:
cd /mnt/docker-data/compose/{node}/{service} docker compose up -d
Troubleshoot
# On target node:
docker compose -f /mnt/docker-data/compose/{node}/{service}/docker-compose.yml logs --tail 100
docker ps --format "table {{.Names}}\t{{.Status}}"
Check Cluster Health
# On each node:
docker ps # Local containers
Data Management
Storage
All persistent data on NAS at /mnt/docker-data/:
configs/- Application configurationsdatabases/- Database volumesmedia/- Frigate recordingstraefik/- SSL certificates
NAS Dependency
Risk: NAS failure = all services fail (single point of failure)
- NFS mount uses
soft,timeo=300for graceful timeout - Synology should have "Restart automatically after power failure" enabled
- Original data backups remain on local
/data/configs/on hf-pdocker-01
Specialized Agents
vault-credential-manager
Use for: Storing/retrieving credentials from Vaultwarden
documentation-keeper
Use for: Creating/updating documentation in Wiki.js
Known Issues & Workarounds
- NFS root_squash: Database containers need
user: "1024:100"to avoid chown failures on NAS volumes - Telegraf Docker Input: Disabled due to Docker socket permission issues across nodes
- Watchtower API Version: Requires
DOCKER_API_VERSION=1.44 - InfluxDB Connection: Telegraf may show timeout errors if InfluxDB is not running
- Frigate cameras: Cameras on 192.168.44.x subnet may show RTSP timeouts (check camera network)
- Registry container: Stopped on hf-pdocker-01 to free port 5000 for Frigate
- systemd-resolved DNSStubListener: Must be disabled on any node running AdGuard Home (port 53 conflict). Fix:
/etc/systemd/resolved.conf.d/nostub.confwithDNSStubListener=no, thensystemctl restart systemd-resolved. Applied on hf-pdocker-01 and hf-pdocker-02. - NFS boot-mount race (hf-pdocker-01): NFS mounts failed at boot (network not ready in time). Fixed 2026-05-20 by adding
nofail,x-systemd.after=network-online.target,x-systemd.mount-timeout=60sto both fstab entries. If mounts are missing after reboot:sudo mount /mnt/docker-data && sudo mount /mnt/infrastructure. - Guacamole schema: App image pinned to
guacamole/guacamole:1.6.0+guacd:1.6.0with 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. - Gitea image: Pinned to
gitea/gitea:1.26.1with Watchtower disabled (2026-05-20). Watchtower previously pulled broken:latestimages causing missing git binary errors. - Home Assistant reverse proxy: Config at
/mnt/docker-data/configs/homeassistant/configuration.yaml(no hyphen). Must havehttp: use_x_forwarded_for: true+trusted_proxies: [172.16.0.0/12, 127.0.0.1]or proxied requests return 400. - hf-pdocker-01 memory pressure (2026-07-05, retrimmed 2026-07-11): Node runs hot (~15-18/23GB, chronic swap). Mitigations applied:
vm.swappinesslowered 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. 2026-07-11: Frigate's built-in AI features (semantic_search: large,face_recognition,lpr,classification.bird) were found enabled (contradicting the intended baseline) and re-disabled — dropped the embeddings CLIP model ~1.2GB→168MB and cut CPU inference; host mem 18→~9GB, swap 3.9→1.3GB right after. Config/mnt/docker-data/configs/frigate/config/config.yml(backup.bak.20260711-aitrim). Recognition is instead handled externally by clip-service + frigate-notify v2. 2026-07-12 — WHOLE-NODE CRASH: .192 froze hard (~14:15, sshd/HA/DNS all dead, required hard reboot). Cause: Frigatememswap_limithad drifted to 16G vsmem_limit12G, letting it spill ~4GB into host swap; with the ffmpeg leak pinning it at 12G all day (cgroup OOM every ~45min), the host swap-thrashed to death. Fix: node composememswap_limit: 12G(= mem_limit) so Frigate can never use host swap — a hit now trims its own ffmpeg (contained cgroup OOM), not the host. Repo already had 12G; node had drifted (never redeployed). NOTE: recording ffmpeg still leaks ~0.8GB/h atop ~6GB baseline (fills 12G in ~7.5h); twice-daily 4am+4pm restart-frigate cron mitigates but doesn't fully beat it — consider every-6h restart + fixing broken cameras (gate_camera, garage_2). Always keepmemswap_limit == mem_limiton this node. - 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 memorydocker-desktop-unifi-podman-conflict. - 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 withsemaphore user add. Any MySQL DB on NFS is at risk — prefer LOCAL storage like guacamole-db. See memorysemaphore-db-nfs-corruption. - litellm hangs after DB blip: litellm does NOT self-recover from a transient
litellm-dbdisconnect — it loops on "Prisma DB reconnect failed" forever; needs manualdocker restart litellm. Its healthcheck was fixed fromcurl(absent from image, permanent false-unhealthy) to a python urllib check (2026-07-11). - Stale NFS locks after unclean reboot/power loss (2026-07-12 power event): NFS file locks held by pre-reboot client identities survive on the Synology and block relocking — gitea crash-looped 7,972× on its LevelDB queue lock, semaphore-db 975× on
ibdata1. Fix applied 2026-07-13: lock-prone data moved to LOCAL disk (gitea queues →/var/lib/gitea-queues, semaphore-db →/var/lib/semaphore-dbon .100; sonarr config →/var/lib/sonarron bart). Rule: anything holding long-lived file locks (MySQL, SQLite, LevelDB) goes on local disk, never NFS. - Synapse rate-limited Alertmanager notifications: default
rc_message(0.2 msg/s) made every matrix-alertmanager-receiver send fail with M_LIMIT_EXCEEDED 429 — no alerts reached Matrix for weeks while outages accumulated (Onyx down 5 weeks, AdGuard dead 2 months, unnoticed). Fixed 2026-07-13:rc_message: {per_second: 5, burst_count: 50}in homeserver.yaml (backup.bak-20260713-ratelimit). - AdGuard Home unconfigured since 2026-05-17: its conf dir (
/mnt/docker-data/configs/adguardhome/conf) is empty — container runs the first-launch wizard and serves NO DNS. No AdGuardHome.yaml backup exists anywhere. LAN DNS has been coming from elsewhere (router) since May. Needs manual re-setup at http://192.168.86.192:3000. - Duplicate failover stacks removed from .100 and bart (2026-07-13): .100 ran a second monitoring stack (prometheus/grafana/alertmanager/influxdb/blackbox), an idle traefik (shared acme.json with .192 — ACME conflict risk), and a stray guacamole; bart ran a failover guacamole. All removed (~2GB RAM reclaimed). Compose dirs kept for reference; data volumes untouched.
- UniFi OS Server probe port:
unifi-os-serveron .100 serves HTTPS on host port 11443 (container 443), not 8443. Blackbox probe fixed 2026-07-13. solo.hobbs.farm returns 401 (Traefik basicAuth) when healthy — probed viahttp_2xx_or_401module.