zero-stack/ZERO

My homelab watches my cameras, texts me a photo, and went blind without telling me

6 MIN READPOST 2 OF 2
TL;DR
WHAT
An old gaming laptop runs Frigate for a DVR's cameras, detects people and cars on the CPU, and pushes snapshots to my phone over Telegram. I reach the dashboard through Tailscale.
STACK
Frigate 0.18 · Docker · OpenVINO · Caddy · Tailscale · Uptime Kuma · Telegram

the cupboard

My homelab is one laptop in a corner, running about a dozen containers.

One of them is Frigate, an open source NVR that watches camera streams and runs object detection on them. Mine watches the cameras on a four-channel DVR. When a person walks into frame, my phone buzzes with a photo of them and a box drawn around their head.

Then the DVR dropped off the network, and for days the whole system carried on running, healthy and green, watching nothing. That story is below, because it’s the useful part.

what Frigate actually does

Frigate is a pipeline, not just a recorder.

  1. Cameras. Each camera is one or more RTSP streams. Mine come from the DVR, which gives a main stream (sharp, heavy) and a sub stream (small, cheap) per channel. You give each stream a role: detect or record. Reserve the DVR’s address on your router first, before it goes anywhere near the Frigate config.
  2. Detection. Frigate samples the detect stream at low resolution and frame rate and runs it through a model. Here that is 352x288 at 5 fps, an SSDLite MobileNet on OpenVINO, on the laptop CPU. Frigate’s stats showed 10 ms per inference on 6 November.
  3. Events. When the model sees something it cares about with enough confidence, Frigate opens an event with a camera, a label and times. The main camera tracks person, car, motorcycle and dog. The person filter needs a 0.4 minimum score and a 0.5 threshold.
  4. Snapshots and clips. Each event gets a still with the bounding box drawn on, kept 14 days. Alert footage is kept 14 days and plain detections 7, in motion mode.
  5. Notifications. The part Frigate leaves to you. It can publish events over MQTT. I left that off and wrote a small script instead.

The database holds 183 events, mostly people.

how it’s wired

01Cameras + DVRRTSP, main and sub streams
02Frigatedetect, record, events
03alerts.pypolls events, sends a photo
04PhoneTelegram, dashboard over Tailscale
the dashboard is only reachable from the tailnet, through Caddy

Frigate is one container, with a tmpfs cache so ffmpeg isn’t hammering the disk. The unauthenticated API on 5000 listens on localhost only, and the authenticated UI on 8971 is the one that goes out. There’s no Coral stick: OpenVINO on the CPU is plenty for a small frame at 5 fps, and the laptop’s RTX 2060 isn’t involved at all.

Reaching it from the phone

The laptop runs Tailscale and so does my iPhone. A domain I own has DNS-only records pointing at the laptop’s tailnet address, and Caddy gets a real certificate through the DNS challenge, so no port is open to the internet. From the phone the dashboard is an HTTPS address that only resolves for tailnet devices.

The Telegram script

A short poller reads new rows from Frigate’s SQLite database, filters by camera and label, rate limits itself, fetches the snapshot from the local API and sends it through the Telegram Bot API. The journal on 6 November holds 76 sent alerts, though that may not be the full history.

the day it went blind

[FAULT] WHAT BROKE

The DVR changed address and Frigate kept knocking on the old one.

The DVR had no static address or DHCP reservation on the router, so it got a new one. Frigate stayed pointed at the old address and went blind. When I checked, the newest event in the database was days old, pinging the old address got 100% packet loss and both cameras reported 0 fps. Frigate has logged No route to host about 660,000 times, with its watchdog restarting ffmpeg roughly once a second.

Everything else stayed green. The Uptime Kuma monitor named Frigate is a port check, so it’s happy while the container listens. The Telegram script ran with no delivery errors, because there was nothing to report. A silent alert system looks exactly like a quiet street. Nothing told me.

The fix is a DHCP reservation (or a static address) for the DVR on the router, with Frigate pointed at that address.

known gaps

  • No alert for a blind camera. The fix is a monitor on Frigate’s stats endpoint that fires when a camera’s fps is 0, plus the DVR itself, in Uptime Kuma. Not done.
  • Port 8971 is published on all interfaces. It has authentication, but it’s reachable from the LAN. Binding it to localhost would leave Caddy as the only door.
  • No zones. A person anywhere in frame counts, including the road beyond the property. I never counted false positives.

