Fn
Applications Azure

Frigate NVR on Ubuntu 24.04 on Azure User Guide

| Product: Frigate NVR on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of Frigate NVR on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Frigate is an open source network video recorder built around realtime object detection. Instead of triggering on raw pixel change the way a traditional motion recorder does, Frigate runs each camera's stream through an object detection model and tells you that a person, a car or an animal was seen, where it was, and for how long.

That distinction is the whole point of the product. Motion detection fires on headlights, rain, moving shadows and swaying branches. Object detection does not, so the alerts you get are the ones worth looking at, and the recordings you keep are the ones worth keeping.

Everything happens on your own VM. Frames are never sent to a cloud service, the detection model runs locally, and the recordings stay on your disk.

The cloudimg image ships the free and open source, MIT licensed Frigate as the official upstream container image, pinned by digest and tied to the exact upstream commit it was built from. No camera address and no camera credential is ever baked into the image — you connect your own. A unique gate credential and a unique Frigate admin password are generated for each VM on first boot, and the gate in front of Frigate will not start until first boot has proved that an anonymous request is answered with 401. Backed by 24/7 cloudimg support.

Frigate(TM), Frigate NVR(TM) and the Frigate logo are trademarks of Frigate, Inc. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by Frigate, Inc. It ships the free and open source MIT licensed software, unmodified. The names are used here solely to identify the software this image contains.

The frigate, nginx and docker services all active on the appliance, with Frigate bound to loopback and nginx on port 80

What is included:

  • Frigate NVR 0.17.2 — the official upstream image, pinned by digest sha256:d4351369…96967010, never by a moving tag
  • Verified provenance: the image stamps its own build commit into its version string, and this appliance asserts it reads 0.17.2-3d4dd3a, matching upstream commit 3d4dd3ac4b00e7257bd3412608a783001d7d77ed
  • The CPU object detector with Google's Apache-2.0 ssdlite_mobiledet_coco model — no Coral TPU, no GPU and no TensorRT in this image
  • An nginx access gate on port 80, installed as a host package so unattended security upgrades keep the internet facing component patched
  • Frigate's own authentication left enabled as a genuine second layer, with a per VM admin password
  • Embedded SQLite storage — no database server of any kind on the instance
  • A shipped dependency licence inventory at /usr/share/frigate/dependency-licences.txt covering all 959 Python and frontend dependencies
  • The verbatim configuration of every bundled ffmpeg build at /usr/share/frigate/ffmpeg-configuration.txt

Prerequisites

  • An Azure subscription with permission to create VMs
  • One or more IP cameras that expose an RTSP stream, reachable from the VM. You connect these yourself after launch — nothing is preconfigured
  • A Standard_B2ms VM (2 vCPU, 8 GiB) or larger. See Step 10: sizing and the realistic camera load before you commit to a size, because CPU object detection is genuinely compute hungry
  • Inbound TCP 80 open only to the networks your administrators connect from. This is a camera system; treat access to it the way you would treat access to the cameras themselves
  • An SSH key pair for administrative access

Step 1: Deploy from the Azure Portal

  1. In the Azure Portal, go to Create a resource and search the Marketplace for Frigate NVR on Ubuntu 24.04 LTS by cloudimg.
  2. Select the offer and choose Create.
  3. On the Basics tab pick your subscription, resource group and region, name the VM, and choose size Standard_B2ms.
  4. Set the authentication type to SSH public key and supply your key. The admin username is azureuser.
  5. On the Networking tab allow inbound SSH (22) and HTTP (80), and restrict the source to your own address range.
  6. On the Disks tab, consider increasing the OS disk if you plan to keep more than a couple of days of recordings — see Step 11: recording storage and retention.
  7. Review and create.

Step 2: Deploy from the Azure CLI

az vm create \
  --resource-group my-resource-group \
  --name frigate-nvr \
  --image cloudimg:frigate-ubuntu-24-04:default:latest \
  --size Standard_B2ms \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard \
  --os-disk-size-gb 128

