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-virtnone. 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.

PropertyValue
CPUIntel i9-12900K
RAM62 GB
GPUNVIDIA RTX 3080 12 GB
Disk3.6 TB
OSDebian 13 (trixie)
Kernel6.12.95
Default targetmulti-user.target (no GUI)
NVIDIA driver550.163.01 (Debian nvidia-driver, contrib/non-free)
Docker29.6.1 (Compose v5.3.1)
nvidia-container-toolkit1.19.1
LAN192.168.1.123
Tailnettelep-mainframe / telep-mainframe.taild4189d.ts.net

Headless hardening

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:

systemctl set-default multi-user.target
systemctl mask sleep.target suspend.target hibernate.target \
               hybrid-sleep.target suspend-then-hibernate.target

/etc/systemd/logind.conf.d/99-nvr-headless.conf:

[Login]
HandlePowerKey=ignore
IdleAction=ignore

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:

ServiceImageVersion
frigateghcr.io/blakeblackshear/frigate:stable-tensorrtFrigate 0.17.2
frigate-notifyghcr.io/0x2142/frigate-notify:latestv0.5.4

frigate-notify polls 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

SettingValue
Detectoronnx
ModelYOLOv9-s @ 640x640
Path/config/model_cache/yolo.onnx
Labelmap/labelmap/coco-80.txt
input_tensornchw
input_dtypefloat
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: 0events only, no 24/7 continuous recording.
  • Alerts / detections retained 14 days.
  • Storage: /srv/frigate.

Networking

All ports bound to 127.0.0.1 only8971 (UI), 5000 (internal API), 8554 (RTSP/birdseye). Nothing listens on the LAN.

Access

Tailscale Serve terminates TLS with a real Let’s Encrypt certificate:

tailscale serve --bg --https=443 https+insecure://127.0.0.1:8971
  • 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:
mpv --vo=gpu --gpu-context=drm \
    --drm-device=/dev/dri/card1 --drm-connector=HDMI-A-3 \
    --hwdec=nvdec-copy \
    ...
  • 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.

Camera network

The camera sits on the isolated telep-cc VLAN behind telep-router — see Camera VLAN (telep-cc).

PropertyValue
CameraTP-Link Tapo TC47 (dual-lens: 1 fixed + 1 PTZ)
Address192.168.30.119
SSIDhidden, WPA2-PSK (Tapo does not support WPA3-SAE)
Client isolationon
Internetnonecams → wan rejected
Reachabilitylan → cams allowed (this is how Frigate pulls RTSP)
ClockNTP 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

LensMainSub
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 -tensorrt image 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.

9. iOS Private Relay vs MagicDNS

See Access.

Files on the Mac

All mode 600:

PathContents
~/telep/camera-rtsp.txtcamera RTSP credentials
~/telep/telegram-bot.envTelegram bot token + chat id
~/telep/telep-cc-wifi.txtcamera VLAN WiFi PSK
~/telep/frigate-admin.txtFrigate admin login
~/telep/test-rtsp.pyRTSP probe / auth test
~/telep/rtsp-streams.pystream enumeration + PSNR comparison
~/telep/nvr/copy of the deployed stack

Open Items

  • 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.
  • Consider removing passwordless sudo (/etc/sudoers.d/levander).
  • Router security gaps — see Security Gaps.

Other services on this box

Beyond the NVR, telep-mainframe fronts a few extra apps, each on its own tailscale serve port:

PortServiceNote
:443Frigate UIthis doc
:8443top4 pickercamera-wall image picker
:8445filestashin-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.