How to Sync & Backup Frigate NVR Recordings to Offsite Cloud Storage
This article shows how to synchronize your Frigate NVR recordings to offsite cloud storage for backup and safekeeping in case your onsite NVR is stolen by a burglar.
Toolkit: Backblaze + rclone + Systemd Service Script
I’m using Backblaze as cloud storage because it’s fast and inexpensive. It also offers temporary immutability, preventing burglars who gained access to the source machine from deleting uploaded recordings for a configurable length of time. The backup job is left to rclone, a modern file copy and sync tool for a multitude a cloud storage providers. A custom systemd service controls the backup process via a little script. To ensure low upload latency, it monitors Frigate’s recordings directory for changes and invokes rclone immediately when a new file has been created.
Caveats
No solution is perfect. I’m aware of the following downsides and issues.
Files Being Uploaded Before Frigate Detection Has Completed
New video files are uploaded as soon as Frigate has finished writing to the file, resulting in too many files being uploaded to cloud storage.
When the initial recording is saved to disk, the detection process may not have completed yet. Once Frigate has completed the detection process, files without detections are deleted from disk. Such files should be deleted from cloud storage, too. That is not possible, however, as the immutability (object lock) policies prevent deletion of files.
As a result, you’ll have some “surplus” files in your cloud storage that didn’t pass Frigate’s detection rules, e.g., clips with leaves blowing in the wind.
Prerequisites
I’m assuming that you’ve set up Frigate as described in my earlier article.
Creating a Backblaze Bucket
Create a Bucket
If you haven’t already, create a Backblaze account and enable 2FA. Note that region selection is only possible during account setup. Choose between EU Central
, US West
, and US East
.
Create a new bucket with the following properties:
- Private
- Encryption disabled
- Object lock enabled
- Default retention policy: 60 days
- Lifecycle settings: custom
- File path: empty
- Days till hide: 60
- Days till delete: 10
With the above settings, new files uploaded to the bucket cannot be deleted for 60 days (object lock retention policy). The lifecycle policy ensures that files are hidden after 60 days and deleted ten days later.
Create an Application Key
In the left navigation pane click Application keys > Add a new application key and specify the following settings:
- Key name: same as name of bucket
- Allow access to buckets: select the bucket created earlier
- Type of access: read and write
Copy the key ID, the key name and the actual application key for later use (see below).
Installing & Configuring rclone
rclone Installation
Don’t install rclone via the operating system’s package manager as that would give you a version that is horribly out of date (details). Instead, install rclone directly from the project site using the official automated install script:
sudo curl https://rclone.org/install.sh | sudo bash
rclone Configuration
Invoke rclone’s configuration by running sudo rclone config
. Notice we’re running it as root. This is because the systemd service expects its rclone config in /root/.config/rclone/rclone.conf
.
Specify the following when asked:
New remote
- Name for remote:
b2
- Storage:
b2
- Paste the Backblaze application key ID
- Paste the Backblaze application key
- Hard delete:
false
(not relevant for us) - Edit advanced settings:
no
- Keep this “b2” remote:
yes
- Quit
Backblaze Access Test
Run the following command to list the contents of your bucket. It should not output anything since the bucket is empty.
rclone ls b2:BUCKET_NAME
Triggering rclone Sync/Copy from Custom Systemd Service
In this final section we’re bringing it all together. rclone by itself doesn’t run in the background, continuously monitoring a directory for changes. It needs to be invoked explicitly. We’re doing that from a little script started at boot as a systemd service.
The script watches the Frigate file system for changes where a file was opened for writing and has been closed. When that happens, it runs rclone immediately. While rclone is running, additional files could have been changed. That’s why we also include a timeout of 60 s: we’ll run rclone at least once every 60 s to ensure that no file is missed.
rclone Launch Script
Create the file /usr/local/bin/rclone-launcher.sh
with the following content:
#!/bin/bash
# Directory to watch for changes
DIR_TO_WATCH=/data/docker/frigate/data/recordings
# Remote storage name as configured in rclone
REMOTE_NAME=b2
# Remote bucket name
REMOTE_BUCKET_NAME=
# How long to wait between scheduled retries
TIMEOUT_SEC=60
while true; do
# Run inotifywait with a timeout
# Wait for events: close_write
inotifywait --timeout ${TIMEOUT_SEC} --event close_write --recursive "${DIR_TO_WATCH}"
# Store the return value
status=$?
if [ $status -eq 0 ] || [ $status -eq 2 ]; then
rclone copy "${DIR_TO_WATCH}" ${REMOTE_NAME}:${REMOTE_BUCKET_NAME} --verbose 2>&1
else
echo "inotifywait failed. Waiting before trying again..."
sleep ${TIMEOUT_SEC}
fi
done
Don’t forget to specify the bucket name for the environment variable REMOTE_BUCKET_NAME
.
Make rclone-launcher.sh
executable:
sudo chmod a+x /usr/local/bin/rclone-launcher.sh
Install Inotifywait
Make sure the install inotifywait
package which is not available on Raspberry Pi OS by default:
sudo apt install inotify-tools
Install the rclone Launch Script as Systemd Service
Create the systemd service unit file /etc/systemd/system/rclone-launcher.service
with the following content:
[Unit]
Description=rclone launcher service
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/rclone-launcher.sh
[Install]
WantedBy=multi-user.target
Instruct systemd to reload its configuration so that the new service is recognized:
sudo systemctl daemon-reload
Enable the service (to be started at next boot:
sudo systemctl enable rclone-launcher.service
Manually start the service to verify it’s working:
sudo systemctl start rclone-launcher.service
Check the service’s status:
sudo systemctl status rclone-launcher.service
Maintenance & Operations
Updating Rclone
Use rclone’s self-updating capability to update it to the latest stable version:
sudo rclone selfupdate
Monitoring
The output of our script is written to the systemd journal. The operation of our solution can be monitored with the journalctl
command.
Show the last 100 lines from the log:
journalctl -u rclone-launcher.service -n 100
Monitor the output in real time (“follow” the log):
journalctl -u rclone-launcher.service -f
Typical log output looks as follows:
14:04:01 nvr-int rclone-launcher.sh[5185]: Setting up watches. Beware: since -r was given, this may take a while!
14:04:01 nvr-int rclone-launcher.sh[5185]: Watches established.
14:05:05 nvr-int rclone-launcher.sh[5318]: INFO : There was nothing to transfer
14:05:05 nvr-int rclone-launcher.sh[5318]: INFO :
14:05:05 nvr-int rclone-launcher.sh[5318]: Transferred: 0 B / 0 B, -, 0 B/s, ETA -
14:05:05 nvr-int rclone-launcher.sh[5318]: Checks: 1932 / 1932, 100%
14:05:05 nvr-int rclone-launcher.sh[5318]: Elapsed time: 3.2s
14:05:05 nvr-int rclone-launcher.sh[5337]: Setting up watches. Beware: since -r was given, this may take a while!
14:05:05 nvr-int rclone-launcher.sh[5337]: Watches established.
14:06:08 nvr-int rclone-launcher.sh[5472]: INFO : 2025-05-31/12/door/05.50.mp4: Copied (new)
14:06:10 nvr-int rclone-launcher.sh[5472]: INFO :
14:06:10 nvr-int rclone-launcher.sh[5472]: Transferred: 2.056 MiB / 2.056 MiB, 100%, 7.061 MiB/s, ETA 0s
14:06:10 nvr-int rclone-launcher.sh[5472]: Checks: 1932 / 1932, 100%
14:06:10 nvr-int rclone-launcher.sh[5472]: Transferred: 1 / 1, 100%
14:06:10 nvr-int rclone-launcher.sh[5472]: Elapsed time: 3.2s
14:06:10 nvr-int rclone-launcher.sh[5539]: Setting up watches. Beware: since -r was given, this may take a while!
14:06:10 nvr-int rclone-launcher.sh[5539]: Watches established.
14:06:14 nvr-int rclone-launcher.sh[5539]: /data/docker/frigate/data/recordings/2025-05-31/12/door/ CLOSE_WRITE,CLOSE 06.00.mp4
14:06:17 nvr-int rclone-launcher.sh[5555]: INFO : 2025-05-31/12/door/06.00.mp4: Copied (new)
14:06:18 nvr-int rclone-launcher.sh[5555]: INFO :
14:06:18 nvr-int rclone-launcher.sh[5555]: Transferred: 4.968 MiB / 4.968 MiB, 100%, 5.475 MiB/s, ETA 0s
14:06:18 nvr-int rclone-launcher.sh[5555]: Checks: 1933 / 1933, 100%
14:06:18 nvr-int rclone-launcher.sh[5555]: Transferred: 2 / 2, 100%
14:06:18 nvr-int rclone-launcher.sh[5555]: Elapsed time: 3.2s
14:06:18 nvr-int rclone-launcher.sh[5568]: Setting up watches. Beware: since -r was given, this may take a while!
14:06:18 nvr-int rclone-launcher.sh[5568]: Watches established.