Initial Boone stack: observability and automation baseline

This commit is contained in:
farm 2026-03-04 23:17:06 -05:00
commit ba22ab548f
10 changed files with 392 additions and 0 deletions

6
.env.example Normal file
View File

@ -0,0 +1,6 @@
POSTGRES_USER=farmadmin
POSTGRES_PASSWORD=change_this_now
POSTGRES_DB=farm
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=change_this_now
TZ=America/New_York

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# secrets
.env
# persistent runtime data
data/
# macOS
.DS_Store

34
README.md Normal file
View File

@ -0,0 +1,34 @@
# Farm Control Stack
This folder contains a starter central-control stack for your farm:
- PostgreSQL (operational + business data)
- Mosquitto MQTT (sensor/device messaging)
- Grafana (dashboards/alerts)
- Node-RED (automation workflows)
- Prometheus (metrics collection)
## First-run on this Mac
1. Open `Rancher Desktop` from Applications.
2. Complete first-run setup in the GUI.
3. In Rancher Desktop settings, ensure Docker CLI is enabled.
4. Open a new terminal and run:
```bash
cd /Users/farm/blackfish/projects/farm-control
./start.sh
```
## Default endpoints
- Grafana: http://localhost:3000
- Node-RED: http://localhost:1880
- MQTT broker: `localhost:1883`
- PostgreSQL: `localhost:5432`
- Prometheus: http://localhost:9090
- Mac host metrics exporter: http://localhost:9100/metrics
## Mac performance metrics
This Mac is monitored via `node_exporter` (installed with Homebrew and started as a service).
Prometheus scrapes it at `host.docker.internal:9100`, and Grafana uses Prometheus as default datasource.
Grafana also auto-loads a dashboard: `Mac Host Overview` (folder: `Farm Control`).
## Important
Edit `.env` and change default passwords before exposing this machine on a farm network.

85
docker-compose.yml Normal file
View File

@ -0,0 +1,85 @@
name: farm-control
services:
postgres:
image: postgres:16-alpine
container_name: farm-postgres
restart: unless-stopped
env_file: .env
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
- TZ=${TZ}
volumes:
- ./data/postgres:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
mosquitto:
image: eclipse-mosquitto:2
container_name: farm-mqtt
restart: unless-stopped
volumes:
- ./mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
- ./data/mosquitto:/mosquitto/data
- ./data/mosquitto-log:/mosquitto/log
ports:
- "1883:1883"
grafana:
image: grafana/grafana:11.6.0
container_name: farm-grafana
restart: unless-stopped
env_file: .env
environment:
- GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER}
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
- TZ=${TZ}
volumes:
- ./data/grafana:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/etc/grafana/dashboards:ro
ports:
- "3000:3000"
depends_on:
- postgres
- prometheus
nodered:
image: nodered/node-red:4.0.9
container_name: farm-nodered
restart: unless-stopped
environment:
- TZ=${TZ}
volumes:
- ./data/nodered:/data
ports:
- "1880:1880"
depends_on:
- mosquitto
- postgres
prometheus:
image: prom/prometheus:v3.2.1
container_name: farm-prometheus
restart: unless-stopped
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=30d
- --storage.tsdb.retention.size=20GB
volumes:
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./data/prometheus:/prometheus
ports:
- "9090:9090"
networks:
default:
name: farm-control-net

View File

