Frigate NVR 0.18.2 on AWS User Guide
Overview
This guide covers launching and verifying Frigate NVR 0.18.2 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened SRS Streaming Media Server & Media Servers AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other media and streaming products in the listing are described on the listing's guide hub.
Frigate is a free, open source network video recorder (MIT licence). It records your IP cameras and runs object detection on their streams, so it can record and alert on what a camera actually sees (a person, a car, a dog) rather than on raw pixel motion. All detection runs on your own instance; no video or image leaves it unless you configure a notification service.
What is on the image:
- Frigate NVR 0.18.2, the official upstream container image
ghcr.io/blakeblackshear/frigate, pinned by its linux/amd64 manifest digest and pulled at build time (your instance never pulls an image) - Docker CE from Docker's official repository, configured so that Frigate's container cannot reach the instance metadata service
- nginx serving HTTPS on port 443 with a certificate created on your instance; port 80 redirects to HTTPS
- A per-instance gate login in front of every page, stream and API call, then Frigate's own per-instance admin login, both created on first boot
- No cameras configured and the CPU detector enabled, so you add your own cameras (Step 6)
frigate-set-hostname(serve Frigate under your own DNS name) andfrigate-selftest(an end-to-end check)
Default user: ubuntu · Architecture: x86_64 · Root volume: 24 GiB gp3
Security posture
- No open default and no shared password. nginx puts a sign-in (the "gate", HTTP Basic over HTTPS) in front of every path: the web interface, the API, the live-view WebSocket, recordings and clips. Behind it, Frigate's own login protects the interface with the
adminaccount. The gate password (32 characters) and the admin password (28 characters) are created on your instance's first boot and written only to/root/frigate-credentials.txt(mode 0600, root). - The first admin password never reaches a log. Upstream Frigate prints the random password it gives its first admin to its log. On this image Frigate's datastore is created in a container with no network and no log at all, and the admin's password hash is replaced with this instance's own before Frigate is ever reachable.
- Port 5000 is never exposed. Frigate serves an internal API on port 5000 with no authentication at all. On this image it listens only on the container's own loopback interface; only Frigate's authenticated port 8971 is published, to the instance's loopback, and only nginx reaches it. A firewall rule on Frigate's bridge network also keeps go2rtc's ports (1984, 8554, 8555) unreachable.
- The container cannot read instance metadata. A rule in Docker's
DOCKER-USERchain, applied before dockerd starts any container, drops traffic to 169.254.169.254 and fd00:ec2::254. The Frigate container therefore cannot fetch the instance's IAM role credentials. Docker, Frigate's unit and nginx also carry systemdIPAddressDenyfor those addresses. - Cross-site requests are refused. nginx refuses state-changing requests and WebSocket upgrades whose
OriginorRefereris another site (403), and Frigate itself requires itsX-CSRF-TOKENheader on every API write. - Per-instance secrets. The gate password, the gate cookie token, the admin password, Frigate's JWT signing secret, its database and the TLS key are all created on your instance. The image ships without a database.
- HTTPS from first boot. TLS 1.2 and 1.3 only (TLS 1.1 is refused),
Strict-Transport-Security, andSecurecookies. The host name comes from configuration, never from a request: first boot reads the instance's public DNS name from EC2 instance metadata (IMDSv2) and pins nginx to it; requests for any other host name get their connection closed. - Verified build. The image digest was scanned for known vulnerabilities before the build. The three FFmpeg builds inside the image are GPL builds with no non-free components, and every Python and JavaScript dependency of this Frigate release was checked for its licence. Frigate's version check is turned off, so it makes no calls home.
- Hardened SSH and system. SSH allows keys only, no root login and no X11 forwarding. The AMI requires IMDSv2. SSH host keys and the machine ID are generated fresh on every instance.
Prerequisites
- An AWS account and permission to launch EC2 instances
- An EC2 key pair in the Region you launch in
- IP cameras that offer an RTSP stream, reachable from the instance (for example over a site-to-site VPN or AWS Direct Connect, or cameras with a public RTSP address)
Recommended instance type: m5.large (2 vCPUs, 8 GiB). Frigate's CPU detector and recorder want at least 4 GiB of memory. Object detection on the CPU handles a few cameras at a low detection frame rate; size up (for example m5.xlarge or a compute-optimised c5 type) for more cameras, and see Step 6 for the settings that cut CPU use.
Connecting to your instance
| Version | Operating system | SSH user |
|---|---|---|
| Frigate NVR 0.18.2 on Ubuntu 24.04 | Ubuntu 24.04 LTS | ubuntu |
Step 1: Launch the instance
Option A: AWS Marketplace console
- Open the cloudimg Hardened SRS Streaming Media Server & Media Servers AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
- Once the subscription is active, choose Launch your software.
- Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose Frigate NVR 0.18.2 on Ubuntu 24.04, then choose your Region.
- Pick an instance type (
m5.largerecommended) and your key pair. - Security group: the listing's default group opens SSH (22), HTTP (80) and HTTPS (443). Restrict SSH to your own IP address and restrict 80 and 443 to the networks you view Frigate from. Never open 5000, 8971, 1984, 8554 or 8555.
- Choose your VPC and subnet, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.
Option B: AWS CLI
Run this on your own workstation. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and CIDRs with your own:
SG=$(aws ec2 create-security-group --group-name frigate-nvr \
--description "Frigate NVR" --vpc-id vpc-0123456789abcdef0 \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 443 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 80 --cidr <your-mgmt-cidr>
aws ec2 run-instances \
--image-id ami-0123456789abcdef0 \
--instance-type m5.large \
--key-name my-key-pair \
--security-group-ids "$SG" \
--subnet-id subnet-0123456789abcdef0 \
--block-device-mappings 'DeviceName=/dev/sdf,Ebs={VolumeSize=500,VolumeType=gp3}' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=frigate-nvr}]'
The block device mapping adds an empty 500 GiB volume for recordings (Step 8); leave it out to record on the root volume. You do not need --metadata-options: the AMI already makes IMDSv2 required. Associate an Elastic IP if you want the address to survive a stop and start.
Step 2: Connect and find your logins
Connect as ubuntu with the key pair you chose:
ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>
First boot takes one to two minutes after the instance starts (a little longer on the very first start, while EC2 reads the Frigate image from its snapshot). It sets the name Frigate is served as (the instance's public DNS name when it has one), creates the TLS certificate and both logins, lets Frigate create its database and signing secret, and then opens nginx. The logins are in a root-only file; this shows its addresses, user names and fields without printing the passwords:
sudo grep -E '^(FRIGATE_URL|GATE_USERNAME|FRIGATE_USERNAME)=' /root/frigate-credentials.txt
sudo grep -oE '^[A-Z_]+=' /root/frigate-credentials.txt
sudo stat -c '%a %U:%G %n' /root/frigate-credentials.txt
FRIGATE_URL=https://ec2-3-95-132-108.compute-1.amazonaws.com/
GATE_USERNAME=frigate
FRIGATE_USERNAME=admin
FRIGATE_URL=
GATE_USERNAME=
GATE_PASSWORD=
FRIGATE_USERNAME=
FRIGATE_PASSWORD=
600 root:root /root/frigate-credentials.txt
| Value | What it is for |
|---|---|
FRIGATE_URL |
The address of this instance's Frigate web interface |
GATE_USERNAME / GATE_PASSWORD |
The gate: your browser asks for these first, on every new browser session |
FRIGATE_USERNAME / FRIGATE_PASSWORD |
Frigate's own login page, shown after the gate (the admin account) |
To read the passwords, run sudo cat /root/frigate-credentials.txt in your own session and store them in your password manager.
Step 3: Confirm Frigate is running
The first-boot unit has finished; Docker, the metadata block, Frigate and nginx are active; exactly one container runs, from the pinned image, publishing only port 8971 to the loopback interface. The only public listening sockets are sshd and nginx on 22, 80 and 443; Frigate's 8971 is on 127.0.0.1 only:
systemctl is-active frigate-firstboot docker cloudimg-docker-imds-deny frigate nginx
sudo ss -Htlnp | awk '{print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u
cat /usr/share/frigate/VERSION
sudo docker ps --format '{{.Names}} {{.Image}} {{.Ports}}'
sudo docker image ls --digests --format '{{.Repository}}:{{.Tag}} {{.Digest}}'
active
active
active
active
active
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
127.0.0.1:8971 users:(("docker-proxy"))
127.0.0.53%lo:53 users:(("systemd-resolve"))
127.0.0.54:53 users:(("systemd-resolve"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))
0.18.2-cfa08c5
frigate ghcr.io/blakeblackshear/frigate 5000/tcp, 8554-8555/tcp, 8555/udp, 127.0.0.1:8971->8971/tcp
ghcr.io/blakeblackshear/frigate:0.18.2 sha256:9ebab4801be69ab4ebc7f6ab1d582c859ff7412d457a3fa550056894e8444dad
Step 4: Prove the logins and the web defaults
Every page and API path refuses a signed-out request, the health check answers, plain HTTP redirects to HTTPS, an unknown Host header gets no answer and TLS 1.1 is not offered. The last command is the image's own round-trip check: it signs in through the gate and Frigate's login with this instance's credentials over https://<your-name>/, and proves that wrong passwords are refused:
H=$(sudo sed -nE 's#^FRIGATE_URL=https://([^/]+)/?.*#\1#p' /root/frigate-credentials.txt)
for p in / /api/version /api/config /api/stats; do
printf '%-14s %s\n' "$p" "$(curl -sk -o /dev/null -w '%{http_code}' --connect-to ::127.0.0.1: "https://$H$p")"
done
printf '%-14s %s\n' /nginx-health "$(curl -sk --connect-to ::127.0.0.1: "https://$H/nginx-health")"
curl -s -o /dev/null -w 'http:// %{http_code} -> https\n' --connect-to ::127.0.0.1: "http://$H/"
curl -sk -o /dev/null -w 'unknown Host %{http_code}\n' -H 'Host: evil.example' https://127.0.0.1/ || true
curl -sk -o /dev/null -w 'TLS 1.1 %{http_code}\n' --tls-max 1.1 --connect-to ::127.0.0.1: "https://$H/" || true
sudo frigate-selftest
/ 401
/api/version 401
/api/config 401
/api/stats 401
/nginx-health OK
http:// 301 -> https
unknown Host 000
TLS 1.1 000
round-trip OK https://ec2-3-95-132-108.compute-1.amazonaws.com/: 19 routes 401 anonymously; wrong gate password 401; gate alone cannot read /api/config; wrong admin password refused; admin login cookie Secure+HttpOnly; 0.18.2-cfa08c5; safe_mode False; detector cpu1; 0 cameras
--connect-to ::127.0.0.1: sends each request to this instance while keeping the real host name in the request, exactly as a browser would send it. 000 means the server closed the connection without an answer.
Step 5: Sign in to Frigate
Browse to FRIGATE_URL (https://<public-dns-name>/). The certificate is self-signed, so your browser warns once; accept it for this host, or install your own certificate (Step 9). The browser then asks for a user name and password: enter GATE_USERNAME and GATE_PASSWORD. Frigate's own sign-in page follows: enter FRIGATE_USERNAME (admin) and FRIGATE_PASSWORD.

With no cameras configured yet, Frigate opens on its live view and offers Add Camera:

The gear at the bottom left opens a menu with System metrics, System logs, Settings and Configuration Editor. The configuration editor shows the shipped configuration, with comments explaining each hardening choice (the internal port bound to the container's loopback, the secure session cookie, the login rate limit and the version check turned off):

Settings, then Users, then Users lists the accounts. The image creates one, admin, with the password from the credentials file:

To change the admin password, use Reset Password next to admin on that page; then replace the old password in /root/frigate-credentials.txt once you have stored the new one.
Step 6: Add your first camera
Frigate is configured in /var/lib/frigate/config/config.yml (inside the container, /config/config.yml). You can edit it in the browser (the gear menu, then Configuration Editor, then Save & Restart) or on the instance. A minimal camera, with the low-resolution sub-stream used for detection and the main stream for recording:
cameras:
front_door:
ffmpeg:
inputs:
- path: rtsp://CAMERA_USER:CAMERA_PASSWORD@192.0.2.20:554/stream2
roles:
- detect
- path: rtsp://CAMERA_USER:CAMERA_PASSWORD@192.0.2.20:554/stream1
roles:
- record
detect:
width: 640
height: 360
fps: 5
record:
enabled: true
retain:
days: 7
mode: motion
Replace the addresses, the paths and the camera credentials with your camera's own (the RTSP path differs by manufacturer; your camera's manual or its web interface lists it). To keep camera passwords out of config.yml, Frigate can read them from environment variables named FRIGATE_* (for example {FRIGATE_RTSP_PASSWORD} in the path); add them with a systemd drop-in for frigate.service that passes -e FRIGATE_RTSP_PASSWORD=... to the container. After editing the file on the instance, restart Frigate:
sudo nano /var/lib/frigate/config/config.yml
sudo systemctl restart frigate
Frigate takes 20 to 40 seconds to load its model and come back; nginx answers 502 until it does. Detection on the CPU costs most for high frame rates and resolutions: keep detect on the camera's sub-stream at 5 frames per second or fewer, and record from the main stream. Without a camera, the CPU detector (cpu1) stays idle (Step 7 reads its state from the API).
Step 7: Use the Frigate API
Frigate's HTTP API (/api/...) sits behind both logins. This example signs in with this instance's gate and admin passwords, keeps Frigate's session cookie in a temporary file and reads the version and the detector state. The passwords go to curl through file descriptors, so neither appears on a command line, in the shell history or in a log. Frigate refuses any write (POST, PUT, DELETE) that lacks the X-CSRF-TOKEN header, so the login request sends it:
H=$(sudo sed -nE 's#^FRIGATE_URL=https://([^/]+)/?.*#\1#p' /root/frigate-credentials.txt)
GU=$(sudo sed -n 's/^GATE_USERNAME=//p' /root/frigate-credentials.txt)
GP=$(sudo sed -n 's/^GATE_PASSWORD=//p' /root/frigate-credentials.txt)
FP=$(sudo sed -n 's/^FRIGATE_PASSWORD=//p' /root/frigate-credentials.txt)
JAR=$(mktemp)
curl -sk --connect-to ::127.0.0.1: -K <(printf 'user = "%s:%s"\n' "$GU" "$GP") \
-c "$JAR" -o /dev/null -w 'login %{http_code}\n' \
-H 'Content-Type: application/json' -H 'X-CSRF-TOKEN: 1' \
--data-binary @<(printf '{"user":"admin","password":"%s"}' "$FP") "https://$H/api/login"
curl -sk --connect-to ::127.0.0.1: -K <(printf 'user = "%s:%s"\n' "$GU" "$GP") \
-b "$JAR" -w '\n' "https://$H/api/version"
curl -sk --connect-to ::127.0.0.1: -K <(printf 'user = "%s:%s"\n' "$GU" "$GP") \
-b "$JAR" "https://$H/api/stats" | python3 -I -c '
import json, sys
s = json.load(sys.stdin)
print("detectors:", ", ".join(sorted(s["detectors"])))
print("cameras:", len(s.get("cameras", {})))
print("frigate version:", s["service"]["version"])'
rm -f "$JAR"; unset GU GP FP
login 200
0.18.2-cfa08c5
detectors: cpu1
cameras: 0
frigate version: 0.18.2-cfa08c5
From your workstation, drop --connect-to ::127.0.0.1: and use your instance's name; keep -k until you install a trusted certificate (Step 9). For automation, Frigate also accepts its session token as Authorization: Bearer <token>; the gate passes a Bearer header through to Frigate but never your gate password.
Step 8: Recording storage
Recordings, clips and snapshots go to /var/lib/frigate/media (inside the container, /media/frigate). On the 24 GiB root volume that is room for testing only. For continuous recording, attach an EBS volume (the CLI example in Step 1 adds one as /dev/sdf) and mount it there before you add cameras:
lsblk
sudo systemctl stop frigate
sudo mkfs.ext4 -L frigate-media /dev/nvme1n1
echo 'LABEL=frigate-media /var/lib/frigate/media ext4 defaults,nofail 0 2' | sudo tee -a /etc/fstab
sudo mount /var/lib/frigate/media
sudo systemctl start frigate
Check the device name with lsblk first (Nitro instances name EBS volumes /dev/nvme1n1 and so on). A 1080p camera recording continuously at a typical 2 to 4 Mbit/s uses roughly 20 to 40 GiB a day; set record.retain.days to what your volume holds, and Frigate deletes the oldest recordings first when the disk runs low.
Step 9: Serve Frigate under your own DNS name
Point a DNS record (for example nvr.example.com) at the instance's public or Elastic IP address, then tell the image to serve that name. It creates a new self-signed certificate for the name, pins nginx and the cross-site guard to it, updates FRIGATE_URL in the credentials file and keeps the name across reboots:
sudo frigate-set-hostname nvr.example.com
For a trusted certificate, obtain one for that name (for example with certbot's certonly mode using its own temporary web server, or from your organisation's CA), copy the certificate chain and key over /etc/ssl/frigate/frigate.crt and /etc/ssl/frigate/frigate.key (key mode 0600, root), and run sudo systemctl reload nginx.
Step 10: Check the security controls
The DOCKER-USER rules drop the metadata addresses (IPv4 and IPv6), the Frigate container gets no answer from instance metadata, Frigate's port 5000 answers only on the container's own loopback (the first 200 is that positive control) and nothing on the host reaches it, the services carry IPAddressDeny, and SSH runs with the cloudimg pins:
sudo iptables -S DOCKER-USER
sudo ip6tables -S DOCKER-USER
sudo docker exec frigate curl -s -m 5 -o /dev/null -w 'container -> IMDS token: %{http_code}\n' -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 60' http://169.254.169.254/latest/api/token || true
sudo docker exec frigate curl -s -m 5 -o /dev/null -w 'container loopback :5000: %{http_code}\n' http://127.0.0.1:5000/api/version
curl -s -m 5 -o /dev/null -w 'host 127.0.0.1:5000: %{http_code}\n' http://127.0.0.1:5000/api/version || true
CIP=$(sudo docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' frigate)
curl -s -m 5 -o /dev/null -w 'host -> container :5000: %{http_code}\n' "http://$CIP:5000/api/version" || true
systemctl show -p IPAddressDeny docker frigate nginx
sudo sshd -T | grep -E '^(passwordauthentication|permitrootlogin|kbdinteractiveauthentication|maxauthtries|x11forwarding) '
-N DOCKER-USER
-A DOCKER-USER -o br-frigate -m conntrack --ctstate NEW -j DROP
-A DOCKER-USER -d 168.63.129.16/32 -j DROP
-A DOCKER-USER -d 169.254.169.254/32 -j DROP
-N DOCKER-USER
-A DOCKER-USER -o br-frigate -j DROP
-A DOCKER-USER -d fd00:ec2::254/128 -j DROP
container -> IMDS token: 000
container loopback :5000: 200
host 127.0.0.1:5000: 000
host -> container :5000: 000
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
maxauthtries 6
permitrootlogin no
passwordauthentication no
kbdinteractiveauthentication no
x11forwarding no
Maintenance
- Change the gate password: run
sudo htpasswd -B /etc/nginx/frigate.htpasswd frigate(it prompts for the new password) and thensudo systemctl reload nginx; replace the old password in the credentials file once you have stored the new one. - Operating system updates: unattended security upgrades are on. Reboot when
/var/run/reboot-requiredexists; the metadata block, Frigate and nginx start again by themselves. - Frigate updates: the image runs Frigate from the pinned image and never pulls one, and Frigate's own version check is off. To move to a newer release, launch the newer version of this image when the listing offers it and copy
/var/lib/frigateacross with Frigate stopped. Do not rundocker image prune -aor remove the image:frigate.servicestarts the pinned image only. - Backups: Frigate keeps its configuration, database and model cache in
/var/lib/frigate/configand its recordings in/var/lib/frigate/media. Take EBS snapshots, or stop Frigate (sudo systemctl stop frigate) and copy/var/lib/frigate/config. - Hardware acceleration: general-purpose EC2 instances have no GPU, so decoding and detection run on the CPU. On a GPU instance, install the NVIDIA driver and the NVIDIA container toolkit, then follow Frigate's documentation for the TensorRT detector and add the GPU to the container with a systemd drop-in for
frigate.service. - Logs:
sudo docker logs frigateand System logs in the gear menu for Frigate;/var/log/nginx/frigate-access.logandfrigate-error.logfor web requests;sudo journalctl -u frigate-firstbootfor first boot.
Troubleshooting
- The page does not load right after launch: first boot is still running.
sudo journalctl -u frigate-firstboot -fshows its progress. - Browser shows "connection closed": nginx answers only the configured name and the instance's own IP addresses, and closes the connection for any other name. Use
FRIGATE_URL, or runsudo frigate-set-hostname <name>for a new name. Use the name rather than an IP address in the browser too: the cross-site guard accepts changes (saving the configuration, signing in) only from the configured name, so they are refused (403) fromhttps://<ip-address>/. - The browser keeps asking for a password: the first prompt is the gate: use
GATE_USERNAMEandGATE_PASSWORD. Frigate's own login page comes after it and takesFRIGATE_USERNAMEandFRIGATE_PASSWORD. - "Too many requests" (429) on the login page: Frigate limits sign-in attempts (one a second, five a minute, twenty an hour); wait a minute and try again with the password from the credentials file.
- 502 Bad Gateway: Frigate is restarting (for example after a configuration change) or refused its configuration.
sudo docker logs --tail 50 frigateshows why; with an invalid configuration Frigate starts in safe mode with the configuration editor open so you can fix it. - A camera shows no picture: check the RTSP address from the instance with the FFmpeg inside the container:
sudo docker exec frigate /usr/lib/ffmpeg/7.0/bin/ffprobe -rtsp_transport tcp '<rtsp-address>'. Most often the instance cannot reach the camera's network or the path or credentials are wrong. - The end-to-end check:
sudo frigate-selftestsigns in through the gate and Frigate's login and printsround-trip OKwhen everything works.
Licensing
Frigate is licensed under the MIT licence; the text is on the instance in /usr/share/doc/frigate/LICENSE. The Frigate image contains FFmpeg builds licensed under the GNU General Public License version 3 (their build options are in /usr/share/frigate/ffmpeg-configuration.txt), and its other components are listed with their licences in /usr/share/frigate/THIRD-PARTY-NOTICES and /usr/share/frigate/dependency-licences.txt. Frigate and Frigate NVR are trademarks of Frigate, Inc.; cloudimg is not affiliated with, sponsored by or endorsed by Frigate, Inc., and the names identify the software this image contains.
Support
cloudimg provides 24/7 technical support for this image by email and live chat: deployment, first login, HTTPS and DNS setup, camera configuration, storage, upgrades and troubleshooting. Contact support@cloudimg.co.uk.