Compare commits

...

4 Commits

Author SHA1 Message Date
Nicolas Massé da015ea091 frigate: do not mount /etc/localtime 4 days ago
Nicolas Massé 825d861f82 frigate: network video recorder with object detection 4 days ago
Nicolas Massé ee66d557de asterisk: serve a static HTTP directory with busybox httpd 4 days ago
Nicolas Massé 6096e5d0ab asterisk: load app_directed_pickup, name the group pickup code 4 days ago
  1. 11
      cookbooks/asterisk/README.md
  2. 48
      cookbooks/asterisk/asterisk-http.container
  3. 4
      cookbooks/asterisk/asterisk.target
  4. 9
      cookbooks/asterisk/config/container/Containerfile
  5. 4
      cookbooks/asterisk/config/etc/features.conf
  6. 3
      cookbooks/asterisk/config/etc/modules.conf
  7. 4
      cookbooks/asterisk/tmpfiles.d/asterisk.conf
  8. 10
      cookbooks/frigate/Makefile
  9. 100
      cookbooks/frigate/README.md
  10. 42
      cookbooks/frigate/config/config.yml
  11. 20
      cookbooks/frigate/config/init.sh
  12. 24
      cookbooks/frigate/frigate-init.service
  13. 80
      cookbooks/frigate/frigate.container
  14. 27
      cookbooks/frigate/frigate.image
  15. 11
      cookbooks/frigate/frigate.target
  16. 9
      cookbooks/frigate/tmpfiles.d/frigate.conf

11
cookbooks/asterisk/README.md

