Full build of telep-mainframe — the bare-metal Debian 13 box that runs the Frigate NVR for the Telephely site: GPU object detection, event recording, an HDMI camera wall, and Telegram alerting. Cameras live on an isolated VLAN behind telep-router.
For Agents
Access:ssh levander@telep-mainframe (tailnet MagicDNS) or 192.168.1.123 on the LAN. Passwordless sudo.
Stack lives at:~/nvr on the host (docker-compose.yml + frigate/config.yml). A copy is at ~/telep/nvr/ on the Mac.
UI:https://telep-mainframe.taild4189d.ts.net — tailnet only, real Let’s Encrypt cert via Tailscale Serve. Not exposed on the LAN.Detector is onnx, NOT tensorrt — see 2. TensorRT detector is dead on amd64 before touching detector config.
The dual-lens camera’s /cam1/ and /cam2/ paths are the same lens — see 4. The Tapo’s /cam1/ and /cam2/ are the SAME lens. Use /stream1 and /stream6.
Host
This is bare metal, not a VM
systemd-detect-virt → none. There is no hypervisor to fall back on: a bad kernel/driver/modeset change means a physical trip to the site. Treat GPU and display changes accordingly.
The Debian installer ticks the desktop task by default — GNOME came along for the ride and had to be pushed out of the boot path. A machine whose entire job is to never stop recording must also never sleep:
getty@tty1 is disabled — mpv owns tty1 for the TV wall (HDMI). tty2–tty6 and SSH remain available.
Passwordless sudo — accepted tradeoff
/etc/sudoers.d/levander grants NOPASSWD. Convenient for automation, but it means any shell as levander is instantly root. Flagged deliberately; can be removed if the box’s exposure changes.
Architecture
Data flow
graph LR
CAM["<b>Tapo TC47</b> (dual-lens)<br/>192.168.30.119<br/><i>telep-cc VLAN, no internet</i>"]
G2["<b>go2rtc</b><br/>restreams each RTSP once"]
DET["<b>Detect</b><br/>720p sub @ 5fps"]
REC["<b>Record</b><br/>2304x1296 main"]
ONNX["<b>ONNX detector</b><br/>YOLOv9-s 640x640<br/>RTX 3080 · ~12ms"]
DB["<b>/srv/frigate</b><br/>events, 14d"]
BE["<b>Birdseye</b><br/>3840x1080 RTSP"]
MPV["<b>camwall.service</b><br/>mpv → DRM/KMS → HDMI"]
FN["<b>frigate-notify</b><br/>polls HTTP API /15s"]
TG["<b>Telegram</b><br/>Dezsi az őr"]
TS["<b>Tailscale Serve</b><br/>:443 → 127.0.0.1:8971"]
CAM -->|RTSP| G2
G2 --> DET
G2 --> REC
DET --> ONNX
ONNX --> DB
REC --> DB
G2 --> BE --> MPV
DB --> FN --> TG
DB --- TS
style CAM fill:#3d2020,stroke:#c05050,color:#fff
style ONNX fill:#264653,stroke:#2a9d8f,color:#fff
style G2 fill:#2d2d2d,stroke:#888,color:#fff
style DET fill:#2d2d2d,stroke:#888,color:#fff
style REC fill:#2d2d2d,stroke:#888,color:#fff
style DB fill:#264653,stroke:#2a9d8f,color:#fff
style BE fill:#2d2d2d,stroke:#888,color:#fff
style MPV fill:#2d2d2d,stroke:#888,color:#fff
style FN fill:#2d2d2d,stroke:#888,color:#fff
style TG fill:#264653,stroke:#2a9d8f,color:#fff
style TS fill:#264653,stroke:#2a9d8f,color:#fff
The stack (~/nvr)
Two containers, no MQTT broker:
Service
Image
Version
frigate
ghcr.io/blakeblackshear/frigate:stable-tensorrt
Frigate 0.17.2
frigate-notify
ghcr.io/0x2142/frigate-notify:latest
v0.5.4
frigate-notifypolls the Frigate HTTP API every 15s instead of subscribing to MQTT — one less moving part to run and monitor.
The -tensorrt image is still the right image
Despite 2. TensorRT detector is dead on amd64, stable-tensorrt remains correct for NVIDIA — it is simply the CUDA build of Frigate. Only the detector type must be onnx.
Detection
Setting
Value
Detector
onnx
Model
YOLOv9-s @ 640x640
Path
/config/model_cache/yolo.onnx
Labelmap
/labelmap/coco-80.txt
input_tensor
nchw
input_dtype
float
Inference
~12 ms
GPU load
~6%
Tracked objects: person, car.
Video
go2rtc restreams each RTSP stream exactly once — the Tapo caps concurrent RTSP clients, so detect + record + birdseye must all feed from the restream, not from the camera directly.
Detect on the 720p sub stream @ 5 fps; record the 2304x1296 main.
ffmpeg hwaccel_args: preset-nvidia (NVDEC).
Recording
record.retain.days: 0 — events only, no 24/7 continuous recording.
Alerts / detections retained 14 days.
Storage: /srv/frigate.
Networking
All ports bound to 127.0.0.1 only — 8971 (UI), 5000 (internal API), 8554 (RTSP/birdseye). Nothing listens on the LAN.
Access
Tailscale Serve terminates TLS with a real Let’s Encrypt certificate:
Requires HTTPS certs enabled in the tailnet admin console.
Persists across reboots.
Frigate’s own admin login still applies on top of the tailnet ACL — two layers.
iOS: iCloud Private Relay breaks MagicDNS in Safari
*.ts.net resolves to “website not found” on iPhone/iPad with Private Relay (or Limit IP Address Tracking) enabled. Turn it off for the tailnet to resolve. This is a client-side setting, nothing on the server can fix it.
TV wall (HDMI)
A wall-mounted TV shows a live multi-camera view, rendered straight to DRM/KMS with no X server at all.
Frigate birdseye: enabled, restream: true, mode: continuous, 3840x1080, quality 8 → published at rtsp://127.0.0.1:8554/birdseye.
camwall.service (systemd) runs /usr/local/bin/camwall.sh, which is mpv:
Camera name labels are burned in with an ffmpeg drawtext filter — hence nvdec-copy rather than nvdec: a software filter needs the frames back in system memory.
The 3080 exposes NO display connectors until DRM modesetting is on
Out of the box the GPU showed zero connectors — no HDMI, nothing to render to. Fix:
/etc/modprobe.d/nvidia-drm.conf:
options nvidia-drm modeset=1
then update-initramfs -u. It was applied live without a reboot by stopping frigate and reloading nvidia_drm.
lan → cams allowed (this is how Frigate pulls RTSP)
Clock
NTP DNAT’d back to the router so the offline camera does not drift
Hidden-SSID rejoin was actually tested: the camera was deauthed, and it re-associated and kept its lease. Tapo handles hidden SSIDs fine on this firmware.
Consequence: the Tapo app can no longer reach the camera
No cloud (no internet) and the phone sits on a different subnet with client isolation on. Changing any camera setting now requires temporarily reopening the VLAN. Budget for this before planning camera-side changes.
RTSP stream map
Lens
Main
Sub
Fixed
/stream1 (2304x1296)
/stream2 (1280x720)
PTZ
/stream6
/stream7
Alerting
Telegram bot “Dezsi az őr” (@tlphlyortbot) → supergroup Telephely biztonsági riasztások, chat_id -1004475187307.
Credentials: ~/telep/telegram-bot.env on the Mac (mode 600).
frigate-notify alerts on person and car.
Dedup is zone-aware
frigate-notify logs Already notified on this zone when suppressing a duplicate. Defining zones will make dedup precise — right now the whole frame is effectively one zone. See Open Items.
Gotchas
The expensive ones, in the order they cost time.
1. Secure Boot silently blocked the NVIDIA module
DKMS built and signed nvidia.ko with a MOK that was never enrolled, so modprobe nvidia failed with:
Key was rejected by service
Fix chosen: disable Secure Boot in the BIOS. Enrolling the MOK is the “proper” fix, but MOK Manager throws up a blue enrollment screen that requires a physical monitor and keypress — a non-starter on a headless box. Use mokutil --test-key to confirm enrollment state before assuming a signing problem.
2. TensorRT detector is dead on amd64
On Frigate 0.17 the TensorRT detector errors out immediately:
TensorRT detector is no longer supported on amd64 system.
Please use ONNX detector instead.
The -tensorrtimage is still correct (it is the CUDA build) — only the detector must be onnx. Easy to conflate the two and chase the wrong thing.
3. Frigate's YOLOv9 export recipe is broken by PyTorch ≥ 2.6
Frigate’s export Dockerfile pins nothing, so it pulls torch 2.13. Torch 2.6 flipped torch.load to weights_only=True by default, and yolov9’s export.py never passes weights_only=False:
_pickle.UnpicklingError: Weights only load failed
Fix — pin torch, after the requirements install:
RUN uv pip install --system torch==2.5.1 torchvision==0.20.1 \ --index-url https://download.pytorch.org/whl/cpu
4. The Tapo's /cam1/ and /cam2/ are the SAME lens
This was the big one. Probing suggested three working paths — /cam1/stream1, /cam2/stream1 and /stream1 all returned 200. But a PSNR comparison proved /cam1/* and /cam2/* are the same fixed lens (PSNR ≈ 36 → identical scene), while /stream6 is a genuinely different scene (PSNR ≈ 3.9).
Symptoms: the UI showed the same feed twice; every detection produced two identical events and two Telegram alerts; and the PTZ lens moved but was never actually visible anywhere.
Correct mapping: fixed = /stream1 + /stream2; PTZ = /stream6 + /stream7. An HTTP 200 on an RTSP path proves nothing about which sensor is behind it — compare frames.
5. Tapo silently truncates long camera-account passwords
A 28-character password was accepted by the Tapo app but never authenticated (401 forever). A 14-character alphanumeric password worked.
Also worth knowing:
The camera only offers Basic auth — WWW-Authenticate: Basic realm="TP-Link IP-Camera". Not digest.
Reboot the camera after changing the camera account, or RTSP keeps rejecting the new credentials.
6. Frigate keeps its database at /config/frigate.db
Bind-mounting only config.yml into /config leaves the event DB container-local — it is wiped on every docker compose up --force-recreate. Mount the whole directory:
volumes: - ./frigate:/config
7. Birdseye does not draw camera names
There is no putText in birdseye.py. Labels on the TV must be burned in at the player (mpv drawtext). Do not go looking for a Frigate config option — there isn’t one.
8. Birdseye grid geometry is driven by the canvas aspect
On a 16:9 canvas with 2 cameras, birdseye builds a 2x2 grid with an empty dark bottom row. Setting the canvas to 3840x1080 (32:9) forces a single row of two.
Note the inherent limit: two 16:9 cameras can never fill a 16:9 screen without cropping. Some letterboxing is unavoidable.
ANPR / license plates. Frigate 0.17 supports LPR natively (lpr: enabled, device: GPU, known_plates with wildcards/regex). But the current optics cannot resolve a plate — LPR needs roughly 100 px across the plate, and the wide lens at night gives about 20 px. Requires a dedicated entrance camera with illumination; no amount of config fixes this.
Define zones. Nothing is zoned yet — this is what makes frigate-notify’s dedup precise.
End-to-end test: person → Telegram, with a real human in frame. Not yet done.
in-browser Windows NTFS drive browser (/mnt/win read-only)
NVMe device nodes on this box are not stable across reboots
As of 2026-07-21 the 1.8T Windows disk is nvme0n1 (nvme0n1p2 = NTFS) and the 3.6T LUKS+LVM OS disk is nvme1n1 (p1 /boot/efi, p2 /boot ext4, p3 crypto_LUKS) — the reverse of the earlier enumeration. Always lsblk -f before mounting anything; use UUID= in fstab/systemd units. Details and the mount procedure: The Windows drive.
Other drives: sda (447.1G) is essentially unused (one 16M partition, no filesystem); sdb (115.5G) is a Hiren’s BootCD PE rescue USB stick (UDF, label HBCD_PE_x64).
GPU OCR venv (~/ocr/venv)
A reusable marker-pdf install lives at ~/ocr/venv for turning scanned PDFs into markdown on the RTX 3080 (564 pages in ~22 min, ~3.5 GB VRAM, no impact on Frigate).
Its torch was hand-swapped to a cu126 build — do not "fix" it
The default marker-pdf install pulls a cu130 torch that this box’s driver 550.163.01 cannot initialise, so it silently runs on CPU. And do not upgrade the NVIDIA driver to chase CUDA 13 — driver 550 is load-bearing for Frigate detection and the camera wall. Full procedure: Running it on a CUDA GPU box (telep-mainframe).
Also running (no served port): intruder-alarm.service — presence-based auto-arming alarm that watches Frigate detections, plays a TV siren, and escalates intruders to Telegram. See 2026-07-17-intruder-alarm.
Related
2026-07-17-intruder-alarm — auto-arming alarm on this box; watches Frigate, sirens the TV, escalates to Telegram
telep-router — the gateway, the telep-cc camera VLAN, and the tailnet path in
filestash — Windows drive file browser sharing this host’s tailscale serve pattern