- 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.
- 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:
detectorrecord. Reserve the DVR’s address on your router first, before it goes anywhere near the Frigate config. - 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.
- 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.
- 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
motionmode. - 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
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
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.
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=500000000auth: { 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.
(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.
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 captionThe 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.