@ -4,7 +4,7 @@
[Asterisk](https://www.asterisk.org/) is an open source PBX. This cookbook runs it as a SIP server for
phones on the local network, together with the TFTP server the phones fetch their configuration and
firmware from.
firmware from, and an HTTP server for their directory.
This cookbook:
@ -13,6 +13,7 @@ This cookbook:
- Runs Asterisk directly as a dedicated, unprivileged UID/GID (no user namespace mapping), with
PJSIP as its only channel driver.
- Runs dnsmasq as a TFTP server only (no DNS, no DHCP), from the same image, as the same user.
- Runs the httpd of busybox as a static HTTP server, from the same image, as the same user.
- Loads an explicit list of Asterisk modules: what calls between SIP phones need, and nothing else.
- Includes a timer to rebuild the container image daily.
@ -34,11 +35,12 @@ interface.
| `/etc/quadlets/asterisk/etc/extensions.conf` | Dialplan. Required. |
| `/etc/quadlets/asterisk/etc/indications.conf` | Call progress tones of the country. Optional: without it, Asterisk logs a warning and uses no tones of its own; the phones play theirs. |
| `/var/lib/virtiofs/data/asterisk/tftp/` | Configuration files and firmware images of the phones, as each vendor names them. Optional: the TFTP server also runs on an empty directory, so that files written later are served without a restart. |
| `/var/lib/virtiofs/data/asterisk/http/` | Static files served over HTTP, such as the XML directory of the phones. Optional, like the TFTP root. Keep the configuration files of the phones out of it: they hold SIP passwords, and HTTP is easier to reach from another network than TFTP. |
Asterisk does not start without `pjsip.conf` and `extensions.conf`. Examples of both are in
`config/examples/etc/`.
The TFTP root is on the virtiofs mount rather than under `/etc/quadlets`, so that the host can write
The TFTP and HTTP roots are on the virtiofs mount rather than under `/etc/quadlets`, so that the host can write
it: a firmware image weighs tens of megabytes, which an Ignition file cannot carry. The files must be
readable by UID 10036.
@ -53,14 +55,15 @@ missing.
| 5060 | UDP | SIP |
| 10000-10999 | UDP | RTP (set in `rtp.conf`) |
| 69 | UDP | TFTP |
| 80 | TCP | HTTP |
Both containers use host networking.
All three containers use host networking.
## UID and GID
| UID | GID | Usage |
|-----|-----|-------|
| 10036 | 10000 | Asterisk and dnsmasq |
| 10036 | 10000 | Asterisk, dnsmasq and httpd |
## Usage

48
cookbooks/asterisk/asterisk-http.container

@ -0,0 +1,48 @@
[Unit]
Description=Asterisk - HTTP server for the directory of the phones
Documentation=https://busybox.net/downloads/BusyBox.html
Wants=asterisk-build.service
After=network.target asterisk-build.service var-lib-virtiofs-data.mount
Requires=var-lib-virtiofs-data.mount
# Static files only, operator-provided on the precious-data mount, like the
# TFTP root and for the same reasons. A separate directory from the TFTP root:
# the configuration files of the phones hold their SIP passwords, and HTTP,
# unlike TFTP, is easily reached from another network.
# Start/stop this unit when the target is started/stopped
PartOf=asterisk.target
[Container]
ContainerName=asterisk-http
Image=localhost/asterisk:latest
AutoUpdate=local
User=10036
Group=10000
# HTTP listens on 80/tcp, a privileged port.
AddCapability=CAP_NET_BIND_SERVICE
# Host networking, as the two other containers: the phones reach the same
# address for SIP, TFTP and HTTP.
Network=host
# busybox httpd: -f stays in the foreground, -v logs each request. It reads
# /etc/httpd.conf of the image for the MIME type of XML files, and lists no
# directory.
Entrypoint=/usr/sbin/httpd
Exec=-f -v -p 80 -h /var/lib/virtiofs/data/asterisk/http
# Read-only: the phones fetch, they never upload.
Volume=/var/lib/virtiofs/data/asterisk/http:/var/lib/virtiofs/data/asterisk/http:ro,z
[Service]
Restart=always
RestartSec=10
TimeoutStartSec=300
TimeoutStopSec=30
[Install]
WantedBy=asterisk.target

4
cookbooks/asterisk/asterisk.target

@ -2,8 +2,8 @@
Description=Asterisk Service Target
Documentation=man:systemd.target(5)
Requires=asterisk.service asterisk-build.timer
Wants=asterisk-tftp.service
After=asterisk.service asterisk-tftp.service asterisk-build.timer
Wants=asterisk-tftp.service asterisk-http.service
After=asterisk.service asterisk-tftp.service asterisk-http.service asterisk-build.timer
# Allow isolation - can stop/start this target independently
AllowIsolate=yes

9
cookbooks/asterisk/config/container/Containerfile

@ -2,8 +2,13 @@ FROM docker.io/library/alpine:3.24
# Asterisk 22 (the LTS branch), PJSIP included: Alpine bundles pjproject in the
# main package. dnsmasq serves the provisioning files of the phones over TFTP,
# from the same image, so that a single build keeps both up to date.
RUN apk add --no-cache asterisk dnsmasq
# and the httpd of busybox-extras their directory over HTTP, from the same
# image, so that a single build keeps all three up to date.
RUN apk add --no-cache asterisk dnsmasq busybox-extras
# busybox httpd sends no Content-Type for .xml files without this line, and
# the XML services of the phones expect text/xml.
RUN printf '.xml:text/xml; charset=utf-8\n' > /etc/httpd.conf
# /etc/asterisk is empty: the sample files are in asterisk-sample-config, which
# is not installed. The configuration is mounted read-only there at run time.

4
cookbooks/asterisk/config/etc/features.conf

@ -1,3 +1,7 @@
; Default transfer and pickup settings. The phones transfer with SIP REFER,
; handled by res_pjsip_refer, not with DTMF feature codes.
[general]
; Dialled by a phone, picks up a call ringing on a phone of its pickup groups
; (named_pickup_group and named_call_group of pjsip.conf). Handled by chan_pjsip
; before the dialplan. *8 is the default, written here to be found.
pickupexten = *8

3
cookbooks/asterisk/config/etc/modules.conf

@ -45,6 +45,9 @@ load => codec_a_mu.so
; Dialplan
load => pbx_config.so
load => app_dial.so
; Directed pickup: Pickup() and PickupChan(). Group pickup (pickupexten of
; features.conf, *8 by default) is in the core and chan_pjsip.
load => app_directed_pickup.so
load => func_callerid.so
; "channel originate" from the CLI, to test a phone without a second one

4
cookbooks/asterisk/tmpfiles.d/asterisk.conf

@ -1,4 +1,5 @@
# Provisioning files of the phones, served over TFTP. Operator-provided, on the
# Provisioning files of the phones, served over TFTP, and their directory,
# served over HTTP. Operator-provided, on the
# precious-data mount so that the host can write them. The "$" marks it for
# removal on `systemd-tmpfiles --purge` (i.e. `make uninstall`).
#
@ -13,3 +14,4 @@
# not affected: its label is fixed by the mount.
d$ /var/lib/virtiofs/data/asterisk 0750 10036 10000 -
d$ /var/lib/virtiofs/data/asterisk/tftp 0750 10036 10000 -
d$ /var/lib/virtiofs/data/asterisk/http 0750 10036 10000 -

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

80
cookbooks/frigate/frigate.container

@ -0,0 +1,80 @@
[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
# No /etc/localtime: Fedora CoreOS has none (it runs in UTC), and podman fails
# the start on a missing mount source. Frigate files its recordings by UTC date
# and its web interface shows the time zone of the browser.
# 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