steal this

Frigate’s docs are the reference. These are my files with the private details swapped for variables, as of 6 November 2026 on Frigate 0.18.2.

docker-compose.yml
services:
  frigate:
    image: ghcr.io/blakeblackshear/frigate:stable
    container_name: frigate
    restart: unless-stopped
    shm_size: "256mb"
    env_file: .env            # holds FRIGATE_RTSP_PASSWORD
    ports:
      - "8971:8971"           # authenticated UI
      - "127.0.0.1:5000:5000" # unauthenticated API, local only
    volumes:
      - ./config:/config
      - ./media:/media/frigate
    tmpfs:
      - /tmp/cache:size=500000000
config.yml
auth: { enabled: true }
mqtt: { enabled: false }
detectors:
  ov: { type: openvino, device: CPU }
model:
  width: 300
  height: 300
  input_tensor: nhwc
  input_pixel_format: bgr
  path: /openvino-model/ssdlite_mobilenet_v2.xml
  labelmap_path: /openvino-model/coco_91cl_bkgr.txt
snapshots: { enabled: true, bounding_box: true, retain: { default: 14 } }
cameras:
  cam1:
    ffmpeg:
      inputs:
        - path: rtsp://admin:{FRIGATE_RTSP_PASSWORD}@{FRIGATE_DVR_HOST}:554/{FRIGATE_SUB_PATH}
          roles: [detect]
        - path: rtsp://admin:{FRIGATE_RTSP_PASSWORD}@{FRIGATE_DVR_HOST}:554/{FRIGATE_MAIN_PATH}
          roles: [record]
    detect: { enabled: true, width: 352, height: 288, fps: 5 }
    record:
      enabled: true
      alerts: { retain: { days: 14, mode: motion } }
      detections: { retain: { days: 7, mode: motion } }
    objects:
      track: [person, car, motorcycle, dog]
      filters:
        person: { min_score: 0.4, threshold: 0.5 }

The Caddy block that puts it behind a certificate. Frigate serves its own self-signed TLS on 8971, which is why verification is skipped on that one hop.

Caddyfile
(cf) {
	tls {
		dns cloudflare {env.CF_API_TOKEN}
	}
}

frigate.example.com {
	import cf
	reverse_proxy https://{env.HOST_ADDR}:8971 {
		transport http {
			tls_insecure_skip_verify
		}
	}
}

The alert script, trimmed. The token and chat id come from an env file.

alerts.py
import json, os, subprocess, time

env = dict(l.strip().split("=", 1) for l in open("alerts.env") if "=" in l)
TOKEN, CHAT = env["BOT_TOKEN"], env["CHAT_ID"]
WATCH = {"cam1": {"person", "car", "motorcycle"}, "cam2": {"person"}}
COOLDOWN = 60
seen, last_sent, last_ts = set(), {}, time.time() - 5

def dexec(*args):
    return subprocess.run(["docker", "exec", "frigate", *args],
                          capture_output=True, timeout=30).stdout

def new_events(since):
    code = ("import sqlite3,json;c=sqlite3.connect('/config/frigate.db');"
            "print(json.dumps([dict(zip(('id','camera','label','start_time'),r)) for r in "
            f"c.execute('select id,camera,label,start_time from event where start_time>{since}')]))")
    out = dexec("python3", "-c", code)
    return json.loads(out) if out.strip() else []

# Left out: the loop. For each new event on a watched camera and label, once per COOLDOWN:
#   wait a few seconds, then fetch the snapshot with the box drawn on
#   docker exec frigate curl -s http://127.0.0.1:5000/api/events/<id>/snapshot.jpg?bbox=1
#   POST it to https://api.telegram.org/bot<TOKEN>/sendPhoto with chat_id and a caption

The loop that polls, de-duplicates and sends is the part left out above.

what i’d do differently

  • Reserve addresses first. Cameras and DVRs get a DHCP reservation before they get a Frigate config.
  • Alert on silence. Camera fps of 0 for ten minutes should page me.
  • Monitor the thing I care about. The port check proved the container was alive, never the cameras.

secrets

This site has hidden things. Every one has instructions below, but what it is and what it does stays secret until you find it. You found 0 of 20.

Shortcut: press ? on any page to open this panel. The pixel buddy knows every secret and gives hints.