Frigate turns ordinary RTSP cameras into a recorder that knows a person from a parked car, and it does that on your own hardware. Installing it is easy, since the NVR is one container plus a broker. Feeding it is harder. On a generic 4 vCPU VM without AVX, the container used about three cores for one camera on the default CPU detector, so the box you pick matters more than the install.
This guide shows you how to install Frigate NVR with Docker Compose, add a camera over RTSP, record on motion and hand events to Home Assistant through Mosquitto. Our lab had no physical camera, so a looping parking-lot clip played one. Everything ran in October 2026 on Ubuntu 26.04.1 with Docker 29.9.1, Frigate 0.18.1 (upgraded to 0.18.2), Mosquitto 2.1.2 and Home Assistant 2026.10.1, with the Docker steps repeated in Ubuntu 24.04 and Debian 13 and 12 containers.
Plan the Frigate stack
You end up with two containers on one host. Frigate pulls each camera over RTSP into its bundled go2rtc restreamer, sends the low resolution sub stream to the object detector and writes the high resolution main stream to disk. Mosquitto carries the events out to Home Assistant or anything else that speaks MQTT.
Only three ports leave the host: 8971 for the logged-in web UI, 8554 for the restream and 1883 for MQTT. The restream on 8554 needs no login by default, so anyone on your network could pull the camera through it, and this setup puts a username and password on it from the first start. Port 5000 serves the same UI and API with no login at all, so it stays inside Docker.
Everything up to “Check detections and recordings” is the core path that every reader needs. The sections after it are optional and independent of each other, so pick what you need:
| Section | Needed for | Extra things you need |
|---|---|---|
| Add a zone and watch MQTT events | Alerts for one area of the frame | Nothing |
| Choose a detector and hardware acceleration | More than one or two cameras | Intel or AMD iGPU, NVIDIA GPU, Coral or Hailo |
| Turn on face recognition and license plates | Names and plates on events | CPU with AVX and AVX2, internet once for models |
| Connect Frigate to Home Assistant | Automations and phone alerts | A running Home Assistant |
| Serve the UI with a trusted certificate | No browser warning | A domain name, port 80 or a DNS API token |
Size the host
Frigate runs on any 64-bit Ubuntu or Debian host that runs Docker, but three things decide whether it keeps up: how fast the detector answers, whether video decoding lands on a GPU, and how many megabits your cameras record. Before you start, have these ready:
- Ubuntu 26.04 or 24.04, or Debian 13 or 12, with sudo
- IP cameras with RTSP and a separate sub stream
- About 10 GB free for the image and a separate disk for recordings
- A CPU with AVX and AVX2 if you want face or plate recognition
- Optional: an Intel iGPU, Coral or Hailo-8 for detection
The detector is the number to watch. Our lab VM had 4 vCPUs of a generic virtual CPU with no AVX. There, Frigate’s default CPU detector took 38 to 66 ms per inference and used 210 to 275% CPU on its own, yet it still skipped about one frame in five at 5 fps. That is fine for following along, but it is a floor, not a production recommendation, and we’d expect a second camera on that box to fall further behind. Any real install wants an accelerator, and the hardware in our Frigate NVR appliance build handles detection off the CPU entirely.
Storage is plain arithmetic, because Frigate saves the camera’s own stream without re-encoding. Our 720p test stream wrote 660 MiB of recordings in 38.6 minutes, about 1.1 GB an hour, which works out to roughly 2.4 Mbps. That makes the camera’s bitrate setting your real storage knob: lower it in the camera’s own web page and the recordings shrink with it, while Frigate’s retention days only decide how long the pile stays on disk. The first row comes from that disk measurement; the 4 and 8 Mbps rows are plain bitrate arithmetic for a camera that sees motion all day, which is the worst case.
| Main stream bitrate | Per hour | Per day | 14 days |
|---|---|---|---|
| About 2.4 Mbps (our test camera, from disk use) | 1.1 GB | 26 GB | 360 GB |
| 4 Mbps (typical 1080p) | 1.8 GB | 43 GB | 605 GB |
| 8 Mbps (typical 4K) | 3.6 GB | 86 GB | 1.2 TB |
Motion-only retention usually lands well under those figures on a quiet driveway, and well over a few gigabytes a day on a street. Pick cameras with a proper sub stream and H.264 output, since both save CPU; our PoE camera picks for Frigate list the models that do this cleanly. If the host is a Proxmox VM, check its CPU type before you install. Our VM ran kvm64, the backend default, which hides SSE4.2 and AVX and broke the OpenVINO detector. The web UI’s default for new VMs, x86-64-v2-AES, adds SSE4.2 but still has no AVX or AVX2, so pick host or x86-64-v3 if you want face or plate recognition.
Install Docker Engine on Ubuntu or Debian
Install Docker from Docker’s own apt repository rather than the distro’s docker.io package, because the Compose plugin and current releases come from there. The commands below read your distribution and codename from /etc/os-release, so the same lines work on Ubuntu and Debian. Start with the signing key.
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
. /etc/os-release
sudo curl -fsSL "https://download.docker.com/linux/${ID}/gpg" -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
Next, write the repository definition. It’s one printf instead of a pasted file, so ubuntu or debian and the codename fill themselves in.
printf 'Types: deb\nURIs: https://download.docker.com/linux/%s\nSuites: %s\nComponents: stable\nArchitectures: %s\nSigned-By: /etc/apt/keyrings/docker.asc\n' "$ID" "$VERSION_CODENAME" "$(dpkg --print-architecture)" | sudo tee /etc/apt/sources.list.d/docker.sources
The suite line carries your release codename, and Docker already publishes packages for the newest Ubuntu LTS. Our Ubuntu host wrote resolute, and the same command writes trixie on Debian:
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: resolute
Components: stable
Architectures: amd64
Signed-By: /etc/apt/keyrings/docker.asc
Refresh the package lists and install the engine with the Compose and Buildx plugins. On our Ubuntu box the install took 16 seconds.
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Confirm both the engine and the Compose plugin answer, then run the test container.
docker --version
docker compose version
sudo docker run --rm hello-world
The test container should print “Hello from Docker!” followed by an explanation of what just happened. The two version lines come first, and our Ubuntu and Debian container checks all resolved to the same engine and Compose releases:
Docker version 29.9.1, build dfc520d
Docker Compose version v5.6.0
If you want Docker tuned beyond this, our Docker CE install guide covers rootless mode, the log driver and the daemon settings. Frigate itself needs nothing more than what you just installed.
Set the variables for your camera and broker
Eight values repeat through the rest of the guide: where the project lives, your camera’s address and login, the MQTT account Frigate will use, and the login you’ll put on the restream. Export them once and every later command runs as written.
export APP_ROOT="/opt/frigate"
export CAMERA_IP="10.0.1.60"
export RTSP_USER="admin"
export RTSP_PASS="CameraPass123"
export MQTT_USER="frigate"
export MQTT_PASS="MqttPass2026x"
export RESTREAM_USER="viewer"
export RESTREAM_PASS='Restream$2026x'
Use the camera account you created in the camera’s own web page or app, and pick fresh passwords for MQTT and the restream; nothing else needs to know them yet. Keep single quotes around any value with a $ in it, or the shell expands it. If the camera password contains @, :, / or %, URL-encode those characters in RTSP_PASS (an @ becomes %40), because the password ends up inside an RTSP URL. Keep RESTREAM_PASS raw, since go2rtc checks it exactly as typed. Encode it only where it goes into a URL, such as the ffprobe test and the Home Assistant template, or pick a restream password without those four characters. These exports only last for the current shell, so run the block again if you reconnect. Check that none of them is empty.
echo "root=${APP_ROOT} camera=${CAMERA_IP} rtsp_user=${RTSP_USER} mqtt_user=${MQTT_USER} restream_user=${RESTREAM_USER}"
Every field after an equals sign should hold a value. A blank one now turns into a silent 401 from the camera later.
Find and test your camera’s RTSP URLs
Frigate needs two RTSP paths from each camera: the main stream to record and the sub stream to run detection on. Each vendor uses its own path format, and these are the ones you’ll meet most often:
| Brand | Main stream path | Sub stream path |
|---|---|---|
| Hikvision and most OEM clones | /Streaming/Channels/101 | /Streaming/Channels/102 |
| Dahua, Amcrest | /cam/realmonitor?channel=1&subtype=0 | /cam/realmonitor?channel=1&subtype=1 |
| Reolink | /Preview_01_main | /Preview_01_sub |
| TP-Link Tapo | /stream1 | /stream2 |
| Axis | /axis-media/media.amp | set by resolution parameter |
All of them listen on port 554 unless you changed it. Tapo cameras need a separate camera account created in the Tapo app first. UniFi Protect is the exception, because it serves RTSPS on port 7441. Frigate’s docs say to change its rtsps:// link to rtspx:// and drop ?enableSrtp in go2rtc. We had no UniFi camera to test that, so follow the camera-specific notes. Our test camera was a looping CC BY parking-lot clip served by MediaMTX on the Frigate host with Hikvision-style paths, so the commands below match a Hikvision camera exactly.
Pull the Frigate image now. It is large, and it ships its own ffprobe, so you can test the camera without installing FFmpeg and about 150 packages on the host. The pull downloaded 2.16 GB and took 4 minutes 44 seconds here.
sudo docker pull ghcr.io/blakeblackshear/frigate:stable
Probe the main stream with the bundled ffprobe. The -rtsp_transport tcp flag matches what Frigate’s presets use.
sudo docker run --rm --entrypoint /usr/lib/ffmpeg/8.0/bin/ffprobe ghcr.io/blakeblackshear/frigate:stable -hide_banner -rtsp_transport tcp "rtsp://${RTSP_USER}:${RTSP_PASS}@${CAMERA_IP}:554/Streaming/Channels/101"
A working URL prints the codec, resolution and frame rate. The “co located POCs unavailable” line is harmless; it only means the probe joined the stream between keyframes:
[h264 @ 0x5afe18e16f00] co located POCs unavailable
Input #0, rtsp, from 'rtsp://admin:[email protected]:554/Streaming/Channels/101':
Metadata:
title : No Name
Duration: N/A, start: -0.144800, bitrate: N/A
Stream #0:0: Video: h264 (Main), yuv420p(tv, smpte170m, progressive), 1280x720, 12 fps, 12 tbr, 90k tbn, start -0.144800
Run it again with 102 at the end, and the sub stream should report a smaller resolution (ours was 640×360). Write that down, because the detect settings in the config must match it. There’s no point running the sub stream much larger, because Frigate crops each area of motion and resizes it to the model’s 320×320 input, so extra resolution mostly costs CPU unless your objects are tiny in the frame. A wrong password fails loudly instead:
[rtsp @ 0x5da1c2c19140] method DESCRIBE failed: 401 (Unauthorized)
rtsp://admin:[email protected]:554/Streaming/Channels/101: Server returned 401 Unauthorized (authorization failed)
A wrong path fails too, but the status code depends on the camera’s firmware; our MediaMTX test server answered 400 Bad Request. Fix the URL here, because Frigate will only show you a grey tile and a reconnect loop later.
Create the Mosquitto broker
Frigate can run without MQTT, but Home Assistant and most automations listen there, so we run Mosquitto in the same stack and give it a password from day one. Create the project tree first.
sudo mkdir -p "${APP_ROOT}"/{config,storage,mosquitto/{config,data,log}}
With no listener defined, Mosquitto only accepts connections from its own machine, which inside a container means nobody. The broker needs a short configuration file, so open it:
sudo vim "${APP_ROOT}/mosquitto/config/mosquitto.conf"
Paste this. It keeps retained messages across restarts, logs to Docker, and refuses any client without a username:
persistence true
persistence_location /mosquitto/data/
log_dest stdout
listener 1883
allow_anonymous false
password_file /mosquitto/config/passwd
Generate the password file with the broker’s own mosquitto_passwd tool, then hand the file to UID 1883, the mosquitto user inside the image. Skip the chown and the broker restarts in a loop, because it cannot read a root-owned file with mode 600:
sudo docker run --rm -v "${APP_ROOT}/mosquitto/config:/mosquitto/config" eclipse-mosquitto:2 mosquitto_passwd -c -b /mosquitto/config/passwd "${MQTT_USER}" "${MQTT_PASS}"
sudo chown 1883:1883 "${APP_ROOT}/mosquitto/config/passwd"
sudo ls -l "${APP_ROOT}/mosquitto/config"
The listing should show passwd owned by 1883 and readable only by it. If it still says root, the broker will refuse to start:
total 8
-rw-r--r-- 1 root root 147 Oct 15 20:31 mosquitto.conf
-rw------- 1 1883 1883 194 Oct 15 20:31 passwd
Do not start the broker yet; it comes up with Frigate in one Compose file a few steps further on. The password_file option still works on the eclipse-mosquitto:2 tag, but the Mosquitto 2.1 changelog deprecates it in favour of the password-file plugin and removes it in 3.0, so revisit this file before you move to a 3.x image.
Write the Frigate configuration
Keep the secrets out of config.yml. Frigate substitutes any environment variable that starts with FRIGATE_ into camera paths, MQTT settings and the restream login, so the config can stay generic while a root-only .env file holds the real values. Create the file empty with mode 600 first, so no password is ever readable by other users, then write it:
sudo install -m 600 /dev/null "${APP_ROOT}/.env"
printf "FRIGATE_MQTT_USER='%s'\nFRIGATE_MQTT_PASSWORD='%s'\nFRIGATE_RTSP_USER='%s'\nFRIGATE_RTSP_PASSWORD='%s'\nFRIGATE_CAMERA_IP='%s'\nFRIGATE_GO2RTC_RTSP_USERNAME='%s'\nFRIGATE_GO2RTC_RTSP_PASSWORD='%s'\n" "${MQTT_USER}" "${MQTT_PASS}" "${RTSP_USER}" "${RTSP_PASS}" "${CAMERA_IP}" "${RESTREAM_USER}" "${RESTREAM_PASS}" | sudo tee "${APP_ROOT}/.env" >/dev/null
sudo ls -l "${APP_ROOT}/.env"
The listing should show -rw------- and root as the owner. The single quotes the printf writes around each value matter, because Compose expands a $ in an unquoted .env value; our restream password has one, and all 14 characters arrived intact inside the container.
Frigate writes a starter config if none exists, but you want your own camera in it from the first start. Open the main configuration file:
sudo vim "${APP_ROOT}/config/config.yml"
This is the exact file we ran. Rename front_cam if you like, but keep the go2rtc stream names and the input paths in step with it.
mqtt:
enabled: true
host: mosquitto
port: 1883
user: "{FRIGATE_MQTT_USER}"
password: "{FRIGATE_MQTT_PASSWORD}"
go2rtc:
rtsp:
username: "{FRIGATE_GO2RTC_RTSP_USERNAME}"
password: "{FRIGATE_GO2RTC_RTSP_PASSWORD}"
streams:
front_cam:
- rtsp://{FRIGATE_RTSP_USER}:{FRIGATE_RTSP_PASSWORD}@{FRIGATE_CAMERA_IP}:554/Streaming/Channels/101
front_cam_sub:
- rtsp://{FRIGATE_RTSP_USER}:{FRIGATE_RTSP_PASSWORD}@{FRIGATE_CAMERA_IP}:554/Streaming/Channels/102
cameras:
front_cam:
ffmpeg:
inputs:
- path: rtsp://127.0.0.1:8554/front_cam
input_args: preset-rtsp-restream
roles:
- record
- path: rtsp://127.0.0.1:8554/front_cam_sub
input_args: preset-rtsp-restream
roles:
- detect
detect:
enabled: true
width: 640
height: 360
fps: 5
live:
streams:
Main: front_cam
Sub: front_cam_sub
objects:
track:
- person
- car
- bicycle
record:
enabled: true
motion:
days: 3
alerts:
retain:
days: 14
mode: motion
detections:
retain:
days: 14
mode: motion
snapshots:
enabled: true
retain:
default: 14
version: 0.18-0
A few choices in that file are deliberate, and each one saves you a problem later:
- go2rtc holds the only connection to the camera
- The restream on 8554 asks for a username and password
- Detection reads the 640×360 sub stream at 5 fps
- Recording keeps motion for 3 days, alerts and detections for 14
version: 0.18-0marks the config schema
Cheap cameras often allow one or two RTSP sessions, which is why everything else reads from the local restream on 127.0.0.1. Those local reads skip the restream login, as the Frigate restream docs note, so the password only applies to clients from outside the container. The detect width and height must match the sub stream you probed, or Frigate scales every frame. Leave out the version line and Frigate treats the file as a 0.13 config: our first start walked it through six migrations and saved a backup copy before it would run.
Create the Frigate Docker Compose file
The Compose file ties Mosquitto and Frigate together and decides which ports reach your network. Open it:
sudo vim "${APP_ROOT}/docker-compose.yml"
Paste the stack below. There is no devices block because our lab had no GPU; the detector section later shows what to add for one:
services:
mosquitto:
container_name: mosquitto
image: eclipse-mosquitto:2
restart: unless-stopped
ports:
- "1883:1883"
volumes:
- ./mosquitto/config:/mosquitto/config
- ./mosquitto/data:/mosquitto/data
- ./mosquitto/log:/mosquitto/log
frigate:
container_name: frigate
image: ghcr.io/blakeblackshear/frigate:stable
restart: unless-stopped
stop_grace_period: 30s
shm_size: "128mb"
depends_on:
- mosquitto
env_file: .env
ports:
- "8971:8971"
- "8554:8554"
volumes:
- /etc/localtime:/etc/localtime:ro
- ./config:/config
- ./storage:/media/frigate
- type: tmpfs
target: /tmp/cache
tmpfs:
size: 1000000000
The 1 GB tmpfs takes the constant rewrites of fresh recording segments off your disk. Shared memory is where decoded frames live, and 128 MB is the value we tested with a single 640×360 detect stream. It is also the floor. Frigate’s stats API reported a 126 MB minimum and room for exactly 20 frames, the count below which Frigate warns, because its sizing already reserves space for two 720p cameras added later from the UI. The 0.18.2 upgrade reported the same figures.
Larger detect streams cross that line. By the same formula, the minimum before Frigate warns is 146 MB with one 720p camera and 178 MB with two, and even the common 640×480 and 704×576 sub streams warn at 128 MB. The current Frigate install docs size for 25 frames instead and say Frigate needs “about 210MB for 2 cameras detecting at 720p”. Keep 128mb only if you run a single 640×360 stream like ours, and set shm_size to 256mb if your detect stream is any larger. When it runs short, Frigate logs a warning:
The current SHM size of 128.0MB is too small, recommend increasing it to at least 146MB.
That message comes from Frigate’s source rather than our lab, since our 360p camera never triggered it. The min_shm field in the stats API gives the same number, so check it after you add cameras.
The stable tag now pulls 0.18.2, a security release. It fixes two remote code execution bugs that run as root, one reachable from an admin account and one from a viewer account, as the 0.18.2 release notes explain. We moved our lab stack to it with sudo docker compose pull and sudo docker compose up -d. It reported healthy 10 seconds later with no config migration and logged a car 15 seconds after the start, and the restream still answered 401 without its login, so you lose nothing by upgrading. The only new log noise was an nginx [warn] about an uninitialized go2rtc_stream_proxy variable on every 15-second health check, which affected nothing we tested.
The stable tag follows each new release, and 0.x releases do carry breaking changes. If you’d rather upgrade on your own schedule, pin a tag yourself and make it 0.18.2 or later. Put the exact release from the Frigate releases page in place of stable, and before every upgrade you should copy config.yml and frigate.db aside, as the release notes ask.
Start Frigate and log in
Run Compose with sudo. The .env file is root-only on purpose, and a plain docker compose up from your own account stops at open /opt/frigate/.env: permission denied even when you’re in the docker group:
cd "${APP_ROOT}"
sudo docker compose up -d
sudo docker compose ps
Frigate reported healthy 9 seconds after Compose returned. Look for (healthy) on Frigate and only 8971, 8554 and 1883 bound to the host; 5000 shows up without a host mapping:
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
frigate ghcr.io/blakeblackshear/frigate:stable "/init" frigate 18 seconds ago Up 17 seconds (healthy) 5000/tcp, 0.0.0.0:8554->8554/tcp, [::]:8554->8554/tcp, 8555/tcp, 0.0.0.0:8971->8971/tcp, [::]:8971->8971/tcp, 8555/udp
mosquitto eclipse-mosquitto:2 "/docker-entrypoint.…" mosquitto 18 seconds ago Up 18 seconds 0.0.0.0:1883->1883/tcp, [::]:1883->1883/tcp
On first start Frigate creates an admin user with a random password and prints it once in the log. Pull it out of the log.
sudo docker logs frigate 2>&1 | grep -E "User:|Password:"
The password is a 32-character hex string on the second line, and it is printed only on the first start:
2026-10-15 20:31:41.479911889 [2026-10-15 20:31:41] frigate.app INFO : *** User: admin ***
2026-10-15 20:31:41.505997502 [2026-10-15 20:31:41] frigate.app INFO : *** Password: 5c0e9a7d21f84b6e93a1d07c4f2b8e16 ***
Ignore the nginx 502 and connect() failed lines in the first seconds of the log. They come from the health check probing the API before it’s up, and they stopped once FastAPI started. Our host showed this once both containers had settled:

Before you open the browser, prove the restream wants its password. The test runs in a throwaway container, which cannot reach Frigate on 127.0.0.1, so point it at the host’s LAN address. Replace 10.0.1.50 with your Docker host’s address in both commands:
sudo docker run --rm --entrypoint /usr/lib/ffmpeg/8.0/bin/ffprobe ghcr.io/blakeblackshear/frigate:stable -hide_banner -rtsp_transport tcp "rtsp://10.0.1.50:8554/front_cam"
go2rtc turns the anonymous request away:
[rtsp @ 0x57d5895b3140] method OPTIONS failed: 401 (Unauthorized)
rtsp://10.0.1.50:8554/front_cam: Server returned 401 Unauthorized (authorization failed)
Now pass the restream login in the URL. Anything that pulls the restream from another machine, Home Assistant included, needs these same credentials:
sudo docker run --rm --entrypoint /usr/lib/ffmpeg/8.0/bin/ffprobe ghcr.io/blakeblackshear/frigate:stable -hide_banner -rtsp_transport tcp "rtsp://${RESTREAM_USER}:${RESTREAM_PASS}@10.0.1.50:8554/front_cam" 2>&1 | grep -E "Input|Stream"
The 720p main stream answers. Detection was not affected by the login either: Frigate logged its first person 41 seconds after the container started with it enabled, then a car three seconds later.
Input #0, rtsp, from 'rtsp://viewer:[email protected]:8554/front_cam':
Stream #0:0: Video: h264 (Main), yuv420p(tv, smpte170m, progressive), 1280x720, 12 fps, 12 tbr, 90k tbn, start 0.083333
Browse to https://10.0.1.50:8971, using your host’s address. Frigate serves that port over TLS with a self-signed certificate it generates on first start, so the browser warns once; accept it for now, and the trusted certificate section removes the warning later. You’ll land on the login page:

Sign in as admin, then set your own password of at least 12 characters under Settings, Users, because the generated one only lives in the container log. If you ever lose it, Frigate has a documented recovery: add this block to config.yml, restart Frigate, then run the same grep and take the last Password: line, because the first-start password is still in the container log above it. Delete the block afterwards, since Frigate resets the password on every start while it is set.
auth:
reset_admin_password: true
We never needed it in the lab, so the block you see comes from the authentication docs rather than our own run. After you sign in, the live view opens straight away, and a red border marks a camera with an active object:

The small person icon in the top left of the tile is Frigate’s label for the object it is tracking. If the tile stays grey, go back to the ffprobe test; it’s almost always the URL or the password. The footage in this and the following screenshots is person-bicycle-car-detection.mp4 from Intel’s IoT DevKit sample-videos repository (github.com/intel-iot-devkit/sample-videos), licensed CC BY 4.0, which we re-encoded to 720p and 360p for the test camera.
Check detections and recordings
Detection is working when Review fills up without you touching anything. Within eight minutes our test camera had four alerts, each with a car and a person, and every one replays from the recording:

Explore lists every tracked object, and clicking one opens its snapshot with the label and score. Frigate keeps an object once its top score passes 70% by default, and this person peaked at 77%:

Our clip has a cyclist in it, but in our run Frigate tracked the rider as a person and never raised a bicycle object, so don’t read too much into a label you never see. Then confirm segments are landing on disk:
sudo du -sm "${APP_ROOT}/storage/recordings"
The number should grow every few minutes. Ours read 660 MiB after 38.6 minutes, which is where the storage table earlier came from:
660 /opt/frigate/storage/recordings
The current Frigate release can edit every setting from the web UI, and the fields read straight from your YAML. Settings, Global configuration, Recording shows the 3-day motion retention from config.yml, which is a quick way to confirm Frigate parsed the file the way you meant:

The core setup is done: one camera recording, objects detected, a login in front of the UI and a password on the restream. The next sections are optional, and each one stands on its own.
Add a zone and watch MQTT events
A zone tells you where an object went, which is what turns “a person somewhere in frame” into “a person at the gate”. Zones are drawn in normalized coordinates from 0 to 1, so they survive a change of detect resolution. Open the config again.
sudo vim "${APP_ROOT}/config/config.yml"
Add a zones block under the camera, right after live. Ours covers the marked parking bays in the top right of the frame:
zones:
parking_bays:
friendly_name: Parking bays
coordinates: 0.28,0.0,1.0,0.0,1.0,0.42,0.28,0.42
Frigate reads config.yml when it starts, so restart it after a manual edit and give it about 20 seconds to report healthy again.
cd "${APP_ROOT}"
sudo docker compose restart frigate
Settings, Camera configuration, Masks / Zones now draws the zone over the camera image. For your own cameras it is easier to draw the polygon here with the mouse and let Frigate write the coordinates:

MQTT is the quickest proof that the zone works, and it’s the same feed Home Assistant reads. Subscribe from inside the broker container and let jq trim each event down to the fields that matter; run sudo apt install -y jq first if your host does not have it:
sudo docker exec mosquitto mosquitto_sub -h 127.0.0.1 -u "${MQTT_USER}" -P "${MQTT_PASS}" -t frigate/events -C 30 | jq -c 'select(.type != "update") | {type, label: .after.label, score: .after.top_score, zones: .after.entered_zones}'
Each object produces a new event and later an end event, and the end lists every zone it entered. Two of the people here crossed parking_bays; the cars never did:
{"type":"new","label":"person","score":0.7265625,"zones":[]}
{"type":"new","label":"car","score":0.70703125,"zones":[]}
{"type":"new","label":"person","score":0.701171875,"zones":[]}
{"type":"end","label":"person","score":0.7265625,"zones":["parking_bays"]}
{"type":"end","label":"person","score":0.701171875,"zones":["parking_bays"]}
{"type":"end","label":"car","score":0.74609375,"zones":[]}
{"type":"new","label":"person","score":0.740234375,"zones":[]}
{"type":"end","label":"person","score":0.75390625,"zones":[]}
Once that works, list the zone under the camera’s review.alerts.required_zones if you only want alerts for objects that enter it. Everything else then drops to a detection, which keeps your phone quiet when people walk past on the street.
Choose a detector and hardware acceleration
With no detectors block, Frigate runs a TensorFlow Lite model on the CPU. Its own log warns that CPU detectors “should only be used for testing or for trial purposes”. Our System page agreed, with the inference time in red and a “Cpu is slow” warning in the status bar.