@ -0,0 +1,187 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {"type": "grafana", "uid": "-- Grafana --"},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {"type": "prometheus", "uid": "$datasource"},
"fieldConfig": {
"defaults": {"unit": "percent", "decimals": 1},
"overrides": []
},
"gridPos": {"h": 4, "w": 6, "x": 0, "y": 0},
"id": 1,
"options": {"colorMode": "background", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"},
"targets": [
{
"expr": "(1 - avg(rate(node_cpu_seconds_total{instance=\"$instance\",mode=\"idle\"}[5m]))) * 100",
"refId": "A"
}
],
"title": "CPU Usage",
"type": "stat"
},
{
"datasource": {"type": "prometheus", "uid": "$datasource"},
"fieldConfig": {
"defaults": {"unit": "percent", "decimals": 1},
"overrides": []
},
"gridPos": {"h": 4, "w": 6, "x": 6, "y": 0},
"id": 2,
"options": {"colorMode": "background", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"},
"targets": [
{
"expr": "(1 - ((node_memory_free_bytes{instance=\"$instance\"} + node_memory_inactive_bytes{instance=\"$instance\"} + node_memory_purgeable_bytes{instance=\"$instance\"}) / node_memory_total_bytes{instance=\"$instance\"})) * 100",
"refId": "A"
}
],
"title": "Memory Usage",
"type": "stat"
},
{
"datasource": {"type": "prometheus", "uid": "$datasource"},
"fieldConfig": {
"defaults": {"unit": "percent", "decimals": 1},
"overrides": []
},
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 0},
"id": 3,
"options": {"colorMode": "background", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"},
"targets": [
{
"expr": "100 * (1 - (node_filesystem_avail_bytes{instance=\"$instance\",mountpoint=\"/\",fstype!~\"tmpfs|devfs\"} / node_filesystem_size_bytes{instance=\"$instance\",mountpoint=\"/\",fstype!~\"tmpfs|devfs\"}))",
"refId": "A"
}
],
"title": "Root Disk Usage",
"type": "stat"
},
{
"datasource": {"type": "prometheus", "uid": "$datasource"},
"fieldConfig": {
"defaults": {"unit": "none", "decimals": 2},
"overrides": []
},
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 0},
"id": 4,
"options": {"colorMode": "background", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, "textMode": "auto"},
"targets": [
{
"expr": "node_load5{instance=\"$instance\"}",
"refId": "A"
}
],
"title": "Load (5m)",
"type": "stat"
},
{
"datasource": {"type": "prometheus", "uid": "$datasource"},
"fieldConfig": {"defaults": {"unit": "percentunit", "decimals": 2}, "overrides": []},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 4},
"id": 5,
"options": {"legend": {"displayMode": "list", "placement": "bottom"}, "tooltip": {"mode": "single"}},
"targets": [
{"expr": "1 - avg(rate(node_cpu_seconds_total{instance=\"$instance\",mode=\"idle\"}[5m]))", "legendFormat": "used", "refId": "A"},
{"expr": "avg(rate(node_cpu_seconds_total{instance=\"$instance\",mode=\"idle\"}[5m]))", "legendFormat": "idle", "refId": "B"}
],
"title": "CPU Usage (ratio)",
"type": "timeseries"
},
{
"datasource": {"type": "prometheus", "uid": "$datasource"},
"fieldConfig": {"defaults": {"unit": "bytes", "decimals": 2}, "overrides": []},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 4},
"id": 6,
"options": {"legend": {"displayMode": "list", "placement": "bottom"}, "tooltip": {"mode": "single"}},
"targets": [
{"expr": "node_memory_total_bytes{instance=\"$instance\"} - (node_memory_free_bytes{instance=\"$instance\"} + node_memory_inactive_bytes{instance=\"$instance\"} + node_memory_purgeable_bytes{instance=\"$instance\"})", "legendFormat": "used", "refId": "A"},
{"expr": "node_memory_free_bytes{instance=\"$instance\"} + node_memory_inactive_bytes{instance=\"$instance\"} + node_memory_purgeable_bytes{instance=\"$instance\"}", "legendFormat": "available", "refId": "B"}
],
"title": "Memory Bytes",
"type": "timeseries"
},
{
"datasource": {"type": "prometheus", "uid": "$datasource"},
"fieldConfig": {"defaults": {"unit": "Bps", "decimals": 2}, "overrides": []},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 12},
"id": 7,
"options": {"legend": {"displayMode": "table", "placement": "bottom"}, "tooltip": {"mode": "multi"}},
"targets": [
{"expr": "sum by (device) (rate(node_network_receive_bytes_total{instance=\"$instance\",device!~\"lo0|utun.*|awdl0|llw0|gif0|stf0\"}[5m]))", "legendFormat": "{{device}} rx", "refId": "A"},
{"expr": "sum by (device) (rate(node_network_transmit_bytes_total{instance=\"$instance\",device!~\"lo0|utun.*|awdl0|llw0|gif0|stf0\"}[5m]))", "legendFormat": "{{device}} tx", "refId": "B"}
],
"title": "Network Throughput",
"type": "timeseries"
},
{
"datasource": {"type": "prometheus", "uid": "$datasource"},
"fieldConfig": {"defaults": {"unit": "bytes", "decimals": 2}, "overrides": []},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 12},
"id": 8,
"options": {"legend": {"displayMode": "list", "placement": "bottom"}, "tooltip": {"mode": "single"}},
"targets": [
{"expr": "node_filesystem_size_bytes{instance=\"$instance\",mountpoint=\"/\",fstype!~\"tmpfs|devfs\"}", "legendFormat": "size", "refId": "A"},
{"expr": "node_filesystem_avail_bytes{instance=\"$instance\",mountpoint=\"/\",fstype!~\"tmpfs|devfs\"}", "legendFormat": "available", "refId": "B"}
],
"title": "Root Disk Bytes",
"type": "timeseries"
}
],
"refresh": "15s",
"schemaVersion": 39,
"style": "dark",
"tags": ["farm", "mac", "host"],
"templating": {
"list": [
{
"current": {"selected": false, "text": "Prometheus", "value": "prometheus"},
"hide": 2,
"label": "datasource",
"name": "datasource",
"options": [],
"query": "prometheus",
"refresh": 1,
"type": "datasource"
},
{
"current": {"selected": false, "text": "host.docker.internal:9100", "value": "host.docker.internal:9100"},
"datasource": {"type": "prometheus", "uid": "$datasource"},
"definition": "label_values(node_uname_info, instance)",
"hide": 0,
"includeAll": false,
"label": "Instance",
"multi": false,
"name": "instance",
"options": [],
"query": {"qryType": 1, "query": "label_values(node_uname_info, instance)", "refId": "PromVar"},
"refresh": 2,
"sort": 1,
"type": "query"
}
]
},
"time": {"from": "now-6h", "to": "now"},
"timepicker": {},
"timezone": "",
"title": "Mac Host Overview",
"uid": "mac-host-overview",
"version": 1,
"weekStart": ""
}

