From 825d861f82aeee6edf710c3c3e415d745fc85c66 Mon Sep 17 00:00:00 2001 From: Nicolas MASSE Date: Sun, 1 Nov 2026 15:13:17 +0100 Subject: [PATCH] frigate: network video recorder with object detection Upstream image pinned to 0.18.2 (amd64 + arm64), configuration and recordings on the virtiofs mount, configuration seeded once and owned by Frigate afterwards. The unauthenticated listeners of Frigate (5000) and go2rtc (1984, 8554) are bound to the loopback by the seed. Detection with OpenVINO on the CPU, the default of a new 0.18 configuration. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PrUtBTGszYnsSBcxNKUTjF --- cookbooks/frigate/Makefile | 10 +++ cookbooks/frigate/README.md | 100 ++++++++++++++++++++++ cookbooks/frigate/config/config.yml | 42 +++++++++ cookbooks/frigate/config/init.sh | 20 +++++ cookbooks/frigate/frigate-init.service | 24 ++++++ cookbooks/frigate/frigate.container | 79 +++++++++++++++++ cookbooks/frigate/frigate.image | 27 ++++++ cookbooks/frigate/frigate.target | 11 +++ cookbooks/frigate/tmpfiles.d/frigate.conf | 9 ++ 9 files changed, 322 insertions(+) create mode 100644 cookbooks/frigate/Makefile create mode 100644 cookbooks/frigate/README.md create mode 100644 cookbooks/frigate/config/config.yml create mode 100755 cookbooks/frigate/config/init.sh create mode 100644 cookbooks/frigate/frigate-init.service create mode 100644 cookbooks/frigate/frigate.container create mode 100644 cookbooks/frigate/frigate.image create mode 100644 cookbooks/frigate/frigate.target create mode 100644 cookbooks/frigate/tmpfiles.d/frigate.conf diff --git a/cookbooks/frigate/Makefile b/cookbooks/frigate/Makefile new file mode 100644 index 0000000..4b955f5 --- /dev/null +++ b/cookbooks/frigate/Makefile @@ -0,0 +1,10 @@ +## +## Makefile for Frigate quadlet +## + +# Frigate runs as root inside its container (s6-overlay, nginx, go2rtc), so its +# files belong to root on the host. PROJECT_UID and PROJECT_GID keep their +# default, 0. + +# Include common Makefile +include ../../scripts/common.mk diff --git a/cookbooks/frigate/README.md b/cookbooks/frigate/README.md new file mode 100644 index 0000000..c9ecff1 --- /dev/null +++ b/cookbooks/frigate/README.md @@ -0,0 +1,100 @@ +# Podman Quadlet: Frigate + +## Overview + +[Frigate](https://frigate.video/) is a network video recorder with object detection for IP cameras. +This cookbook runs it as a Podman Quadlet. + +This cookbook: + +- Runs the upstream image `ghcr.io/blakeblackshear/frigate`, pinned to a full version (`0.18.2`). + The manifest is multi-architecture (amd64 + arm64). +- Runs Frigate as root inside its container, as the image requires (s6-overlay). +- Stores the configuration, the database and the recordings as **precious data** on the virtiofs + mount `/var/lib/virtiofs/data/frigate`, so that they inherit the hypervisor's ZFS snapshots, + quota and backups. +- Seeds the configuration file on first boot, then never touches it again: Frigate owns it, and its + web interface writes the cameras, the masks and the zones back to it. +- Binds the unauthenticated listeners of Frigate and go2rtc to the loopback. +- Detects objects with OpenVINO on the CPU, the default of a new Frigate 0.18 configuration. +- Uses host networking. + +It does not provide MQTT, a reverse proxy, nor any camera definition. + +## Prerequisites + +- The `base` cookbook (implicit dependency), for the virtiofs mount. +- A virtiofs mount on `/var/lib/virtiofs/data`, sized for the recordings. +- Network access at the first boot, to pull the image (about 2 GB on arm64). +- CPU and memory in proportion to the cameras. Frigate's own figures are for hardware detectors; + on a CPU, measure the inference time it reports under *System metrics*. + +## Files + +| Path | Content | +|------|---------| +| `/etc/quadlets/frigate/config.yml` | Seed of the configuration file. Shipped by the cookbook; an operator may replace it before the first boot. | +| `/etc/quadlets/frigate/init.sh` | Copies the seed when Frigate has no configuration file yet. | +| `/var/lib/virtiofs/data/frigate/config/` | Mounted on `/config`: `config.yml`, the SQLite database, the model cache, the backup of the configuration made before each migration. | +| `/var/lib/virtiofs/data/frigate/media/` | Mounted on `/media/frigate`: recordings, clips, snapshots, exports. | + +`frigate.service` only starts once `/var/lib/virtiofs/data/frigate/config/config.yml` exists. On a +fresh system, `frigate-init.service` creates it from the seed. Once it exists, the seed is never +read again: changing `/etc/quadlets/frigate/config.yml` has no effect on a running installation. + +## The seed + +The seed is the default configuration of Frigate 0.18, plus three restrictions: + +- `networking.listen.internal: 127.0.0.1:5000`. Port 5000 serves the UI and the API without + authentication. The health check and nginx reach it over the loopback; nothing else needs it. +- `go2rtc.api.listen: 127.0.0.1:1984`. The go2rtc API is not authenticated, and it accepts new + stream sources, `exec:` included. It listens on every address by default. +- `go2rtc.rtsp.listen: 127.0.0.1:8554`. The RTSP restream is not authenticated either. + +MQTT is disabled. `cameras` is empty: cameras are added from the web interface, or by editing +`config.yml` on the data mount. + +## Ports + +| Port | Bound to | Use | +|------|----------|-----| +| `8971/tcp` | all | Web interface and API, authenticated, over TLS with a self-signed certificate | +| `8555/tcp+udp` | all | WebRTC, for live views in a browser | +| `5000/tcp` | loopback | Web interface and API, **unauthenticated** | +| `1984/tcp` | loopback | go2rtc API, **unauthenticated** | +| `8554/tcp` | loopback | RTSP restream, unauthenticated | + +## First login + +Frigate creates an `admin` user at its first start and prints its password in the journal: + +```sh +journalctl -u frigate.service | grep -A1 -i password +``` + +Change it under *Settings > Users*. To print a new one, set `auth.reset_admin_password: true` in +`config.yml` and restart Frigate. + +## Upgrades + +The tag is pinned in `frigate.image` and `frigate.container`, and both must change together. A new +minor version migrates `config.yml` in place at its first start, after copying it to +`backup_config.yaml`. There is no migration back: going back to an older version means restoring +the data mount from a snapshot taken before the upgrade. + +## Usage + +```sh +sudo systemctl start frigate.target +sudo systemctl status frigate.service +sudo podman logs -f frigate +curl -k https://localhost:8971/ +``` + +## Shared memory + +A camera detecting at 720p takes about 35 MB of shared memory: 1280 × 720 × 1.5 bytes for each of +the 25 frames Frigate keeps. Logs and nginx cache take up to 50 MB more. `ShmSize=512m` thus covers +eight such cameras and two added while Frigate runs. A shared memory too small shows as `Bus error` +in the journal. The calculator in the upstream installation guide gives the size for other setups. diff --git a/cookbooks/frigate/config/config.yml b/cookbooks/frigate/config/config.yml new file mode 100644 index 0000000..510d1af --- /dev/null +++ b/cookbooks/frigate/config/config.yml @@ -0,0 +1,42 @@ +# Seed of /var/lib/virtiofs/data/frigate/config/config.yml, copied once by +# frigate-init.service. Frigate owns the file afterwards: cameras, masks and +# zones are added from its web interface. An operator who needs other defaults +# provides /etc/quadlets/frigate/config.yml before the first boot. + +mqtt: + enabled: false + +# Port 5000 serves the UI and the API WITHOUT authentication. It is meant for +# the container itself (health check, nginx) and goes on the loopback. The +# authenticated UI stays on 8971, over TLS. +networking: + listen: + internal: 127.0.0.1:5000 + +# go2rtc listens on every address by default. Its API on 1984 is not +# authenticated and accepts new stream sources, "exec:" included; the RTSP +# restream on 8554 is not authenticated either. Frigate reaches both over the +# loopback. WebRTC (8555) stays open: the browsers connect to it directly. +go2rtc: + api: + listen: 127.0.0.1:1984 + rtsp: + listen: 127.0.0.1:8554 + +# The default of a new Frigate 0.18 configuration: OpenVINO on the CPU, with the +# SSDLite MobileNet v2 model shipped in the image (amd64 and arm64). +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 + +cameras: {} + +version: 0.18-0 diff --git a/cookbooks/frigate/config/init.sh b/cookbooks/frigate/config/init.sh new file mode 100755 index 0000000..1da92fc --- /dev/null +++ b/cookbooks/frigate/config/init.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# +# Seed the Frigate configuration file on first boot. +# +# Frigate owns config.yml: the web interface writes the cameras, the masks and +# the zones back to it, and a new minor version migrates it in place. So this +# script only ever creates it when it is absent, and never touches it again. +# +set -eu + +SRC=/etc/quadlets/frigate/config.yml +DST=/var/lib/virtiofs/data/frigate/config/config.yml + +if [ -e "$DST" ]; then + echo "Keeping existing $DST (Frigate owns it)" + exit 0 +fi + +echo "Seeding $DST from $SRC" +install -m 0600 -o root -g root "$SRC" "$DST" diff --git a/cookbooks/frigate/frigate-init.service b/cookbooks/frigate/frigate-init.service new file mode 100644 index 0000000..739d352 --- /dev/null +++ b/cookbooks/frigate/frigate-init.service @@ -0,0 +1,24 @@ +[Unit] +Description=Frigate - Seed the configuration file on first boot +Documentation=https://docs.frigate.video/configuration/ +After=var-lib-virtiofs-data.mount systemd-tmpfiles-setup.service +Requires=var-lib-virtiofs-data.mount + +# Only run if there is a seed to copy +ConditionPathExists=/etc/quadlets/frigate/config.yml + +# Start/stop this unit when the target is started/stopped, before the service +PartOf=frigate.target +Before=frigate.service + +[Service] +Type=oneshot +RemainAfterExit=yes + +# Copy the seed on first boot only (see init.sh). Frigate owns config.yml +# afterwards: its web interface writes every change back to it, and it migrates +# the file in place on an upgrade. +ExecStart=/etc/quadlets/frigate/init.sh + +[Install] +WantedBy=frigate.target diff --git a/cookbooks/frigate/frigate.container b/cookbooks/frigate/frigate.container new file mode 100644 index 0000000..41e4b6f --- /dev/null +++ b/cookbooks/frigate/frigate.container @@ -0,0 +1,79 @@ +[Unit] +Description=Frigate - Network video recorder with object detection +Documentation=https://docs.frigate.video/ +After=network.target var-lib-virtiofs-data.mount frigate-init.service +Requires=var-lib-virtiofs-data.mount frigate-init.service + +# The pull runs in its own unit, and it is Wants=, NOT Requires=. A pull needs +# the registry; starting this container does not, as long as the image is in +# local storage. With Requires=, a boot without network fails the pull, the +# dependency fails, and a failed dependency is never retried: Restart= only +# applies to a unit that started at least once. +# +# After= still orders the pull before this container, so nothing is lost on a +# boot where the network is there. +Wants=frigate-image.service +After=frigate-image.service + +# Frigate starts on the configuration file seeded by frigate-init.service, or on +# the one it already owns. Without it, Frigate would write a default one of its +# own, with none of the settings the README lists. +ConditionPathExists=/var/lib/virtiofs/data/frigate/config/config.yml + +# Start/stop this unit when the target is started/stopped +PartOf=frigate.target + +[Container] +ContainerName=frigate +# The image REFERENCE, and not the name of the frigate.image unit: naming the +# unit makes quadlet generate a Requires= on it, which is the fault described in +# [Unit] above. Podman resolves the reference at start with --pull=missing. +# +# THIS REFERENCE IS ALSO IN frigate.image, and the two must be changed together. +Image=ghcr.io/blakeblackshear/frigate:0.18.2 + +# Root inside the container: the image starts s6-overlay, which runs nginx, +# go2rtc and Frigate as root. Frigate documents no other way to run. + +# Host networking: the cameras push nothing, Frigate pulls their RTSP streams, +# and WebRTC needs the real addresses of the browsers. The listening sockets are +# narrowed by the configuration file instead -- see the README. +Network=host + +# Decoded frames are passed between processes in /dev/shm: about 35 MB per +# camera detecting at 720p, plus up to 50 MB of logs and nginx cache. 512 MB +# covers eight cameras and two more. A shm too small shows as "Bus error" in the +# journal. +ShmSize=512m + +# Recording segments are written here first, then moved to /media/frigate. +Tmpfs=/tmp/cache:rw,size=1g + +# Precious data: configuration, database and model cache on one side, +# recordings on the other. Both on the virtiofs mount, where the host's ZFS +# snapshots and quota apply. +Volume=/var/lib/virtiofs/data/frigate/config:/config:z +Volume=/var/lib/virtiofs/data/frigate/media:/media/frigate:z + +# The recordings are named and the review timeline is drawn in local time. +Volume=/etc/localtime:/etc/localtime:ro + +# Health check: the request of the image's own HEALTHCHECK, on the internal +# port, which the configuration file binds to the loopback. The image also skips +# the check during an orderly shutdown; that part needs a shell, and the state +# of a container being stopped does not matter. +HealthCmd=curl --fail --silent --show-error http://127.0.0.1:5000/api/version +HealthInterval=30s +HealthTimeout=10s +# The first start converts the detection model, which takes minutes on a CPU. +HealthStartPeriod=300s +HealthRetries=5 + +[Service] +Restart=always +RestartSec=10 +TimeoutStartSec=900 +TimeoutStopSec=120 + +[Install] +WantedBy=frigate.target diff --git a/cookbooks/frigate/frigate.image b/cookbooks/frigate/frigate.image new file mode 100644 index 0000000..a9d0ab3 --- /dev/null +++ b/cookbooks/frigate/frigate.image @@ -0,0 +1,27 @@ +[Unit] +Description=podman pull ghcr.io/blakeblackshear/frigate +Documentation=https://docs.frigate.video/ + +# Keep trying. A machine whose network arrives after it does fails this pull +# once, early in the boot, and a oneshot that failed once never runs again. +# StartLimitIntervalSec=0 makes the retry unlimited: the default start limit +# (5 attempts in 10 s) would turn the unit "failed" for good within a minute. +StartLimitIntervalSec=0 + +# Stopping the service target stops the pull with it, so `systemctl stop +# frigate.target` leaves nothing retrying behind. +PartOf=frigate.target + +[Image] +# Multi-architecture manifest (amd64 + arm64, verified 2026-11-01 on 0.18.2). +# The tag is a full version: Frigate migrates its configuration file in place +# when a new minor version starts on it, and keeps no way back. An upgrade is +# therefore something an operator decides, not something auto-update does. +# +# THIS REFERENCE IS ALSO IN frigate.container, on purpose, and the two must be +# changed together. The reason is in the container file. +Image=ghcr.io/blakeblackshear/frigate:0.18.2 + +[Service] +Restart=on-failure +RestartSec=60 diff --git a/cookbooks/frigate/frigate.target b/cookbooks/frigate/frigate.target new file mode 100644 index 0000000..1c8f6cf --- /dev/null +++ b/cookbooks/frigate/frigate.target @@ -0,0 +1,11 @@ +[Unit] +Description=Frigate Service Target +Documentation=man:systemd.target(5) +Requires=frigate-init.service frigate.service +After=frigate-init.service frigate.service + +# Allow isolation - can stop/start this target independently +AllowIsolate=yes + +[Install] +WantedBy=multi-user.target diff --git a/cookbooks/frigate/tmpfiles.d/frigate.conf b/cookbooks/frigate/tmpfiles.d/frigate.conf new file mode 100644 index 0000000..27c9a2b --- /dev/null +++ b/cookbooks/frigate/tmpfiles.d/frigate.conf @@ -0,0 +1,9 @@ +# Precious data, on the virtiofs mount so that the host's ZFS snapshots and +# backups pick it up: +# - config: config.yml, the SQLite database (events, users), the model cache; +# - media: the recordings, the snapshots, the clips and the exports. +# The "$" marks them for removal on `systemd-tmpfiles --purge` (i.e. `make +# uninstall`). +d$ /var/lib/virtiofs/data/frigate 0750 root root - +d$ /var/lib/virtiofs/data/frigate/config 0750 root root - +d$ /var/lib/virtiofs/data/frigate/media 0750 root root -