On an Intel CPU from 6th gen onward, OpenVINO is the first upgrade, and it runs on the integrated GPU. Pass the GPU into the container by adding this under the frigate service in docker-compose.yml. Apply it with sudo docker compose up -d from the project folder, because a plain restart keeps the old container and never adds the device.
devices:
- /dev/dri/renderD128:/dev/dri/renderD128
Then add the detector and the OpenVINO model to config.yml. This uses the SSDLite MobileNet model bundled in the image, which needs no download, so it is a quick starter; the detector docs mark YOLOv9 as the recommended OpenVINO model once you are ready to export one. Our generic vCPU had no GPU, so we ran this block with device: CPU, and OpenVINO refused to compile on that CPU (the error is in the last section). Use GPU on real Intel hardware:
detectors:
ov:
type: openvino
device: GPU
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
Dedicated accelerators take the load off the host entirely. The Coral is no longer the default pick for new builds; see why Frigate now points to Hailo and how the detector block changes. Video decoding is a separate job, and Frigate’s hwaccel_args defaults to auto, so a passed-through GPU is often picked up without any preset. When it is not, pick the preset for your GPU from this table, taken from the Frigate hardware acceleration docs:
| GPU | Preset | Note |
|---|---|---|
| Intel gen 1 to 12 | preset-vaapi | Gen 1 to 5 may need LIBVA_DRIVER_NAME=i965 |
| Intel gen 13+, Arc | preset-intel-qsv-h264 or -h265 | Arc B-series needs host kernel 6.12+ |
| AMD iGPU or dGPU | preset-vaapi | Set LIBVA_DRIVER_NAME=radeonsi |
| NVIDIA | preset-nvidia | Needs the NVIDIA Container Toolkit |
We could not test these presets in our lab, which had no GPU at all, so the two snippets below are untested and follow Frigate’s hardware acceleration docs. A preset can go in a top-level ffmpeg block in config.yml, which applies it to every camera. If you want it on one camera only, add the hwaccel_args line inside that camera’s existing ffmpeg block instead, because a second cameras: key would break the file:
ffmpeg:
hwaccel_args: preset-vaapi
The driver override from the table is an environment variable on the frigate service in docker-compose.yml, next to env_file:
environment:
LIBVA_DRIVER_NAME: radeonsi
Apply the config.yml change with sudo docker compose restart frigate, because Frigate reads that file only at start and up -d leaves a running container alone when only a mounted file changed. The compose edit needs sudo docker compose up -d, which recreates the container. The log line that tells you decoding is still on the CPU is Did not detect hwaccel, which ours printed on every start; once a preset works, that warning should go away.
Turn on face recognition and license plates
Both features run locally, and both are off by default. Frigate’s docs require a CPU with AVX and AVX2, and the small face model runs on the CPU while the large one wants a GPU or NPU. Add this to config.yml to switch them on:
face_recognition:
enabled: true
model_size: small
lpr:
enabled: true
After a restart, Frigate downloaded its PaddleOCR text models, the YOLOv9 plate detector and the FaceNet models from GitHub, which is the one time it needs internet for them. Our test clip is too small and too far away for faces or readable plates, so we saw no recognitions. The plate detector did run, at about 124 ms per pass on our generic vCPU without AVX. Frigate didn’t stop it, but its docs still list AVX and AVX2 as required for both features, so don’t treat them as optional on a real build.
We did not open the Face Library in this lab. Per the face recognition docs, you start by uploading one clear face with its Add Face wizard, then assign new detections to people from its Train tab. Plates are skipped until they cover 1,000 pixels of the frame by default, so an LPR camera needs to be close and zoomed in.
Connect Frigate to Home Assistant
Home Assistant talks to Frigate two ways: MQTT for events, and the Frigate integration for cameras, sensors and switches. Set up the MQTT integration first, because the Frigate integration depends on it. In Home Assistant, go to Settings, Devices & services, Add integration, MQTT, and enter these values:
- Broker:
10.0.1.50, your Frigate host - Port:
1883 - Username and password: the MQTT values you exported
The integration itself is not built into Home Assistant. HACS lists it as a default repository, so if you run HACS, search for Frigate there and install it. On a Home Assistant container you can copy the release into custom_components yourself, which is what we did. Point HA_CONFIG at the folder you mount as /config.
export HA_CONFIG="/opt/homeassistant/config"
export FHI_VER=$(curl -s https://api.github.com/repos/blakeblackshear/frigate-hass-integration/releases/latest | grep tag_name | cut -d '"' -f 4)
echo "${FHI_VER}"
curl -sL "https://github.com/blakeblackshear/frigate-hass-integration/archive/refs/tags/${FHI_VER}.tar.gz" | tar xz -C /tmp
sudo mkdir -p "${HA_CONFIG}/custom_components"
sudo cp -r "/tmp/frigate-hass-integration-${FHI_VER#v}/custom_components/frigate" "${HA_CONFIG}/custom_components/"
The echo should print the latest release tag, which starts with v. Restart Home Assistant, then add the Frigate integration from the same Add integration screen with these values:
- URL:
https://10.0.1.50:8971 - Validate SSL: off while Frigate uses its self-signed certificate
- Username and password: your Frigate login
The form suggests http://ccab4aaf-frigate:5000, which only fits the Frigate app on Home Assistant OS. A Home Assistant on another machine should use the authenticated port, because 5000 is not published here. Ours connected on the first try and created three devices with 57 entities:

The camera entity needs one more setting, because its live view pulls the restream on 8554, which now wants a password. Without it, Home Assistant logged Error from stream worker: Unauthorized error opening stream (Server returned 401 Unauthorized ...) every few seconds and the stream never started. Go to Settings, Devices & services, Frigate, click the cog (Configure) on the integration entry, and set RTSP URL template to:
rtsp://viewer:[email protected]:8554/{{ name }}
Use your own restream login and host address, with any @, :, / or % in the password URL-encoded for this URL. Save, then restart Home Assistant (Settings, System, the power icon at the top right, Restart Home Assistant, or docker restart on its container), because the stream that already failed keeps retrying the old address until then. We set the option through the API behind that form, and our logs kept showing 401s until the restart. After it, the camera returned an HLS playlist of the 720p H.264 stream at about 2.6 Mbps, and no 401 appeared. The Frigate integration docs describe the template, which can use any field from Frigate’s camera API; they still say the field needs advanced mode, but it now shows for everyone.
Those entities include camera.front_cam, a person count, an occupancy sensor per object and per zone, and switches for detection and recording. For an alert, an automation on the MQTT topic is enough. We posted this one to Home Assistant’s automation config API, which is what the automation editor saves through, and Home Assistant wrote it to automations.yaml as:
- id: frigate_person_front_cam
alias: Frigate person on front_cam
description: ''
triggers:
- trigger: mqtt
topic: frigate/events
conditions:
- condition: template
value_template: '{{ trigger.payload_json[''type''] == ''new'' and trigger.payload_json[''after''][''label'']
== ''person'' and trigger.payload_json[''after''][''camera''] == ''front_cam''
}}'
actions:
- action: persistent_notification.create
data:
title: Frigate
message: Person detected on {{ trigger.payload_json['after']['camera'] }}
mode: single
To add it yourself, go to Settings, Automations & scenes, Create automation, Create new automation, then open the three-dot menu, choose Edit in YAML and paste the block. Leave out the leading - list marker and the id line, because the editor adds both when it saves.
It fired within a minute of saving, and kept firing for every new person on the busy test clip:

Swap persistent_notification.create for your phone’s notify.mobile_app_ action once you trust it. On a real street, add the zone to the condition first, or every passer-by becomes a notification.
Serve the UI with a trusted certificate
Port 8971 is already HTTPS; the only problem is the self-signed certificate. Frigate looks for a certificate at /etc/letsencrypt/live/frigate inside the container, so you get rid of the warning by issuing a Let’s Encrypt certificate on the host and mounting it there. This section needs a DNS name for the host:
export FRIGATE_DOMAIN="frigate.example.com"
export ADMIN_EMAIL="[email protected]"
If the host is reachable on port 80 from the internet, the HTTP-01 challenge is the simplest route, and it works with any DNS provider:
sudo apt install -y certbot
sudo certbot certonly --standalone -d "${FRIGATE_DOMAIN}" --non-interactive --agree-tos -m "${ADMIN_EMAIL}"
Standalone mode starts its own web server on port 80 for the challenge, so nothing else on the host may hold that port while certbot runs.
Use DNS-01 when the NVR stays on your LAN
Most NVRs should never answer on the internet, and DNS-01 proves you own the name without opening anything. Install the certbot plugin for your DNS provider:
| DNS provider | Ubuntu and Debian package |
|---|---|
| Cloudflare | python3-certbot-dns-cloudflare |
| AWS Route 53 | python3-certbot-dns-route53 |
| DigitalOcean | python3-certbot-dns-digitalocean |
| Google Cloud DNS | python3-certbot-dns-google |
| Linode, OVH | python3-certbot-dns-linode, python3-certbot-dns-ovh |
| Your own BIND | python3-certbot-dns-rfc2136 |
With Cloudflare as the example, save an API token that can edit DNS for the zone, then request the certificate. Substitute your provider’s plugin and credentials file if you use another one:
sudo apt install -y certbot python3-certbot-dns-cloudflare
sudo install -m 600 /dev/null /etc/letsencrypt/cloudflare.ini
echo "dns_cloudflare_api_token = your-token-here" | sudo tee /etc/letsencrypt/cloudflare.ini >/dev/null
sudo certbot certonly --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini -d "${FRIGATE_DOMAIN}" --non-interactive --agree-tos -m "${ADMIN_EMAIL}"
We installed certbot and the Cloudflare plugin on the lab host but issued no certificate there, so the two issuance commands above are untested. Follow Frigate’s TLS docs and certbot’s own documentation for them. Whichever challenge you use, browsers also need the name to resolve to the NVR. A local DNS record or hosts entry is enough on a LAN, while HTTP-01 also needs the public A record and the port 80 forward kept in place for every renewal. What we did test end to end is the Frigate side, using a certificate laid out exactly the way certbot stores it.
Mount the certificate into Frigate
Certbot’s live folder holds symlinks into archive, and a container cannot follow a symlink to a folder it cannot see. Mount both, and let Compose read the domain from the .env file.
echo "FRIGATE_DOMAIN=${FRIGATE_DOMAIN}" | sudo tee -a "${APP_ROOT}/.env" >/dev/null
sudo vim "${APP_ROOT}/docker-compose.yml"
Add these two lines to the volumes list of the frigate service, after the ./storage line:
- /etc/letsencrypt/live/${FRIGATE_DOMAIN}:/etc/letsencrypt/live/frigate:ro
- /etc/letsencrypt/archive/${FRIGATE_DOMAIN}:/etc/letsencrypt/archive/${FRIGATE_DOMAIN}:ro
Recreate the container from the project folder so it picks up the new mounts, then check which certificate port 8971 serves now.
cd "${APP_ROOT}"
sudo docker compose up -d
echo | openssl s_client -connect 127.0.0.1:8971 -servername "${FRIGATE_DOMAIN}" 2>/dev/null | openssl x509 -noout -subject -issuer
The subject should be your domain. Our lab certificate came from a throwaway test CA, so its issuer reads differently from the Let’s Encrypt one you will see:
subject=CN=frigate.example.com
issuer=CN=Lab Test CA
Renewals need no restart. Frigate compares the mounted certificate’s fingerprint every minute, and when we swapped in a new one the log printed “Reloading nginx to refresh TLS certificate” 38 seconds later. If you also want plain https://frigate.example.com without a port, map 443:8971 in the ports list. Our Let’s Encrypt guide covers renewal hooks and rate limits in more depth.
Limit what the host exposes
Don’t rely on UFW to close Frigate’s ports. Docker’s packet filtering docs say published ports are routed before UFW’s rules apply, so a ufw deny on 1883 changes nothing. The Compose file is the real control. From a second machine on the same LAN, with the restream login in place, the ports answered like this:
- 8971: HTTP 401 until you log in
- 5000: connection refused, not published
- 8554: RTSP 401 without credentials, 200 with the restream login
- 1883: open, so the broker’s password is what protects it
On the Frigate host itself, an anonymous mosquitto_sub against the LAN address got “Connection Refused: not authorised”, so MQTT holds without the firewall too. You can go further and drop the 8554 mapping when nothing outside the host watches the cameras, but Home Assistant’s camera live view then loses its stream unless you point the RTSP URL template somewhere it can still reach. Publishing MQTT as "127.0.0.1:1883:1883" only suits a Home Assistant on the same host that runs with host networking; a containerised one on a bridge network cannot reach the host’s loopback. UFW still matters for SSH and everything outside Docker, and our UFW setup guide covers that side.
Fix common Frigate startup errors
Every error below happened in our lab while building this setup, so the fixes are tested against the exact message.
password-file: Error: Unable to open pwfile “/mosquitto/config/passwd”
Mosquitto shows Restarting (13) in docker compose ps and logs this line on every attempt. The password file was created by root with mode 600, and the broker runs as UID 1883. Change the owner and restart the broker:
sudo chown 1883:1883 "${APP_ROOT}/mosquitto/config/passwd"
cd "${APP_ROOT}"
sudo docker compose restart mosquitto
Restart it by hand rather than waiting, because Docker’s restart backoff had already stretched past 30 seconds by the time we fixed it. The broker then logs mosquitto version 2.1.2 running, and Frigate reconnects on its own.
RuntimeError: could not create a primitive (OpenVINO)
The OpenVINO detector failed to compile its model on our VM, whose Proxmox CPU type was kvm64, the backend default that a VM created from the command line can end up with. That type exposes no SSE4.2 or AVX. The traceback ends in Exception from src/inference/src/dev/plugin.cpp:58: could not create a primitive, then the watchdog logs “Detection appears to have stopped. Exiting Frigate…” and the container restarted eight times in five minutes.
Check what the guest can see before you blame Frigate, because the same VM ran the CPU detector without complaint and only the optimized code paths failed.
grep -ow 'sse4_2\|avx\|avx2' /proc/cpuinfo | sort | uniq -c
A healthy host prints a count for each flag, one per vCPU. On our kvm64 VM the command printed nothing at all, which means the hypervisor hides all three. The Proxmox web UI’s default for new VMs, x86-64-v2-AES, would show sse4_2 but still no avx or avx2. Set the CPU type to host, or x86-64-v3 if you need live migration, and power-cycle the VM; or remove the detectors block to fall back to the CPU detector, which did run on kvm64.
No TLS certificate found. Generating a self signed certificate… in a loop
We hit this when only the live folder was mounted. Frigate cannot follow certbot’s symlinks into archive, decides no certificate exists, then fails to write a self-signed one into the read-only mount:
2026-10-15 21:05:01.652850765 [INFO] No TLS certificate found. Generating a self signed certificate...
2026-10-15 21:05:02.565357695 [INFO] Service NGINX exited with code 1 (by signal 0)
2026-10-15 21:05:03.576121176 [INFO] Starting NGINX...
The container sat at health: starting for four minutes with the UI unreachable. Mounting the matching archive folder fixed it, and Frigate came up healthy in 15 seconds with the mounted certificate.
Error from stream worker: Unauthorized error opening stream (401)
Home Assistant logs this when the restream has a password and the Frigate integration still uses its default RTSP address. Frigate’s own detection and recording keep working, so the only symptom is a camera card that never plays. Set the RTSP URL template with the restream login as shown in the Home Assistant section; in our lab the 401s stopped after that change and a restart of Home Assistant.
open /opt/frigate/.env: permission denied
Compose reads .env as the user who runs it, not as the Docker daemon, so membership in the docker group does not help with a root-only file. Run sudo docker compose for every stack command, and keep the file at mode 600 because it holds your camera and MQTT passwords.