Browse Source

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PrUtBTGszYnsSBcxNKUTjF
main
Nicolas Massé 4 days ago
parent
commit
825d861f82
  1. 10
      cookbooks/frigate/Makefile
  2. 100
      cookbooks/frigate/README.md
  3. 42
      cookbooks/frigate/config/config.yml
  4. 20
      cookbooks/frigate/config/init.sh
  5. 24
      cookbooks/frigate/frigate-init.service
  6. 79
      cookbooks/frigate/frigate.container
  7. 27
      cookbooks/frigate/frigate.image
  8. 11
      cookbooks/frigate/frigate.target
  9. 9
      cookbooks/frigate/tmpfiles.d/frigate.conf

10
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

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

42
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

20
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"

24
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

79
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

27
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

11
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

9
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 -
Loading…
Cancel
Save