diff --git a/CLAUDE.md b/CLAUDE.md index 7b7b41d..56df862 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ This repository manages a **homelab infrastructure** with self-hosted services r **Nodes:** - **hf-pdocker-01** (192.168.86.192) - Swarm Manager, core infrastructure + Frigate NVR - **RESTORED (2026-03-08)** -- **hf-pdocker-02** (192.168.86.100) - Standalone Compose services (dev tools, utilities) + **FAILOVER: Traefik, Monitoring, AdGuard** +- **hf-pdocker-02** (192.168.86.100) - Standalone Compose services (dev tools, utilities) - **bart** (192.168.86.167) - Standalone Compose services (media stack, OpenVAS) **Synology NAS** (192.168.86.30) - Centralized NFS storage for all service data @@ -23,53 +23,39 @@ This repository manages a **homelab infrastructure** with self-hosted services r hf-pdocker-01 (Manager, 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-Notify v2 (:5199) -- MQTT->AI->Matrix notifications -├── [COMPOSE] Home Assistant (:8123) -- MIGRATED BACK 2026-03-08 -├── [COMPOSE] ESPHome (:6052) -- MIGRATED BACK 2026-03-08 -├── [COMPOSE] Mosquitto (:1883, :8883) -- MIGRATED BACK 2026-03-08 -├── [COMPOSE] Farm Agent (:8889) -- image built locally from Gitea +├── [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:18791, clippy:18792, bob:18800) +├── [COMPOSE] Traefik (reverse proxy, ports 80/443/8090) +├── [COMPOSE] AdGuard Home (:3000, :53) +├── [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) ├── [SWARM] Telegraf (global), Node-exporter (global) ├── [CRON] sync-frigate-to-nas (daily 2am), cleanup-frigate-media (weekly Sun 3am) ├── 5TB USB disk at /mnt/frigate-recordings (Frigate recordings storage) -[Failover services remain on hf-pdocker-02/bart until Phase 2] -bart (Worker, 15GB RAM, i5-4570T, HDD) -- ALSO HOSTING FAILOVER SERVICES +bart (Worker, 15GB RAM, i5-4570T, HDD) ├── [COMPOSE] Media Stack (Jellyfin, Sonarr, Radarr, SABnzbd, NZBHydra2, Jellyseerr) ├── [COMPOSE] OpenVAS (vulnerability scanner) ├── [COMPOSE] Farm Agent (:8889) ├── [COMPOSE] Watchtower ├── [SWARM] Telegraf (global), Node-exporter (global) -├── [COMPOSE] Synapse/Matrix (:8008) + synapse-db (postgres) + wellknown (:8010) -- FAILOVER from hf-pdocker-01 -├── [COMPOSE] Guacamole (:8199) + guacd + guacamole-db (mysql, LOCAL /var/lib/guacamole-db) -- FAILOVER from hf-pdocker-01 -├── [COMPOSE] AI Agents (margaret, spirit, bob, clippy) -- FAILOVER from hf-pdocker-01 (2026-03-07) -hf-pdocker-02 (Worker, 7.6GB RAM, i7-6700T, HDD) -- PRIMARY NODE DURING FAILOVER -├── [COMPOSE] Traefik (reverse proxy, ports 80/443/8090) -- FAILOVER from hf-pdocker-01 -├── [COMPOSE] Monitoring (Grafana :3001, Prometheus :9095, Alertmanager :9093, InfluxDB :8086, Blackbox :9115, matrix-receiver :9094) -- FAILOVER -├── [COMPOSE] AdGuard Home (:3000, :53) -- FAILOVER +hf-pdocker-02 (Worker, 7.6GB RAM, i7-6700T, HDD) ├── [COMPOSE] Unifi + unifi-db (MongoDB) ├── [COMPOSE] Gitea + gitea-db (PostgreSQL) ├── [COMPOSE] Semaphore + semaphore-db (MySQL) ├── [COMPOSE] Wiki.js + wikijs-db (PostgreSQL) ├── [COMPOSE] Vaultwarden, Actual Budget, MCPO, Trivy +├── [COMPOSE] NetClaw/Aaron (:1179) -- AI network agent ├── [COMPOSE] Farm Agent (:8889) ├── [COMPOSE] Watchtower -├── [SWARM] Telegraf (global), Node-exporter (global), Portainer +├── [SWARM] Telegraf (global), Node-exporter (global) -Services restored to hf-pdocker-01 (post-recovery): -- Frigate NVR (:5000) -- back on hf-pdocker-01 with GPU -- Frigate-Notify v2 (:5199) -- deployed 2026-03-08 -- Farm Agent (:8889) -- Mosquitto MQTT (:1883, :8883) -- migrated back 2026-03-08 -- Home Assistant (:8123) -- migrated back 2026-03-08 -- ESPHome (:6052) -- migrated back 2026-03-08 -Services still on failover nodes (not yet returned): -- Matrix/Synapse -- still on bart -- Guacamole -- still on bart -- AI Agents -- still on bart -- Traefik -- still on hf-pdocker-02 -- Monitoring stack -- still on hf-pdocker-02 -- AdGuard Home -- still on hf-pdocker-02 -- Farm Manager API (not yet migrated back) ``` ## Working with This Repository @@ -91,8 +77,8 @@ All service data lives on the **Synology NAS** via NFS, mounted at `/mnt/docker- ├── media/ # Frigate recordings ├── traefik/ # Let's Encrypt certs └── compose/ # Standalone compose files per node - ├── hf-pdocker-01/{frigate,frigate-notify}/ - ├── hf-pdocker-02/{unifi,gitea,semaphore,wikijs,vaultwarden,actual-budget,mcpo,trivy,watchtower}/ + ├── hf-pdocker-01/{frigate,frigate-notify,matrix,smarthome,guacamole,ai-agents,traefik,adguard}/ + ├── hf-pdocker-02/{unifi,gitea,semaphore,wikijs,vaultwarden,actual-budget,mcpo,trivy,netclaw,watchtower}/ └── bart/{media,openvas,watchtower}/ ``` @@ -163,9 +149,18 @@ compose/ # Standalone Compose definitions (deployed per-node) - 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 -- AdGuard: :3000 -- Traefik Dashboard: :8090 +- Prometheus: :9095 +- Alertmanager: :9093 +- InfluxDB: :8086 +- Blackbox Exporter: :9115 +- matrix-alertmanager-receiver: :9094 +- AI Agents (margaret :18790, spirit :18791, clippy :18792, bob :18800) -- loopback only - Farm Manager: :8888 (dashboard + API) - Farm Agent: :8889 @@ -179,6 +174,7 @@ compose/ # Standalone Compose definitions (deployed per-node) - Actual Budget: :5006 - MCPO: :8001 - Trivy: :4954 +- NetClaw/Aaron: :1179 (internal only) **bart (192.168.86.167):** - Farm Agent: :8889 @@ -189,7 +185,6 @@ compose/ # Standalone Compose definitions (deployed per-node) - NZBHydra2: :5076 - Jellyseerr: :5055 - OpenVAS: :9392 -- AI Agents (margaret :18790, spirit :18791, clippy :18792, bob :18800) -- loopback only, failover from hf-pdocker-01 **GPU Machine (192.168.86.196, RTX 3090):** - Ollama: :11434 (vision LLM, llava:13b) @@ -305,3 +300,4 @@ All persistent data on NAS at `/mnt/docker-data/`: 4. **InfluxDB Connection**: Telegraf may show timeout errors if InfluxDB is not running 5. **Frigate cameras**: Cameras on 192.168.44.x subnet may show RTSP timeouts (check camera network) 6. **Registry container**: Stopped on hf-pdocker-01 to free port 5000 for Frigate +7. **systemd-resolved DNSStubListener**: Must be disabled on any node running AdGuard Home (port 53 conflict). Fix: `/etc/systemd/resolved.conf.d/nostub.conf` with `DNSStubListener=no`, then `systemctl restart systemd-resolved`. Applied on hf-pdocker-01 and hf-pdocker-02. diff --git a/compose/hf-pdocker-01/adguard/docker-compose.yml b/compose/hf-pdocker-01/adguard/docker-compose.yml new file mode 100644 index 0000000..fd7b2d3 --- /dev/null +++ b/compose/hf-pdocker-01/adguard/docker-compose.yml @@ -0,0 +1,26 @@ +services: + adguardhome: + image: adguard/adguardhome:latest + container_name: adguardhome + restart: unless-stopped + volumes: + - /mnt/docker-data/configs/adguardhome/work:/opt/adguardhome/work + - /mnt/docker-data/configs/adguardhome/conf:/opt/adguardhome/conf + ports: + - "53:53/tcp" + - "53:53/udp" + - "3000:3000/tcp" + - "853:853/tcp" + mem_limit: 256M + memswap_limit: 256M + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/ || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" diff --git a/compose/hf-pdocker-01/farm-manager/docker-compose.yml b/compose/hf-pdocker-01/farm-manager/docker-compose.yml new file mode 100644 index 0000000..8ea8eec --- /dev/null +++ b/compose/hf-pdocker-01/farm-manager/docker-compose.yml @@ -0,0 +1,25 @@ +services: + farm-manager: + image: farm-manager:latest + container_name: farm-manager + restart: unless-stopped + ports: + - "8888:8888" + environment: + - CONFIG_PATH=/app/config.json + - GROUPS_PATH=/data/groups.json + volumes: + - /mnt/docker-data/configs/farm-manager:/data + mem_limit: 256M + memswap_limit: 256M + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:8888/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" diff --git a/compose/hf-pdocker-01/frigate-notify/docker-compose.yml b/compose/hf-pdocker-01/frigate-notify/docker-compose.yml index c4b812c..b63bc6c 100755 --- a/compose/hf-pdocker-01/frigate-notify/docker-compose.yml +++ b/compose/hf-pdocker-01/frigate-notify/docker-compose.yml @@ -9,9 +9,9 @@ services: environment: - TZ=America/Chicago - FRIGATE_URL=http://localhost:5000 - - MQTT_HOST=192.168.86.167 + - MQTT_HOST=192.168.86.192 - MQTT_PORT=1883 - - MATRIX_HOMESERVER=http://192.168.86.167:8008 + - MATRIX_HOMESERVER=http://192.168.86.192:8008 - CLIP_SERVICE_URL=http://192.168.86.196:5200 - OLLAMA_URL=http://192.168.86.196:11434 - OLLAMA_MODEL=llava:13b diff --git a/compose/hf-pdocker-01/guacamole/docker-compose.yml b/compose/hf-pdocker-01/guacamole/docker-compose.yml new file mode 100644 index 0000000..cab7aea --- /dev/null +++ b/compose/hf-pdocker-01/guacamole/docker-compose.yml @@ -0,0 +1,82 @@ +services: + guacamole: + image: guacamole/guacamole:latest + container_name: guacamole + restart: unless-stopped + environment: + - GUACD_HOSTNAME=guacd + - MYSQL_HOSTNAME=guacamole-db + - MYSQL_DATABASE=guacamole_db + - MYSQL_USER=guacamole_user + - MYSQL_PASSWORD=guacamole_pass + ports: + - "8199:8080" + networks: + - guacamole-internal + depends_on: + guacamole-db: + condition: service_healthy + mem_limit: 512M + memswap_limit: 512M + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/guacamole/ || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" + + guacd: + image: guacamole/guacd:latest + container_name: guacd + restart: unless-stopped + networks: + - guacamole-internal + mem_limit: 512M + memswap_limit: 512M + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" + + guacamole-db: + image: mysql:8.0 + container_name: guacamole-db + user: "1024:100" + restart: unless-stopped + command: --innodb-use-native-aio=0 --innodb-buffer-pool-size=128M + environment: + - MYSQL_ROOT_PASSWORD=guacamole_root + - MYSQL_DATABASE=guacamole_db + - MYSQL_USER=guacamole_user + - MYSQL_PASSWORD=guacamole_pass + volumes: + # NFS cannot be used for MySQL due to persistent NLM lock issues (ibdata1, dblwr files) + # guacamole-db uses LOCAL storage on hf-pdocker-01 + - /var/lib/guacamole-db:/var/lib/mysql + networks: + - guacamole-internal + mem_limit: 512M + memswap_limit: 512M + labels: + - "com.centurylinklabs.watchtower.enable=false" + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 --silent"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" + +networks: + guacamole-internal: + driver: bridge diff --git a/compose/hf-pdocker-01/matrix/docker-compose.yml b/compose/hf-pdocker-01/matrix/docker-compose.yml new file mode 100644 index 0000000..045c68e --- /dev/null +++ b/compose/hf-pdocker-01/matrix/docker-compose.yml @@ -0,0 +1,86 @@ +services: + synapse: + image: matrixdotorg/synapse:latest + container_name: synapse + restart: unless-stopped + environment: + - SYNAPSE_SERVER_NAME=mess.hobbs.farm + - SYNAPSE_REPORT_STATS=no + volumes: + - /mnt/docker-data/configs/matrix/data:/data + ports: + - "8008:8008" + networks: + - matrix-internal + depends_on: + synapse-db: + condition: service_healthy + mem_limit: 1G + memswap_limit: 1G + healthcheck: + test: ["CMD-SHELL", "curl -fSs http://localhost:8008/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" + + wellknown: + image: nginx:alpine + container_name: matrix-wellknown + restart: unless-stopped + volumes: + - /mnt/docker-data/configs/matrix/wellknown:/usr/share/nginx/html/.well-known/matrix:ro + ports: + - "8010:80" + mem_limit: 64M + memswap_limit: 64M + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:80/ || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" + + synapse-db: + image: postgres:15-alpine + container_name: synapse-db + user: "1024:100" + restart: unless-stopped + environment: + - POSTGRES_USER=synapse + - POSTGRES_PASSWORD=synapse + - POSTGRES_DB=synapse + - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C + volumes: + - /mnt/docker-data/databases/matrix-db:/var/lib/postgresql/data + networks: + - matrix-internal + mem_limit: 256M + memswap_limit: 256M + labels: + - "com.centurylinklabs.watchtower.enable=false" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U synapse -d synapse"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" + +networks: + matrix-internal: + driver: bridge diff --git a/compose/hf-pdocker-01/monitoring/docker-compose.yml b/compose/hf-pdocker-01/monitoring/docker-compose.yml new file mode 100644 index 0000000..0552447 --- /dev/null +++ b/compose/hf-pdocker-01/monitoring/docker-compose.yml @@ -0,0 +1,153 @@ +networks: + monitoring-net: + name: monitoring-net + +services: + grafana: + image: grafana/grafana:latest + container_name: grafana + restart: unless-stopped + user: "104" + networks: + - monitoring-net + ports: + - "3001:3000" + volumes: + - /mnt/docker-data/configs/grafana:/var/lib/grafana + mem_limit: 512M + memswap_limit: 512M + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" + + prometheus: + image: prom/prometheus:latest + container_name: prometheus + restart: unless-stopped + networks: + - monitoring-net + ports: + - "9095:9090" + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.console.libraries=/usr/share/prometheus/console_libraries' + - '--web.console.templates=/usr/share/prometheus/consoles' + - '--web.enable-lifecycle' + volumes: + - /mnt/docker-data/configs/prometheus:/etc/prometheus + mem_limit: 512M + memswap_limit: 512M + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:9090/-/healthy || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" + + alertmanager: + image: prom/alertmanager:latest + container_name: alertmanager + restart: unless-stopped + networks: + - monitoring-net + ports: + - "9093:9093" + command: + - '--config.file=/etc/alertmanager/alertmanager.yml' + - '--storage.path=/alertmanager' + volumes: + - /mnt/docker-data/configs/alertmanager:/etc/alertmanager + - alertmanager-data:/alertmanager + mem_limit: 256M + memswap_limit: 256M + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:9093/-/healthy || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" + + influxdb: + image: influxdb:1.8-alpine + container_name: influxdb + restart: unless-stopped + networks: + - monitoring-net + ports: + - "8086:8086" + volumes: + - /mnt/docker-data/configs/influxdb:/var/lib/influxdb + mem_limit: 1G + memswap_limit: 1G + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:8086/ping || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" + + matrix-alertmanager-receiver: + image: metio/matrix-alertmanager-receiver:2026.2.25 + container_name: matrix-alertmanager-receiver + restart: unless-stopped + networks: + - monitoring-net + ports: + - "9094:9094" + volumes: + - /mnt/docker-data/configs/alertmanager/matrix-receiver-config.yml:/config/config.yml:ro + command: ["--config-path", "/config/config.yml"] + mem_limit: 128M + memswap_limit: 128M + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" + + blackbox-exporter: + image: prom/blackbox-exporter:latest + container_name: blackbox-exporter + restart: unless-stopped + networks: + - monitoring-net + ports: + - "9115:9115" + command: + - '--config.file=/config/blackbox.yml' + volumes: + - /mnt/docker-data/configs/blackbox:/config:ro + mem_limit: 128M + memswap_limit: 128M + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" + +volumes: + alertmanager-data: diff --git a/compose/hf-pdocker-01/traefik/.env.example b/compose/hf-pdocker-01/traefik/.env.example new file mode 100644 index 0000000..e9b09a7 --- /dev/null +++ b/compose/hf-pdocker-01/traefik/.env.example @@ -0,0 +1,3 @@ +# Cloudflare DNS API token for Let's Encrypt DNS challenge +# Actual value stored in Vaultwarden as "Traefik CF_DNS_API_TOKEN" +CF_DNS_API_TOKEN=your-cloudflare-api-token-here diff --git a/compose/hf-pdocker-01/traefik/docker-compose.yml b/compose/hf-pdocker-01/traefik/docker-compose.yml new file mode 100644 index 0000000..b57349c --- /dev/null +++ b/compose/hf-pdocker-01/traefik/docker-compose.yml @@ -0,0 +1,39 @@ +services: + traefik: + image: traefik:v3.6 + container_name: traefik + restart: unless-stopped + command: + - "--api.insecure=true" + - "--providers.file.directory=/config/dynamic" + - "--providers.file.watch=true" + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + - "--entrypoints.web.http.redirections.entrypoint.to=websecure" + - "--entrypoints.web.http.redirections.entrypoint.scheme=https" + - "--certificatesresolvers.letsencrypt.acme.dnschallenge=true" + - "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare" + - "--certificatesresolvers.letsencrypt.acme.email=jeremydanielhobbs@gmail.com" + - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" + environment: + - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN} + ports: + - "80:80" + - "443:443" + - "8090:8080" + volumes: + - /mnt/docker-data/traefik/letsencrypt:/letsencrypt + - /mnt/docker-data/traefik/dynamic:/config/dynamic:ro + mem_limit: 512M + memswap_limit: 512M + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:8080/api/overview || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" diff --git a/docs/plans/2026-03-08-hf-pdocker-01-phase2-migration.md b/docs/plans/2026-03-08-hf-pdocker-01-phase2-migration.md new file mode 100644 index 0000000..b00126f --- /dev/null +++ b/docs/plans/2026-03-08-hf-pdocker-01-phase2-migration.md @@ -0,0 +1,108 @@ +# hf-pdocker-01 Phase 2 — Service Migration Back + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Migrate all failover services back to hf-pdocker-01 from hf-pdocker-02 and bart. + +**Architecture:** Stop services on failover nodes, start on hf-pdocker-01, update IP references and routing. Order matters due to dependencies. + +**Dependency Chain:** +- Mosquitto must be up before Home Assistant and Frigate MQTT config update +- Matrix/Synapse must be up before AI Agents +- Traefik must be up before external routing works +- AdGuard is DNS — move carefully to minimize resolution downtime + +--- + +### Task 1: Migrate Mosquitto (bart → hf-pdocker-01) + +MQTT broker — Home Assistant and Frigate depend on it. + +1. Deploy Mosquitto on hf-pdocker-01 using compose from `/mnt/docker-data/compose/bart/smarthome/` (extract mosquitto service) or create standalone compose +2. Verify MQTT is listening on hf-pdocker-01:1883 +3. Update Frigate config MQTT host back to 192.168.86.192 (localhost) +4. Restart Frigate to pick up new MQTT +5. Stop Mosquitto on bart + +### Task 2: Migrate Home Assistant + ESPHome (bart → hf-pdocker-01) + +Smart home services — depend on Mosquitto. + +1. Deploy HA + ESPHome on hf-pdocker-01 +2. Update HA config: MQTT broker IP back to 192.168.86.192 +3. Verify HA web UI on :8123, ESPHome on :6052 +4. Stop HA + ESPHome on bart + +### Task 3: Migrate Matrix/Synapse (bart → hf-pdocker-01) + +Matrix homeserver — AI agents and alertbot depend on it. + +1. Deploy Synapse + synapse-db + wellknown on hf-pdocker-01 +2. Verify Matrix federation and client API on :8008 +3. Update matrix-alertmanager-receiver config to point to new Synapse +4. Stop Synapse stack on bart + +### Task 4: Migrate Guacamole (bart → hf-pdocker-01) + +Remote desktop gateway — has local MySQL DB on bart (NFS lock workaround). + +1. Copy guacamole-db data from bart:/var/lib/guacamole-db to hf-pdocker-01 local storage +2. Deploy Guacamole + guacd + guacamole-db on hf-pdocker-01 +3. Verify web UI on :8199 +4. Stop Guacamole stack on bart + +### Task 5: Migrate AI Agents (bart → hf-pdocker-01) + +margaret, spirit, bob, clippy — depend on Matrix/Synapse. + +1. Build margaret-openclaw:latest image on hf-pdocker-01 +2. Update agent configs: Matrix homeserver URL back to 192.168.86.192:8008 +3. Rename Matrix account state dirs +4. Deploy all 4 agents on hf-pdocker-01 +5. Verify Matrix connectivity +6. Stop agents on bart + +### Task 6: Migrate AdGuard Home (hf-pdocker-02 → hf-pdocker-01) + +DNS server — brief downtime expected during switchover. + +1. Deploy AdGuard on hf-pdocker-01 using NAS config +2. Verify DNS resolution on :53 and web UI on :3000 +3. Stop AdGuard on hf-pdocker-02 + +### Task 7: Migrate Traefik (hf-pdocker-02 → hf-pdocker-01) + +Reverse proxy — handles all external HTTPS routing. + +1. Deploy Traefik on hf-pdocker-01 with NAS certs and dynamic configs +2. Update dynamic configs for new service IPs (services now on .192 instead of .167) +3. Verify HTTPS routing for mess/solo/yoda/vault/git.hobbs.farm +4. Stop Traefik on hf-pdocker-02 + +### Task 8: Migrate Monitoring Stack (hf-pdocker-02 → hf-pdocker-01) + +Grafana, Prometheus, Alertmanager, InfluxDB, Blackbox, matrix-receiver. + +1. Deploy full monitoring stack on hf-pdocker-01 +2. Update Prometheus config with correct service IPs (most services now back on .192) +3. Update Telegraf configs on all nodes to point InfluxDB to .192 +4. Verify Grafana on :3001, Prometheus on :9095 +5. Stop monitoring stack on hf-pdocker-02 + +### Task 9: Migrate Farm Manager API (new deploy on hf-pdocker-01) + +Was never failed over — needs fresh deploy. + +1. Clone farm-manager from Gitea, build server image +2. Deploy Farm Manager API on :8888 +3. Update config.json with correct node IPs +4. Verify dashboard and API + +### Task 10: Cleanup + Final Verification + +1. Remove stopped failover containers on bart and hf-pdocker-02 +2. Update all Prometheus blackbox targets for final service locations +3. Hot-reload Prometheus +4. Update Traefik dynamic configs +5. Verify all services healthy +6. Update CLAUDE.md, MEMORY.md, wiki