Frigate is an open source network video recorder (NVR) built around real time AI object detection for IP cameras. This guide covers a production deployment of the current stable release (0.17) on a RamNode KVM VPS running Ubuntu 24.04 LTS, with host hardening, a reverse proxy with automatic TLS, and a backup plan.
Read this before you start: VPS suitability
Frigate is designed to run on bare metal with low overhead access to a Google Coral, Hailo, or a GPU for accelerated inference. A standard RamNode VPS has none of that hardware, so object detection runs on the CPU. That is workable, but it sets two hard constraints on the design:
- Keep the camera count and detection resolution low. CPU inference is several times slower than a Coral. Plan for one to three cameras detecting at 1280x720 or lower. Recording can still be full resolution; only the detect stream needs to be small.
- Do not expose your cameras to the public internet. Cameras live on a private LAN. The correct pattern is a WireGuard tunnel from the VPS back to the camera network so Frigate pulls RTSP over an encrypted link. Port forwarding camera RTSP straight to the internet is a serious security mistake and is out of scope here.
If you need heavy multi camera AI, Frigate belongs on a box with a Coral or GPU, not a budget VPS. For a handful of cameras with motion and basic person/car detection, a VPS deployment is reasonable.
Recommended RamNode sizing
| Workload | vCPU | RAM | Disk |
|---|---|---|---|
| 1 to 2 cameras, CPU detection | 4 | 4 GB | 80 GB+ |
| 3 cameras, CPU detection | 4 to 6 | 8 GB | 160 GB+ |
Recordings dominate disk use. Budget roughly 5 to 15 GB per day per 1080p camera on motion only recording, and around 40 GB per day for continuous recording. Tune retention to your disk.
Prerequisites
- A RamNode KVM VPS with Ubuntu 24.04 LTS installed.
- A domain or subdomain (for example
frigate.example.com) with an A record pointing at the VPS public IP. TLS certificates depend on this. - SSH access as root or a sudo user.
- A reachable camera network. This guide assumes cameras are reachable over a WireGuard tunnel or private network at addresses like
10.10.0.x.
Step 1: Host hardening
Log in and create a non-root sudo user.
adduser deploy
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deployFrom your workstation, confirm you can log in as deploy with your SSH key, then lock down SSH. Edit /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication noReload SSH:
sudo systemctl reload sshInstall and configure the firewall. Frigate sits behind a reverse proxy, so only SSH, HTTP, and HTTPS are public.
sudo apt update && sudo apt -y install ufw fail2ban
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableEnable unattended security updates:
sudo apt -y install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgradesfail2ban ships with a sensible SSH jail enabled by default once installed.
Step 2: Install Docker
sudo apt -y install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker deployLog out and back in so the group change takes effect.
Step 3: Lay out the Frigate directories
sudo mkdir -p /var/lib/frigate/config
sudo mkdir -p /var/lib/frigate/storage
sudo chown -R deploy:deploy /var/lib/frigateStep 4: Write the Frigate config
Create /var/lib/frigate/config/config.yml. This is a minimal CPU detection config for two cameras. Adjust the camera names, RTSP paths, and the detect resolution to match your hardware.
mqtt:
enabled: false
detectors:
cpu1:
type: cpu
num_threads: 3
ffmpeg:
# input_args presets reduce CPU on the decode side
output_args:
record: preset-record-generic-audio-copy
record:
enabled: true
retain:
days: 7
mode: motion
alerts:
retain:
days: 14
detections:
retain:
days: 14
snapshots:
enabled: true
retain:
default: 14
objects:
track:
- person
- car
cameras:
front_door:
enabled: true
ffmpeg:
inputs:
- path: rtsp://user:password@10.10.0.21:554/stream1
roles:
- record
- path: rtsp://user:password@10.10.0.21:554/stream2
roles:
- detect
detect:
width: 1280
height: 720
fps: 5
driveway:
enabled: true
ffmpeg:
inputs:
- path: rtsp://user:password@10.10.0.22:554/stream1
roles:
- record
- path: rtsp://user:password@10.10.0.22:554/stream2
roles:
- detect
detect:
width: 1280
height: 720
fps: 5A few notes:
- Most cameras expose a high resolution main stream and a low resolution sub stream. Use the main stream for
recordand the sub stream fordetect. This is the single most important tuning step on a CPU only host. - Keeping
fpslow (5 is plenty for detection) sharply reduces CPU load. - The built in
cpudetector works out of the box but is slow. Once the deployment is stable, consider switching to the OpenVINO detector running on CPU, which is more efficient. See the Frigate object detector documentation for the OpenVINO model configuration.
Step 5: Create the Compose file
Create /var/lib/frigate/docker-compose.yml. Note that the hardware device passthrough lines from the upstream sample are deliberately omitted, since a VPS has no Coral or GPU. Port 5000 (the unauthenticated internal API) is intentionally not published; only the authenticated UI on 8971 is exposed to the reverse proxy.
services:
frigate:
container_name: frigate
image: ghcr.io/blakeblackshear/frigate:stable
restart: unless-stopped
stop_grace_period: 30s
shm_size: "256mb" # raise this if you add cameras, see the shm formula below
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/lib/frigate/config:/config
- /var/lib/frigate/storage:/media/frigate
- type: tmpfs
target: /tmp/cache
tmpfs:
size: 1000000000
ports:
- "127.0.0.1:8971:8971" # authenticated UI, bound to localhost for the reverse proxy
environment:
FRIGATE_RTSP_PASSWORD: "changeme"Binding port 8971 to 127.0.0.1 means it is only reachable by the reverse proxy on the same host, never directly from the internet.
Sizing shm
The default shm of 128 MB suits two cameras at 720p. For more or higher resolution cameras, compute the minimum with the formula from the Frigate docs:
python3 -c 'print("{:.2f}MB".format(((1280 * 720 * 1.5 * 20 + 270480) / 1048576) * 2 + 40))'Set shm_size above the result, then recreate the container with docker compose down && docker compose up -d (a plain restart does not apply a new shm size).
Step 6: Start Frigate
cd /var/lib/frigate
docker compose up -d
docker compose logs -fWatch the logs until cameras connect and detection starts. The version is shown at the top of the System Metrics page once the UI is up.
Step 7: Reverse proxy with automatic TLS
Caddy gives you HTTPS with automatic Let's Encrypt certificates and a tiny config. Install it:
sudo apt -y install debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt -y install caddyReplace /etc/caddy/Caddyfile with:
frigate.example.com {
reverse_proxy 127.0.0.1:8971
}Reload Caddy:
sudo systemctl reload caddyVisit https://frigate.example.com. Frigate 0.17 presents its own login page on port 8971. On first launch it generates an admin password and prints it in the logs:
docker compose logs frigate | grep -i passwordLog in and change the password under the user settings.
Step 8: Backups
Two things matter for recovery: the config and the SQLite database. The recordings themselves are large and usually treated as expendable, but back them up too if your retention policy requires it.
Create /usr/local/bin/frigate-backup.sh:
#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%Y%m%d-%H%M%S)
DEST="/var/backups/frigate"
mkdir -p "$DEST"
# Config and SQLite database live under /config
tar czf "$DEST/frigate-config-$STAMP.tar.gz" -C /var/lib/frigate config
# Keep the last 14 archives
ls -1t "$DEST"/frigate-config-*.tar.gz | tail -n +15 | xargs -r rmMake it executable and schedule it nightly:
sudo chmod +x /usr/local/bin/frigate-backup.sh
echo "30 3 * * * deploy /usr/local/bin/frigate-backup.sh" | sudo tee /etc/cron.d/frigate-backupCopy the archives off the VPS regularly (rsync over SSH, or push to object storage). A backup that lives only on the same disk is not a backup.
Step 9: Updating
The stable tag always points to the latest stable release after a pull.
cd /var/lib/frigate
docker compose pull
docker compose up -dTo pin a specific version instead, set the image tag explicitly, for example ghcr.io/blakeblackshear/frigate:0.17.0, and run docker compose up -d. Take a config backup before any upgrade so you can roll back by pinning the previous tag.
Step 10: A note on notifications
RamNode restricts outbound SMTP (port 25) on VPS plans by default to prevent abuse, and unblocking requires a support request. Do not rely on email notifications. Frigate supports MQTT and webhook based notifications and integrates cleanly with Home Assistant, which is a better fit for alerting anyway. If you want push notifications, configure them through the Frigate web app (PWA) or Home Assistant rather than email.
Troubleshooting
- Container exits with "Bus error": shm is too small. Recompute with the formula above, raise
shm_size, and recreate the container. - Cameras will not connect: confirm the VPS can reach the camera over the tunnel first with
ffprobe rtsp://...from inside the container (docker exec -it frigate ffprobe <url>). If that fails, it is a network or credentials problem, not a Frigate problem. - CPU pinned at 100 percent: you are detecting on a high resolution stream or at a high fps. Switch detection to a sub stream and lower fps to 5.
- UI not loading through Caddy: confirm port 8971 is published and bound to localhost, and that the DNS A record resolves to the VPS before Caddy can issue a certificate.