View File

@ -0,0 +1,11 @@
apiVersion: 1
providers:
- name: 'Farm Dashboards'
orgId: 1
folder: 'Farm Control'
type: file
disableDeletion: false
editable: true
options:
path: /etc/grafana/dashboards

View File

@ -0,0 +1,10 @@
apiVersion: 1
datasources:
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true

View File

@ -0,0 +1,11 @@
global:
scrape_interval: 15s
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ['prometheus:9090']
- job_name: mac_host
static_configs:
- targets: ['host.docker.internal:9100']

5
mosquitto/mosquitto.conf Normal file
View File

@ -0,0 +1,5 @@
persistence true
persistence_location /mosquitto/data/
log_dest stdout
listener 1883
allow_anonymous true

35
start.sh Executable file
View File

@ -0,0 +1,35 @@
#!/usr/bin/env zsh
set -euo pipefail
RD_BIN_DIR="/Applications/Rancher Desktop.app/Contents/Resources/resources/darwin/bin"
export PATH="$RD_BIN_DIR:$PATH"
DOCKER_BIN=""
if command -v docker >/dev/null 2>&1; then
DOCKER_BIN="$(command -v docker)"
elif [ -x "$RD_BIN_DIR/docker" ]; then
DOCKER_BIN="$RD_BIN_DIR/docker"
else
echo "docker CLI not found."
echo "Enable Docker CLI in Rancher Desktop Preferences > Application and reopen terminal."
exit 1
fi
if ! "$DOCKER_BIN" info >/dev/null 2>&1; then
echo "Docker runtime is not ready. Open Rancher Desktop and wait for status 'Running'."
exit 1
fi
cd "$(dirname "$0")"
"$DOCKER_BIN" compose pull
"$DOCKER_BIN" compose up -d
"$DOCKER_BIN" compose ps
cat <<MSG
Farm stack started.
- Grafana: http://localhost:3000
- Node-RED: http://localhost:1880
- MQTT: mqtt://localhost:1883
- Postgres: localhost:5432
MSG