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 -