From d0360c1585964a2aaf7c0ab68d7707f2ced70da9 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sat, 14 Feb 2026 12:39:02 -0800 Subject: [PATCH] Deploy Frigate NVR stack with Mosquitto, Ntfy, and frigate-notify Add four new services for cloud-free camera recording and alerting: - Mosquitto MQTT broker (shared service in mqtt namespace) - Ntfy push notifications (tailnet-accessible) - Frigate NVR with GableCam via HTTP-FLV, ONNX detection, NFS recordings - frigate-notify bridging detection events to Ntfy Also adds Prometheus scrape target, Grafana dashboard, and Caddy reverse proxy entries for nvr.ops.eblu.me and ntfy.ops.eblu.me. Co-Authored-By: Claude Opus 4.6 --- ansible/roles/caddy/defaults/main.yml | 6 + argocd/apps/frigate.yaml | 18 + argocd/apps/mosquitto.yaml | 18 + argocd/apps/ntfy.yaml | 18 + .../manifests/frigate/configmap-notify.yaml | 32 ++ argocd/manifests/frigate/configmap.yaml | 58 +++ .../manifests/frigate/deployment-notify.yaml | 34 ++ argocd/manifests/frigate/deployment.yaml | 80 +++ argocd/manifests/frigate/external-secret.yaml | 22 + .../manifests/frigate/ingress-tailscale.yaml | 26 + argocd/manifests/frigate/kustomization.yaml | 15 + argocd/manifests/frigate/pv-nfs.yaml | 22 + argocd/manifests/frigate/pvc-database.yaml | 13 + argocd/manifests/frigate/pvc-recordings.yaml | 15 + argocd/manifests/frigate/service.yaml | 19 + .../dashboards/configmap-frigate.yaml | 490 ++++++++++++++++++ .../grafana-config/kustomization.yaml | 1 + argocd/manifests/mosquitto/configmap.yaml | 10 + argocd/manifests/mosquitto/deployment.yaml | 47 ++ argocd/manifests/mosquitto/kustomization.yaml | 8 + argocd/manifests/mosquitto/service.yaml | 13 + argocd/manifests/ntfy/deployment.yaml | 48 ++ argocd/manifests/ntfy/ingress-tailscale.yaml | 26 + argocd/manifests/ntfy/kustomization.yaml | 8 + argocd/manifests/ntfy/service.yaml | 13 + argocd/manifests/prometheus/configmap.yaml | 6 + .../changelog.d/deploy-frigate-nvr.feature.md | 1 + 27 files changed, 1067 insertions(+) create mode 100644 argocd/apps/frigate.yaml create mode 100644 argocd/apps/mosquitto.yaml create mode 100644 argocd/apps/ntfy.yaml create mode 100644 argocd/manifests/frigate/configmap-notify.yaml create mode 100644 argocd/manifests/frigate/configmap.yaml create mode 100644 argocd/manifests/frigate/deployment-notify.yaml create mode 100644 argocd/manifests/frigate/deployment.yaml create mode 100644 argocd/manifests/frigate/external-secret.yaml create mode 100644 argocd/manifests/frigate/ingress-tailscale.yaml create mode 100644 argocd/manifests/frigate/kustomization.yaml create mode 100644 argocd/manifests/frigate/pv-nfs.yaml create mode 100644 argocd/manifests/frigate/pvc-database.yaml create mode 100644 argocd/manifests/frigate/pvc-recordings.yaml create mode 100644 argocd/manifests/frigate/service.yaml create mode 100644 argocd/manifests/grafana-config/dashboards/configmap-frigate.yaml create mode 100644 argocd/manifests/mosquitto/configmap.yaml create mode 100644 argocd/manifests/mosquitto/deployment.yaml create mode 100644 argocd/manifests/mosquitto/kustomization.yaml create mode 100644 argocd/manifests/mosquitto/service.yaml create mode 100644 argocd/manifests/ntfy/deployment.yaml create mode 100644 argocd/manifests/ntfy/ingress-tailscale.yaml create mode 100644 argocd/manifests/ntfy/kustomization.yaml create mode 100644 argocd/manifests/ntfy/service.yaml create mode 100644 docs/changelog.d/deploy-frigate-nvr.feature.md diff --git a/ansible/roles/caddy/defaults/main.yml b/ansible/roles/caddy/defaults/main.yml index 15bdd8a..9e2a7a4 100644 --- a/ansible/roles/caddy/defaults/main.yml +++ b/ansible/roles/caddy/defaults/main.yml @@ -76,6 +76,12 @@ caddy_services: - name: cv host: "cv.{{ caddy_domain }}" backend: "https://cv.tail8d86e.ts.net" + - name: nvr + host: "nvr.{{ caddy_domain }}" + backend: "https://nvr.tail8d86e.ts.net" + - name: ntfy + host: "ntfy.{{ caddy_domain }}" + backend: "https://ntfy.tail8d86e.ts.net" - name: sifaka host: "nas.{{ caddy_domain }}" backend: "http://sifaka:5000" diff --git a/argocd/apps/frigate.yaml b/argocd/apps/frigate.yaml new file mode 100644 index 0000000..a90f412 --- /dev/null +++ b/argocd/apps/frigate.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: frigate + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/frigate + destination: + server: https://kubernetes.default.svc + namespace: frigate + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/apps/mosquitto.yaml b/argocd/apps/mosquitto.yaml new file mode 100644 index 0000000..976dd5c --- /dev/null +++ b/argocd/apps/mosquitto.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: mosquitto + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/mosquitto + destination: + server: https://kubernetes.default.svc + namespace: mqtt + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/apps/ntfy.yaml b/argocd/apps/ntfy.yaml new file mode 100644 index 0000000..8846b7f --- /dev/null +++ b/argocd/apps/ntfy.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: ntfy + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/ntfy + destination: + server: https://kubernetes.default.svc + namespace: ntfy + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/manifests/frigate/configmap-notify.yaml b/argocd/manifests/frigate/configmap-notify.yaml new file mode 100644 index 0000000..0fae36a --- /dev/null +++ b/argocd/manifests/frigate/configmap-notify.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: frigate-notify-config + namespace: frigate +data: + config.yml: | + frigate: + server: http://frigate:5000 + + alerts: + general: + title: "Frigate Alert" + filters: + labels: + allow: + - person + - car + - dog + - cat + min_score: 0.6 + + notif: + ntfy: + enabled: true + server: http://ntfy.ntfy.svc.cluster.local:80 + topic: frigate-alerts + headers: {} + + mqtt: + server: mosquitto.mqtt.svc.cluster.local + port: 1883 diff --git a/argocd/manifests/frigate/configmap.yaml b/argocd/manifests/frigate/configmap.yaml new file mode 100644 index 0000000..8503885 --- /dev/null +++ b/argocd/manifests/frigate/configmap.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: frigate-config + namespace: frigate +data: + config.yml: | + mqtt: + host: mosquitto.mqtt.svc.cluster.local + port: 1883 + + go2rtc: + streams: + gablecam: + - "ffmpeg:http://{FRIGATE_CAMERA_USER}:{FRIGATE_CAMERA_PASSWORD}@192.168.1.159/flv?port=1935&app=bcs&stream=channel0_main.bcs#video=copy#audio=copy#audio=opus" + gablecam_sub: + - "ffmpeg:http://{FRIGATE_CAMERA_USER}:{FRIGATE_CAMERA_PASSWORD}@192.168.1.159/flv?port=1935&app=bcs&stream=channel0_sub.bcs" + + cameras: + gablecam: + enabled: true + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/gablecam + input_args: preset-rtsp-restream + roles: [record] + - path: rtsp://127.0.0.1:8554/gablecam_sub + input_args: preset-rtsp-restream + roles: [detect] + detect: + enabled: true + width: 640 + height: 480 + objects: + track: [person, car, dog, cat] + + detectors: + onnx: + type: onnx + + record: + enabled: true + retain: + days: 3 + mode: all + alerts: + retain: + days: 30 + mode: active_objects + detections: + retain: + days: 14 + mode: motion + + snapshots: + enabled: true + retain: + default: 14 diff --git a/argocd/manifests/frigate/deployment-notify.yaml b/argocd/manifests/frigate/deployment-notify.yaml new file mode 100644 index 0000000..6273d71 --- /dev/null +++ b/argocd/manifests/frigate/deployment-notify.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frigate-notify + namespace: frigate +spec: + replicas: 1 + selector: + matchLabels: + app: frigate-notify + template: + metadata: + labels: + app: frigate-notify + spec: + containers: + - name: frigate-notify + image: ghcr.io/0x2142/frigate-notify:v0.3.5 + volumeMounts: + - name: config + mountPath: /app/config.yml + subPath: config.yml + resources: + requests: + memory: "32Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "100m" + volumes: + - name: config + configMap: + name: frigate-notify-config diff --git a/argocd/manifests/frigate/deployment.yaml b/argocd/manifests/frigate/deployment.yaml new file mode 100644 index 0000000..1a9fc39 --- /dev/null +++ b/argocd/manifests/frigate/deployment.yaml @@ -0,0 +1,80 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frigate + namespace: frigate +spec: + replicas: 1 + selector: + matchLabels: + app: frigate + template: + metadata: + labels: + app: frigate + spec: + containers: + - name: frigate + image: ghcr.io/blakeblackshear/frigate:0.15.1 + ports: + - containerPort: 5000 + name: http + - containerPort: 8554 + name: rtsp + - containerPort: 1984 + name: go2rtc + env: + - name: FRIGATE_CAMERA_USER + valueFrom: + secretKeyRef: + name: frigate-camera + key: username + - name: FRIGATE_CAMERA_PASSWORD + valueFrom: + secretKeyRef: + name: frigate-camera + key: password + volumeMounts: + - name: config + mountPath: /config/config.yml + subPath: config.yml + - name: recordings + mountPath: /media/frigate + - name: database + mountPath: /db + - name: shm + mountPath: /dev/shm + resources: + requests: + memory: "256Mi" + cpu: "200m" + limits: + memory: "2Gi" + cpu: "2000m" + livenessProbe: + httpGet: + path: /api/version + port: 5000 + initialDelaySeconds: 30 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /api/version + port: 5000 + initialDelaySeconds: 15 + periodSeconds: 10 + volumes: + - name: config + configMap: + name: frigate-config + - name: recordings + persistentVolumeClaim: + claimName: frigate-recordings + - name: database + persistentVolumeClaim: + claimName: frigate-database + - name: shm + emptyDir: + medium: Memory + sizeLimit: 256Mi diff --git a/argocd/manifests/frigate/external-secret.yaml b/argocd/manifests/frigate/external-secret.yaml new file mode 100644 index 0000000..abe2c92 --- /dev/null +++ b/argocd/manifests/frigate/external-secret.yaml @@ -0,0 +1,22 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: frigate-camera + namespace: frigate +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-blumeops + target: + name: frigate-camera + creationPolicy: Owner + data: + - secretKey: username + remoteRef: + key: Reolink Floodlight Camera + property: username + - secretKey: password + remoteRef: + key: Reolink Floodlight Camera + property: password diff --git a/argocd/manifests/frigate/ingress-tailscale.yaml b/argocd/manifests/frigate/ingress-tailscale.yaml new file mode 100644 index 0000000..f814b70 --- /dev/null +++ b/argocd/manifests/frigate/ingress-tailscale.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: frigate-tailscale + namespace: frigate + annotations: + tailscale.com/proxy-class: "default" + tailscale.com/proxy-group: "ingress" + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "NVR" + gethomepage.dev/group: "Infrastructure" + gethomepage.dev/icon: "frigate.png" + gethomepage.dev/description: "Network video recorder" + gethomepage.dev/href: "https://nvr.ops.eblu.me" + gethomepage.dev/pod-selector: "app=frigate" +spec: + ingressClassName: tailscale + defaultBackend: + service: + name: frigate + port: + number: 5000 + tls: + - hosts: + - nvr diff --git a/argocd/manifests/frigate/kustomization.yaml b/argocd/manifests/frigate/kustomization.yaml new file mode 100644 index 0000000..0610eca --- /dev/null +++ b/argocd/manifests/frigate/kustomization.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: frigate +resources: + - external-secret.yaml + - configmap.yaml + - configmap-notify.yaml + - pv-nfs.yaml + - pvc-recordings.yaml + - pvc-database.yaml + - deployment.yaml + - deployment-notify.yaml + - service.yaml + - ingress-tailscale.yaml diff --git a/argocd/manifests/frigate/pv-nfs.yaml b/argocd/manifests/frigate/pv-nfs.yaml new file mode 100644 index 0000000..d3a592b --- /dev/null +++ b/argocd/manifests/frigate/pv-nfs.yaml @@ -0,0 +1,22 @@ +# NFS PersistentVolume for Frigate recordings +# Requires: NFS share on sifaka at /volume1/frigate with NFS permissions for indri +# +# To create on Synology: +# 1. Control Panel > Shared Folder > Create +# 2. Name: frigate, Location: Volume 1 +# 3. Control Panel > File Services > NFS > NFS Rules +# 4. Add rule for "frigate" share: Hostname=indri, Privilege=Read/Write, Squash=No mapping +apiVersion: v1 +kind: PersistentVolume +metadata: + name: frigate-recordings-nfs-pv +spec: + capacity: + storage: 2Ti + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: "" + nfs: + server: sifaka + path: /volume1/frigate diff --git a/argocd/manifests/frigate/pvc-database.yaml b/argocd/manifests/frigate/pvc-database.yaml new file mode 100644 index 0000000..040bda3 --- /dev/null +++ b/argocd/manifests/frigate/pvc-database.yaml @@ -0,0 +1,13 @@ +# PersistentVolumeClaim for Frigate SQLite database +# Uses minikube's default storage class for local provisioning +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: frigate-database + namespace: frigate +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi diff --git a/argocd/manifests/frigate/pvc-recordings.yaml b/argocd/manifests/frigate/pvc-recordings.yaml new file mode 100644 index 0000000..19c1be8 --- /dev/null +++ b/argocd/manifests/frigate/pvc-recordings.yaml @@ -0,0 +1,15 @@ +# PersistentVolumeClaim for Frigate recordings +# Binds to the NFS PV for sifaka:/volume1/frigate +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: frigate-recordings + namespace: frigate +spec: + accessModes: + - ReadWriteMany + storageClassName: "" + volumeName: frigate-recordings-nfs-pv + resources: + requests: + storage: 2Ti diff --git a/argocd/manifests/frigate/service.yaml b/argocd/manifests/frigate/service.yaml new file mode 100644 index 0000000..28034a4 --- /dev/null +++ b/argocd/manifests/frigate/service.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: frigate + namespace: frigate +spec: + selector: + app: frigate + ports: + - name: http + port: 5000 + targetPort: 5000 + - name: rtsp + port: 8554 + targetPort: 8554 + - name: go2rtc + port: 1984 + targetPort: 1984 diff --git a/argocd/manifests/grafana-config/dashboards/configmap-frigate.yaml b/argocd/manifests/grafana-config/dashboards/configmap-frigate.yaml new file mode 100644 index 0000000..63bd6ca --- /dev/null +++ b/argocd/manifests/grafana-config/dashboards/configmap-frigate.yaml @@ -0,0 +1,490 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard-frigate + namespace: monitoring + labels: + grafana_dashboard: "1" +data: + frigate.json: | + { + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [ + { + "options": { + "0": { "color": "red", "index": 0, "text": "DOWN" } + }, + "type": "value" + }, + { + "options": { + "1": { "color": "green", "index": 1, "text": "UP" } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "red", "value": null }, + { "color": "green", "value": 1 } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 4, "w": 4, "x": 0, "y": 0 }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "frigate_service_info", + "refId": "A" + } + ], + "title": "Frigate Status", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 15 }, + { "color": "red", "value": 30 } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { "h": 4, "w": 4, "x": 4, "y": 0 }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "frigate_detector_inference_speed_seconds{name=\"onnx\"} * 1000", + "refId": "A" + } + ], + "title": "Detector Inference", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 5 }, + { "color": "red", "value": 10 } + ] + }, + "unit": "fps" + }, + "overrides": [] + }, + "gridPos": { "h": 4, "w": 4, "x": 8, "y": 0 }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "frigate_camera_fps{camera_name=\"gablecam\"}", + "legendFormat": "{{type}}", + "refId": "A" + } + ], + "title": "Camera FPS", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [{ "color": "green", "value": null }] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 4, "w": 4, "x": 12, "y": 0 }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "frigate_detection_total_fps", + "refId": "A" + } + ], + "title": "Detection FPS", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [{ "color": "green", "value": null }] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 }, + "id": 5, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { "mode": "multi", "sort": "desc" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "frigate_cpu_usage_percent", + "legendFormat": "{{type}} - {{pid}}", + "refId": "A" + } + ], + "title": "CPU Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [{ "color": "green", "value": null }] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 }, + "id": 6, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { "mode": "multi", "sort": "desc" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "frigate_mem_usage_percent", + "legendFormat": "{{type}} - {{pid}}", + "refId": "A" + } + ], + "title": "Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [{ "color": "green", "value": null }] + }, + "unit": "fps" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 12 }, + "id": 7, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { "mode": "multi", "sort": "desc" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "frigate_camera_fps{camera_name=\"gablecam\"}", + "legendFormat": "{{type}}", + "refId": "A" + } + ], + "title": "Camera FPS Over Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [{ "color": "green", "value": null }] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 12 }, + "id": 8, + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { "mode": "multi", "sort": "desc" } + }, + "pluginVersion": "10.0.0", + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "expr": "frigate_storage_used_bytes", + "legendFormat": "{{storage}}", + "refId": "A" + } + ], + "title": "Storage Usage", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 38, + "tags": ["frigate", "nvr", "camera"], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Frigate NVR", + "uid": "frigate", + "version": 1, + "weekStart": "" + } diff --git a/argocd/manifests/grafana-config/kustomization.yaml b/argocd/manifests/grafana-config/kustomization.yaml index fa06560..18deacc 100644 --- a/argocd/manifests/grafana-config/kustomization.yaml +++ b/argocd/manifests/grafana-config/kustomization.yaml @@ -17,6 +17,7 @@ resources: - dashboards/configmap-postgresql.yaml - dashboards/configmap-services.yaml - dashboards/configmap-zot.yaml + - dashboards/configmap-frigate.yaml - dashboards/configmap-cv-apm.yaml - dashboards/configmap-docs-apm.yaml - dashboards/configmap-flyio.yaml diff --git a/argocd/manifests/mosquitto/configmap.yaml b/argocd/manifests/mosquitto/configmap.yaml new file mode 100644 index 0000000..c0cd870 --- /dev/null +++ b/argocd/manifests/mosquitto/configmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: mosquitto-config + namespace: mqtt +data: + mosquitto.conf: | + listener 1883 + allow_anonymous true + persistence false diff --git a/argocd/manifests/mosquitto/deployment.yaml b/argocd/manifests/mosquitto/deployment.yaml new file mode 100644 index 0000000..34dbc92 --- /dev/null +++ b/argocd/manifests/mosquitto/deployment.yaml @@ -0,0 +1,47 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mosquitto + namespace: mqtt +spec: + replicas: 1 + selector: + matchLabels: + app: mosquitto + template: + metadata: + labels: + app: mosquitto + spec: + containers: + - name: mosquitto + image: eclipse-mosquitto:2 + ports: + - containerPort: 1883 + name: mqtt + volumeMounts: + - name: config + mountPath: /mosquitto/config/mosquitto.conf + subPath: mosquitto.conf + resources: + requests: + memory: "32Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "100m" + livenessProbe: + tcpSocket: + port: 1883 + initialDelaySeconds: 5 + periodSeconds: 30 + readinessProbe: + tcpSocket: + port: 1883 + initialDelaySeconds: 3 + periodSeconds: 10 + volumes: + - name: config + configMap: + name: mosquitto-config diff --git a/argocd/manifests/mosquitto/kustomization.yaml b/argocd/manifests/mosquitto/kustomization.yaml new file mode 100644 index 0000000..5a9cfa1 --- /dev/null +++ b/argocd/manifests/mosquitto/kustomization.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: mqtt +resources: + - configmap.yaml + - deployment.yaml + - service.yaml diff --git a/argocd/manifests/mosquitto/service.yaml b/argocd/manifests/mosquitto/service.yaml new file mode 100644 index 0000000..7a66aa0 --- /dev/null +++ b/argocd/manifests/mosquitto/service.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: mosquitto + namespace: mqtt +spec: + selector: + app: mosquitto + ports: + - name: mqtt + port: 1883 + targetPort: 1883 diff --git a/argocd/manifests/ntfy/deployment.yaml b/argocd/manifests/ntfy/deployment.yaml new file mode 100644 index 0000000..144e698 --- /dev/null +++ b/argocd/manifests/ntfy/deployment.yaml @@ -0,0 +1,48 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ntfy + namespace: ntfy +spec: + replicas: 1 + selector: + matchLabels: + app: ntfy + template: + metadata: + labels: + app: ntfy + spec: + containers: + - name: ntfy + image: binwiederhier/ntfy:v2.11.0 + args: ["serve"] + ports: + - containerPort: 80 + name: http + volumeMounts: + - name: cache + mountPath: /var/cache/ntfy + resources: + requests: + memory: "32Mi" + cpu: "50m" + limits: + memory: "256Mi" + cpu: "200m" + livenessProbe: + httpGet: + path: /v1/health + port: 80 + initialDelaySeconds: 5 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /v1/health + port: 80 + initialDelaySeconds: 3 + periodSeconds: 10 + volumes: + - name: cache + emptyDir: {} diff --git a/argocd/manifests/ntfy/ingress-tailscale.yaml b/argocd/manifests/ntfy/ingress-tailscale.yaml new file mode 100644 index 0000000..fff1731 --- /dev/null +++ b/argocd/manifests/ntfy/ingress-tailscale.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ntfy-tailscale + namespace: ntfy + annotations: + tailscale.com/proxy-class: "default" + tailscale.com/proxy-group: "ingress" + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "Ntfy" + gethomepage.dev/group: "Infrastructure" + gethomepage.dev/icon: "ntfy.png" + gethomepage.dev/description: "Push notifications" + gethomepage.dev/href: "https://ntfy.ops.eblu.me" + gethomepage.dev/pod-selector: "app=ntfy" +spec: + ingressClassName: tailscale + defaultBackend: + service: + name: ntfy + port: + number: 80 + tls: + - hosts: + - ntfy diff --git a/argocd/manifests/ntfy/kustomization.yaml b/argocd/manifests/ntfy/kustomization.yaml new file mode 100644 index 0000000..3e73104 --- /dev/null +++ b/argocd/manifests/ntfy/kustomization.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: ntfy +resources: + - deployment.yaml + - service.yaml + - ingress-tailscale.yaml diff --git a/argocd/manifests/ntfy/service.yaml b/argocd/manifests/ntfy/service.yaml new file mode 100644 index 0000000..7ed6ffb --- /dev/null +++ b/argocd/manifests/ntfy/service.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: ntfy + namespace: ntfy +spec: + selector: + app: ntfy + ports: + - name: http + port: 80 + targetPort: 80 diff --git a/argocd/manifests/prometheus/configmap.yaml b/argocd/manifests/prometheus/configmap.yaml index 0881d2e..39f18be 100644 --- a/argocd/manifests/prometheus/configmap.yaml +++ b/argocd/manifests/prometheus/configmap.yaml @@ -44,3 +44,9 @@ data: - job_name: "kube-state-metrics" static_configs: - targets: ["kube-state-metrics.monitoring.svc.cluster.local:8080"] + + # Frigate NVR metrics + - job_name: "frigate" + static_configs: + - targets: ["frigate.frigate.svc.cluster.local:5000"] + metrics_path: /api/metrics diff --git a/docs/changelog.d/deploy-frigate-nvr.feature.md b/docs/changelog.d/deploy-frigate-nvr.feature.md new file mode 100644 index 0000000..aa068db --- /dev/null +++ b/docs/changelog.d/deploy-frigate-nvr.feature.md @@ -0,0 +1 @@ +Deploy Frigate NVR stack: Frigate for camera recording/detection, Mosquitto MQTT broker, Ntfy for push notifications, and frigate-notify for detection alerting. GableCam connected via HTTP-FLV with ONNX CPU detection, NFS recordings on sifaka, and Grafana dashboard.