# Then open the two ports you need, scoped to your own network rather than the
# internet. These run on YOUR workstation, not on the appliance.
az vm open-port --resource-group my-resource-group --name frigate-nvr --port 22 --priority 1001
az vm open-port --resource-group my-resource-group --name frigate-nvr --port 80 --priority 1002

Step 3: Connect to your VM

ssh azureuser@<vm-ip>

Step 4: Confirm the services are running

Three units make up the appliance: docker runs the pinned Frigate container, frigate is the container's systemd wrapper, and nginx is the access gate that fronts it.

sudo systemctl is-active docker frigate nginx

Expected output:

active
active
active

First boot does real work — it generates this VM's credentials, proves the gate rejects anonymous callers, starts Frigate, and sets Frigate's own admin password. Confirm it finished:

sudo systemctl is-active frigate-firstboot.service
test -f /var/lib/cloudimg/frigate-firstboot.done && echo "first boot complete"

Expected output:

active
first boot complete

Step 5: Read this VM's credentials

Every credential on this machine was generated for this machine at first boot. Nothing is shared with any other cloudimg VM and nothing is baked into the image.

sudo cat /root/frigate-credentials.txt

You will see two separate credentials, and you need both:

FRIGATE_URL=http://<vm-ip>/

# 1. The cloudimg gate (the browser will prompt for this first)
GATE_USERNAME=frigate
GATE_PASSWORD=<GATE_PASSWORD>

# 2. Frigate's own sign-in (presented after the gate)
FRIGATE_USERNAME=admin
FRIGATE_PASSWORD=<FRIGATE_PASSWORD>

Store both in your password manager and then treat the file as a recovery copy.

Step 6: Open the interface

Browse to http://<vm-ip>/.

Your browser will prompt for the gate credential first. Enter GATE_USERNAME and GATE_PASSWORD.

Behind the gate you get Frigate's own sign-in page. Enter FRIGATE_USERNAME and FRIGATE_PASSWORD.

Frigate's sign-in page, reached only after the cloudimg gate has already accepted this VM's own credential

Once signed in you will land on the live dashboard. On a brand new VM it looks like this, because no cameras are configured yet — that is the state the image ships in, and it is deliberate:

The Frigate live dashboard on a fresh VM showing "No Cameras Configured" and an Add Camera button, with the system reporting healthy

Step 7: Understand the access model

This is the most important section in the guide. An unprotected NVR streams your cameras to anyone who finds it, so it is worth understanding exactly what is protecting this one.

Frigate listens on two ports and they are not equivalent.

  • Port 5000 is Frigate's internal port. Upstream documents it as internal, and its own source states that requests arriving there are treated as admin with no authentication. On a stock install this port answers /api/config — which, once you add cameras, contains your camera RTSP URLs including their passwords.
  • Port 8971 is Frigate's authenticated port.

On this appliance both are bound to 127.0.0.1 only. Neither is reachable from the network. You can confirm that yourself:

sudo ss -lntH | awk '{print $4}' | grep -E ':(80|5000|8971)$' | sort

Expected output — only port 80 is on a routable address:

0.0.0.0:80
127.0.0.1:5000
127.0.0.1:8971
[::]:80

The only way in is nginx on port 80, and nginx is default deny at server scope. Every route inherits the gate: the interface, the entire REST API, /api/config, the MJPEG and HLS stream endpoints, the websocket that drives live view, and anything a future Frigate version adds. Exactly one route is open, and it is nginx's own static health literal, which never reaches Frigate and discloses nothing.

Check it for yourself:

for p in / /api/config /ws /vod/ /api/stats; do
  printf '%-14s -> %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1$p")"
done
printf '%-14s -> %s\n' "/nginx-health" "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/nginx-health)"

Expected output:

/              -> 401
/api/config    -> 401
/ws            -> 401
/vod/          -> 401
/api/stats     -> 401
/nginx-health  -> 200

Anonymous requests refused with 401 on every route, a wrong password refused, this VM's own credential accepted, and Frigate's own ports bound to loopback

Two real layers

Passing the gate is not the same as being signed in to Frigate. With the gate credential but no Frigate session, the API still refuses you:

curl -s -o /dev/null -w 'with gate credential only: %{http_code}\n' \
  -u '<GATE_USERNAME>:<GATE_PASSWORD>' http://127.0.0.1/api/config

Expected output:

with gate credential only: 401

Sign in to Frigate as well and the same call succeeds:

# Wait for Frigate to be answering before asking it anything.
for i in $(seq 1 45); do
  code=$(curl -s -o /dev/null -w '%{http_code}' -u '<GATE_USERNAME>:<GATE_PASSWORD>' http://127.0.0.1/api/version)
  case "$code" in 401|200) break ;; esac
  sleep 2
done

JAR=$(mktemp)
curl -s -o /dev/null -c "$JAR" -u '<GATE_USERNAME>:<GATE_PASSWORD>' \
  -X POST -H 'Content-Type: application/json' \
  -d '{"user":"<FRIGATE_USERNAME>","password":"<FRIGATE_PASSWORD>"}' \
  http://127.0.0.1/api/login
curl -s -o /dev/null -w 'with both credentials: %{http_code}\n' \
  -b "$JAR" -u '<GATE_USERNAME>:<GATE_PASSWORD>' http://127.0.0.1/api/config
rm -f "$JAR"

Expected output:

with both credentials: 200

The gate cannot start unconfigured

nginx.service carries two ConditionPathExists guards plus an ExecStartPre preflight check. It will refuse to start if this VM's gate credential is missing, empty, or stored as anything other than a password hash, or if the gate map is still in the deny-all form the image ships with. The marker that permits nginx to start is created by first boot only after it has proved, against a throwaway loopback-only listener running the real per VM gate configuration, that an anonymous request gets exactly 401, that a one character change to the password is refused, that a wrong username is refused, and that a tampered gate cookie is refused.

In other words: if provisioning ever goes wrong, this appliance serves nothing rather than serving your cameras to the internet.

Step 8: Add your first camera

Nothing about your cameras is preconfigured, so this is where you take over. You will need, for each camera, an RTSP URL, usually of the form rtsp://username:password@camera-ip:554/some/path. Your camera's manual or its web interface will tell you the exact path.

The simplest route is the interface: from the dashboard choose Add Camera, or go to Settings → Cameras → Management.

Frigate's Settings page, with Cameras, Users, Roles and Notifications sections, where cameras are added and users managed

You can also edit the configuration file directly. It lives at /var/lib/frigate/config/config.yml on the VM and is also editable in the interface under Config:

The Frigate configuration editor showing the shipped zero-camera configuration with the CPU detector and conservative retention defaults

A minimal, sensible first camera looks like this. Note the two streams: a small one for detection and the full quality one for recording. This split is what makes CPU detection viable, and it matters more on this appliance than on any other setup.

cameras:
  front_door:
    ffmpeg:
      inputs:
        # Low resolution substream -> object detection. Keep this SMALL.
        - path: rtsp://USER:PASSWORD@192.168.1.50:554/substream
          roles: [detect]
        # Full quality main stream -> recording, copied without re-encoding.
        - path: rtsp://USER:PASSWORD@192.168.1.50:554/mainstream
          roles: [record]
    detect:
      width: 640
      height: 480
      fps: 5
    objects:
      track:
        - person
        - car

After editing, use Save & Restart in the configuration editor, or restart the service:

sudo systemctl restart frigate

# Frigate takes 20-40 seconds to load its model and reopen its ports. Wait for
# it rather than assuming — a request sent too early gets 502 from the gate.
for i in $(seq 1 45); do
  code=$(curl -s -o /dev/null -w '%{http_code}' -u '<GATE_USERNAME>:<GATE_PASSWORD>' http://127.0.0.1/api/version)
  # Warm-up goes 502 (gate cannot reach Frigate) -> 500 -> 401 (Frigate is up and
  # asking for its OWN sign-in). Only 401 or 200 means it is genuinely ready.
  case "$code" in 401|200) break ;; esac
  sleep 2
done
echo "Frigate is back (gate probe returned $code)"

Keep camera passwords out of the configuration file where you can. Frigate supports environment substitution, so you can put the secret in an environment file readable only by root and reference it as {FRIGATE_CAMERA_PASSWORD} in the URL.

Step 9: Confirm detection is running

The CPU detector is what does the actual work. Confirm it is alive and see how fast it is on your hardware:

JAR=$(mktemp)
curl -s -o /dev/null -c "$JAR" -u '<GATE_USERNAME>:<GATE_PASSWORD>' \
  -X POST -H 'Content-Type: application/json' \
  -d '{"user":"<FRIGATE_USERNAME>","password":"<FRIGATE_PASSWORD>"}' \
  http://127.0.0.1/api/login
STATS=$(curl -s -b "$JAR" -u '<GATE_USERNAME>:<GATE_PASSWORD>' http://127.0.0.1/api/stats)
echo "$STATS" | jq -r '.detectors | to_entries[] | "detector \(.key) inference \(.value.inference_speed) ms"'
echo "$STATS" | jq -r '"cameras configured: \(.cameras | length)"'
rm -f "$JAR"

On a fresh Standard_B2s build VM with no cameras this reports roughly:

detector cpu1 inference 10.0 ms
cameras configured: 0

The same numbers are visible in the interface under System → General, along with per process CPU and memory:

Frigate's System page showing the cpu1 detector at 10ms inference speed with CPU and memory charts drawn, and the system reporting healthy

Frigate version, detector inference speed, camera count and the confirmation that the shipped configuration was accepted rather than falling back to safe mode

If you ever see Safe Mode in the interface, Frigate rejected your configuration file and fell back to a minimal one. The banner tells you the offending line. Fix it and restart.

Step 10: Sizing, and the realistic camera load

This appliance ships CPU only detection. There is no Coral TPU, no GPU and no TensorRT in this image. That is a deliberate, honest constraint and it bounds how many cameras a given VM can carry.

Upstream is blunt about this, and so are we. Every time Frigate starts here it logs:

frigate.detectors WARNING : CPU detectors are not recommended and should only be
used for testing or for trial purposes.

That warning is upstream's, it is accurate, and this image does not hide it. Read it as: CPU detection genuinely works and is genuinely useful at small scale, but it is not what you should build a twenty camera installation on. If you are evaluating Frigate, running one or two cameras at home or on a small site, or proving out zone and object configuration before investing in hardware, this appliance is a good fit. If you need many cameras or high frame rates, you want a hardware accelerator, and that is not something an image for general purpose Azure VM sizes can provide.

What makes it workable at all is that Frigate is efficient by design:

  • Object detection runs only on regions where motion was already detected, not on every frame
  • Motion detection runs on the low resolution detect substream, not the full quality stream
  • Recording copies the camera's existing H.264 stream rather than re-encoding it, which is nearly free

What that adds up to in practice:

VM size vCPU / RAM Realistic cameras Notes
Standard_B2s 2 / 4 GiB 1 Workable for a single camera; RAM is tight for an NVR
Standard_B2ms 2 / 8 GiB 1 to 2 Recommended default
Standard_B4ms 4 / 16 GiB 3 to 4 The right choice for a small property
Standard_B8ms 8 / 32 GiB 5 to 8 Diminishing returns; consider hardware acceleration instead

These figures assume the detect substream at 640x480 and 5 fps per camera, recording by stream copy, and no transcoding. They are guidance, not a guarantee — a busy driveway with constant motion costs far more detection time than a quiet back garden.

Things that will push you up a size, or past what CPU detection can sensibly do:

  • A detect stream larger than 640x480, or a detect fps above 5
  • Enabling any transcoding rather than stream copy for recordings
  • Continuous 24/7 recording on several cameras at once
  • Optional extras such as face recognition, licence plate recognition or semantic search, which are considerably heavier than plain object detection

If you need more cameras than the table suggests, the honest answer is that CPU detection is the wrong tool and you want a hardware accelerator. This image does not provide one. Upstream Frigate supports Google Coral TPUs, Intel/AMD GPUs and NVIDIA TensorRT, none of which is available in an image built for general purpose Azure VM sizes.

Step 11: Recording storage and retention

Recordings and snapshots are written to /var/lib/frigate/media on the OS disk. The directory is 0750 root:root, so it is not world readable, and nginx is configured with no root, alias or autoindex directive anywhere — recordings are reachable only through Frigate, which is behind the gate.

sudo stat -c '%a %U:%G %n' /var/lib/frigate/media

Expected output:

750 root:root /var/lib/frigate/media

An NVR fills a disk faster than almost anything else you will run. A rough rule for a single 1080p H.264 camera at a typical 2 Mbps:

  • 2 Mbps is about 0.9 GB per hour, so roughly 21 GB per camera per day of continuous recording
  • Motion-only recording typically costs a fraction of that, but the fraction depends entirely on how busy the scene is

The image therefore ships conservative retention defaults so that adding a camera cannot silently fill your disk:

  • Continuous recording: off (record.continuous.days: 0)
  • Motion recording: 2 days
  • Alerts and detections: 14 days
  • Snapshots: 7 days

Raise these deliberately, once you have sized storage. Check what you are actually using:

df -h /var/lib/frigate/media
sudo du -sh /var/lib/frigate/media 2>/dev/null || true

Adding a dedicated data disk

No data disk is baked into this image on purpose: the right size depends entirely on your camera count, resolution and retention, so a fixed size captured into the image would be wrong for nearly everyone. To move recordings onto a dedicated disk, attach one and mount it:

# Run this on YOUR workstation, not on the appliance.
az vm create --help >/dev/null   # (az CLI is not installed on the VM itself)
az vm disk attach --resource-group my-resource-group --vm-name frigate-nvr \
  --name frigate-media --new --size-gb 512 --sku StandardSSD_LRS

Then, on the VM:

sudo systemctl stop frigate
DEV=/dev/disk/azure/scsi1/lun0
sudo mkfs.ext4 -F -L frigate-media "$DEV"
UUID=$(sudo blkid -s UUID -o value "$(readlink -f "$DEV")")
echo "UUID=$UUID /var/lib/frigate/media ext4 defaults,nofail 0 2" | sudo tee -a /etc/fstab
sudo mount /var/lib/frigate/media
sudo chown root:root /var/lib/frigate/media && sudo chmod 0750 /var/lib/frigate/media
sudo systemctl start frigate

Step 12: What is in the image, and under what licence

Frigate itself is MIT licensed. This appliance ships the official upstream container image unmodified, pinned by digest, and the build refuses to proceed unless the image's own version string identifies the exact upstream commit whose licence tree was reviewed.

Two things are worth calling out specifically.

ffmpeg. Frigate bundles ffmpeg, and ffmpeg's licence depends entirely on how it was compiled. The builds in this image are compiled --enable-gpl --enable-version3, which makes those binaries GPL-3.0 rather than the LGPL an unconfigured ffmpeg would be. Neither build is compiled --enable-nonfree, so both are freely redistributable. The complete, verbatim configuration line of every bundled build is recorded on the VM:

grep -E '^ffmpeg version|^===' /usr/share/frigate/ffmpeg-configuration.txt
# `grep -c` exits 1 when the count is zero, which is the HEALTHY answer here.
echo "--enable-nonfree occurrences: $(grep -c -- '--enable-nonfree' /usr/share/frigate/ffmpeg-configuration.txt || true)"

Expected output — two builds, and zero nonfree:

=== /usr/lib/ffmpeg/5.0/bin/ffmpeg ===
ffmpeg version n5.1-2-g915ef932a3-20220731 Copyright (c) 2000-2022 the FFmpeg developers
=== /usr/lib/ffmpeg/7.0/bin/ffmpeg ===
ffmpeg version n7.0.2-18-g3e6cec1286-20240919 Copyright (c) 2000-2024 the FFmpeg developers
--enable-nonfree occurrences: 0

Those builds include the libx264 (H.264) and libx265 (H.265/HEVC) encoders, which are patent encumbered. Frigate's default recording path copies your camera's existing stream rather than re-encoding it, so a default deployment does not use them. If you enable transcoding, your codec licensing position is yours to assess.

Detection models. Every bundled model is a Google or Intel Apache-2.0 asset — nothing is research-only or non-commercial. The CPU detector this appliance uses is Google's ssdlite_mobiledet_coco.

The full resolved licence inventory for all 959 Python and frontend dependencies ships on the VM:

grep -vc '^#' /usr/share/frigate/dependency-licences.txt
awk -F'\t' '!/^#/{print $4}' /usr/share/frigate/dependency-licences.txt | sort | uniq -c | sort -rn | head -6

The shipped dependency licence inventory, its licence distribution, and the ffmpeg verdict showing enable-gpl present and enable-nonfree absent

The complete notices, including the trademark position, are at:

sudo head -20 /usr/share/frigate/THIRD-PARTY-NOTICES

Day to day operation

sudo systemctl restart frigate       # restart after a configuration change
sudo systemctl status frigate        # check the service
sudo journalctl -u frigate -n 50     # recent logs

Frigate's own logs are also visible in the interface under System → Logs.

To change the Frigate admin password, use Settings → Users in the interface. To change the cloudimg gate password:

# Verify the command shape against a scratch file first, so a typo cannot lock
# you out of your own appliance:
sudo htpasswd -bBc /tmp/frigate-gate-example frigate 'replace-with-your-own-password'
sudo rm -f /tmp/frigate-gate-example

Once you are happy with it, apply it for real and reload the gate:

sudo htpasswd -bB /etc/nginx/frigate.htpasswd frigate 'your-new-strong-password'
sudo systemctl reload nginx

Security hardening

  • Restrict port 80 to your own network. This is a camera system. The gate is strong, but the smallest attack surface is the one nobody can reach
  • Put TLS in front. The appliance speaks plain HTTP by design so it works on a bare IP. For anything beyond a private network, terminate TLS with a certificate for a real hostname
  • Rotate both credentials after your first sign-in and store them in a password manager
  • Keep camera passwords out of config.yml by using environment substitution
  • Unattended security upgrades are enabled, so the OS and nginx stay patched

Troubleshooting

The browser asks for a password twice. That is correct. The first prompt is the cloudimg gate, the second is Frigate's own sign-in. They are different credentials and both are in /root/frigate-credentials.txt.

Everything returns 401, including with the right password. Confirm nginx started: sudo systemctl status nginx. If it refused to start, the preflight guard blocked it — check sudo journalctl -u nginx -n 30 for a frigate-preflight message naming what is missing.

Frigate says Safe Mode. Your config.yml failed validation and Frigate fell back to a minimal configuration. The banner names the offending line. A common cause is copying a configuration written for Frigate 0.16 — record.retain was removed in 0.17 and split into record.continuous and record.motion.

A camera will not connect. Test the RTSP URL from the VM itself before blaming Frigate:

# ffprobe is NOT on PATH inside the Frigate container — use its absolute path.
# Confirm the tool is where it should be:
sudo docker exec frigate /usr/lib/ffmpeg/7.0/bin/ffprobe -version | head -1

Then probe your own camera with that same absolute path. A working camera prints its codec and resolution; a bad URL, wrong credential or unreachable camera prints a connection error instead:

sudo docker exec frigate /usr/lib/ffmpeg/7.0/bin/ffprobe -v error \
  -show_entries stream=codec_name,width,height \
  -of default=noprint_wrappers=1 "rtsp://USER:PASSWORD@CAMERA-IP:554/substream"

Detection is slow or the CPU is pinned. Check the detect stream is genuinely the small substream and not the main stream, and that detect.fps is 5 rather than the camera's native rate. If it is already minimal, you have reached the limit of CPU detection for that camera count — see Step 10.

The disk is filling. Lower retention in config.yml, or attach a data disk as described in Step 11.

Support

This image is published and supported by cloudimg. For assistance with the image, the deployment guide, or the appliance configuration, contact cloudimg support. For questions about Frigate itself, the upstream project's documentation at docs.frigate.video and its community are the best resource.