Why did I do this
I got tired of changing Blink camera batteries and of the thought that someone in the cloud might have recordings of my family's every move.
That's it. That's the origin story. Every now and then I'd get the notification, trudge outside, swap batteries, and wonder why I was paying a cloud subscription for the privilege of watching grainy clips of my own front yard on someone else's server. So I decided to fix it. Overkill style.
I'm a homelabber. I build things to learn things. Half the reason any project gets a green light in my house is because there's something new I get to figure out along the way. This project had a lot of those moments. It was my first time using a PoE switch. It had been a while since I'd fished cable through an attic or soffits, and even longer since I'd worked with PVC conduit outside, cementing it and pulling cable through. Good excuse to dust off those skills. And then Frigate itself was completely new territory for me.
The end goal is straightforward: three wired cameras to start, seven days of continuous recordings, and AI detection for people, cars, and animals. Eventually, when the whole property is covered, I want to figure out who keeps leaving dog poop on my lawn. I haven't worked out what happens after I identify them. Confrontation? A politely worded letter? A wall of shame? That's a problem for future me.
The surveillance software is Frigate, an open source NVR with built-in AI object detection. I'd never run it before, but it looked like one of those cool open source projects that punches way above its weight. It is.
The Inspiration
I watched NetworkChuck's video where he set up Frigate with Reolink cameras and a Google Coral TPU on a Raspberry Pi, and later scaled it to a gaming PC. Great video. But I had different hardware laying around and different priorities, so my setup diverged pretty quickly:
| NetworkChuck's Setup | My Setup |
|---|---|
| Raspberry Pi / Gaming PC | M1 Mac Mini (collecting dust) |
| Google Coral TPU ($100) | M1 Neural Engine via Apple Silicon detector (built-in, free) |
| Reolink cameras (wireless) | Amcrest cameras (wired PoE) |
| Local storage / external drive | TrueNAS NAS via NFS (22TB) |
Going wired was deliberate. Chuck's biggest headache was 10 wireless cameras destroying his WiFi network with RTSP retransmits. His TX retry rate hit 29%. He had to add a third access point, stagger camera reboots, and switch to constant bit rate just to keep things from falling apart. None of that is a problem with Ethernet.
The M1 Mac Mini was just sitting in a drawer. It has a Neural Engine that can handle AI inference without buying a separate accelerator. And the TrueNAS NAS was already running with 22TB of storage, so pointing Frigate at it seemed obvious. It was not obvious. More on that shortly.
Googling one of these? You're in the right place.
docker desktop macOS NFS volume mount NAS "permission denied"/docker desktop mac "host_mnt" "operation not permitted"/frigate NVR macOS Docker Desktop NAS storage/frigate docker compose NFS volume TrueNAS/TrueNAS SCALE NFS "allow non-root mount" Docker/frigate NVR Mac mini Apple Silicon M1 setup/frigate amcrest camera RTSP macOS docker/frigate config KeyError ffmpeg disabled camera
The Plan
Get Frigate running on Docker Desktop macOS(done)Configure NAS storage via NFS(done, eventually)Add Amcrest cameras one at a time with RTSP(done, 2 cameras)Enable AI object detection (person, car, dog, cat)(done)Enable recording, continuous + event-based(done)Set up go2rtc for efficient stream handling(done)- Configure facial recognition and semantic search
- Connect to Home Assistant (optional)
- Identify the lawn pooper (pending)
The NAS Storage Saga
This was supposed to be the easy part. I have a TrueNAS box with 22TB of storage sitting on my network. Frigate needs somewhere to store recordings. Just point one at the other, right?
I spent more time getting NAS storage working than any other part of this project. If you're running Frigate on Linux, you mount your NFS share and move on with your life. On Docker Desktop for macOS, nothing works the way you expect. What follows is every wall I hit and how I got past it.
Why NFS Over SMB?
Both NFS and SMB can share files over a network, but NFS is the better choice for Frigate:
- Performance: NFS has lower protocol overhead. Frigate is I/O heavy, cameras can generate 3GB/hr per stream. SMB's chattier protocol adds latency on every file operation, which compounds across multiple cameras writing continuously.
- Permissions: Both the NAS (Linux-based TrueNAS) and the Docker container (Linux) speak POSIX natively. NFS passes POSIX permissions cleanly. SMB has to translate between Windows-style ACLs and POSIX, which causes permission headaches. Especially when Frigate needs to create directories, write recordings, and manage retention.
- Reliability for continuous writes: NFS handles the constant stream of small file writes (recording segments) more gracefully. SMB connections are more prone to stalling or dropping under sustained write loads, which can cause Frigate to error out or fall behind on recordings.
- Simplicity: No username/password authentication to configure. NFS uses host-based access control, which is simpler for a trusted LAN environment.
Three Approaches (Two of Them Don't Work)
When trying to connect Frigate on Docker Desktop (macOS) to NAS storage, there are three approaches. I tried all of them.
Option A: Mount NFS on macOS, bind-mount into Docker
Mount the NFS share on macOS first (sudo mount -t nfs ...), then reference that path in docker-compose volumes. This is the standard approach on Linux.
Does NOT work on Docker Desktop for macOS. Docker Desktop's Linux VM translates host paths to /host_mnt/... and cannot traverse NFS mount points on the host. You'll get operation not permitted errors.
Option B: Docker NFS volume driver (what worked)
Let Docker's Linux VM mount NFS directly from the NAS, bypassing macOS entirely. Define an NFS volume in docker-compose with driver: local and driver_opts. The VM talks directly to the NAS.
This is what works. Requires enabling "Allow non-root mount" on TrueNAS so Docker's VM can connect from non-privileged ports. THIS IS THE MOST IMPORTANT SETTING.
Option C: SMB/CIFS volume
Similar to Option B but using SMB instead of NFS. Docker supports CIFS volumes with type: cifs in driver_opts. Requires username/password configuration and has the performance drawbacks mentioned above. A viable fallback if NFS isn't an option.
Known Issues and Gotchas
/host_mntpath translation bug: This is a known Docker Desktop for Mac issue. Docker Desktop's VM prefixes host paths with/host_mnt/and cannot handle NFS/network mount points on the host. Switching the file sharing backend from VirtioFS to osxfs has been reported as a workaround but is unreliable.- NAS offline at boot: If the NFS share isn't available when Frigate starts, Frigate will create a local
/media/frigatedirectory inside the container and fill up local storage. Usingsoftin NFS mount options helps Frigate fail gracefully rather than hang. Therestart: unless-stoppedpolicy will keep retrying. - No USB Coral TPU passthrough on macOS: Docker Desktop on macOS does not support USB device passthrough, so you cannot use a Coral TPU. But here's the thing: you don't need one. M1/M2 CPUs achieve 7-11ms inference speeds on their own, which is comparable to a Coral. And as of Frigate 0.17, there's an official Apple Silicon detector that runs natively on macOS using CoreML and the Neural Engine. It runs as a separate app alongside Docker and connects via ZMQ. Users report ~11ms inference with multiple 4K cameras at just 10W power consumption. This is on the roadmap for this project.
- Storage consumption: Frigate is storage-hungry. A single 4K camera stream can use ~3GB/hr. Plan your NAS storage accordingly and configure Frigate's retention settings in
config.ymlto auto-delete old recordings.
The Challenges (a.k.a. The Interesting Part)
What follows is every problem I ran into, in order. If you're here from Google, you probably hit one of these exact errors. Welcome. I feel your pain.
Challenge 1: Docker Desktop Cannot Bind-Mount NFS Paths from macOS
My first attempt was the obvious one. Mount the NFS share on macOS, then tell Docker to use that path. Simple. Except:
Error response from daemon: error while creating mount source path '/host_mnt/Volumes/frigate':
mkdir /host_mnt/Volumes/frigate: operation not permitted
Why: Docker Desktop on macOS runs inside a Linux VM. Host paths get translated to /host_mnt/... inside the VM. NFS mount points on macOS don't pass through this translation layer and the VM can't see or create them.
Solution: Don't mount NFS on macOS at all. Let Docker's Linux VM mount NFS directly from the NAS using Docker's built-in NFS volume driver:
volumes:
frigate-media:
driver: local
driver_opts:
type: nfs
o: addr=192.168.1.7,rw,nfsvers=3,nolock,soft
device: ":/mnt/Storage/frigate"
Challenge 2: Docker NFS Volume Mount, Permission Denied
Great, so Docker needs to mount NFS directly. I set that up, ran docker compose up, and:
failed to mount local volume: mount :/mnt/Storage/frigate:...: permission denied
Why: Docker Desktop's Linux VM connects to NFS from a non-privileged port (above 1024). By default, NFS servers reject connections from non-privileged ports, a security holdover from the Unix days where only root could use ports below 1024.
Solution: On TrueNAS (tested on v25.04.2.4), enable "Allow non-root mount":
- Go to System > Services > NFS
- Click the edit/config icon
- Check "Allow non-root mount"
- Save and restart the NFS service
Challenge 3: NFS Share Write Permissions
At this point I was getting good at reading the words "Permission denied." This time the mount succeeded, but writing to it didn't.
Why: The NFS share's default permissions don't map the connecting user to a user with write access on the NAS filesystem.
Solution: On the TrueNAS NFS share settings for the Frigate dataset:
- Set Mapall User to
root - Set Mapall Group to
wheel
This maps all connecting users (including Docker) to root on the NAS, granting full read/write. Acceptable for a dedicated Frigate media folder.
Challenge 4: Frigate Config, Disabled Camera Crash
NAS finally working. Time to actually run Frigate. It crashed immediately.
KeyError: 'ffmpeg'
Why: Even disabled cameras in config.yml require an ffmpeg.inputs section. Frigate validates the config structure before checking if the camera is enabled.
Solution: Always include the ffmpeg block, even for placeholder/disabled cameras:
cameras:
dummy_camera:
enabled: false
ffmpeg:
inputs:
- path: rtsp://127.0.0.1:8554/dummy
roles:
- detect
Challenge 5: RTSP Authentication Fails Because of a Question Mark
This one was maddening. I could see the camera feed in the Amcrest web UI. I could pull frames with ffmpeg manually. But Frigate kept saying 401 Unauthorized or wrong user/pass. The password was correct. I triple-checked.
Why: RTSP URLs follow standard URL format (rtsp://user:pass@host/path). Characters like ?, @, :, #, & in the password break URL parsing. Both ffmpeg and go2rtc interpret ? as the start of a query string, not part of the password. URL-encoding (%3F) gets double-encoded depending on whether the value is quoted in YAML.
Solution: Change your camera password to avoid special URL characters. Stick to letters and numbers. This is easier than fighting encoding issues across multiple layers (YAML → go2rtc → ffmpeg → RTSP).
Avoid in camera passwords: ? @ : # & % /
Challenge 6: Using go2rtc as a Stream Proxy
This one isn't really a problem. It's more of a "you should do this and here's why."
Without go2rtc, every viewer of your camera (Frigate detection, Frigate recording, Home Assistant, your phone) opens a separate RTSP connection directly to the camera. That's four connections to one camera. Multiply by ten cameras and your network is doing unnecessary work. Some cameras don't even handle multiple concurrent RTSP connections well.
Frigate already includes go2rtc, a lightweight stream proxy. It connects to the camera once and re-serves the stream locally. All consumers read from rtsp://127.0.0.1:8554/stream_name instead of hitting the camera directly. This is exactly what NetworkChuck did in his video to fix his WiFi issues, and it's even more important if you plan to add Home Assistant later.
Config:
go2rtc:
streams:
front_door_sub:
- rtsp://admin:[email protected]:554/cam/realmonitor?channel=1&subtype=1
cameras:
front_door:
ffmpeg:
inputs:
- path: rtsp://127.0.0.1:8554/front_door_sub
roles:
- detect
This is especially important if you plan to add Home Assistant or view streams from multiple devices.
Adding Amcrest Cameras
With NAS storage sorted and Frigate running, this is where it actually starts feeling like a surveillance system. I unboxed one camera at a time, got it fully working end-to-end before moving to the next. Resist the urge to plug them all in at once. Trust me.
Initial Camera Setup
- Plug camera into PoE switch and it powers on and gets a DHCP address
- Find the camera's IP and check your router's DHCP lease list, or broadcast ping and check ARP:
ping -c 1 192.168.1.255 && arp -a - Verify it's the camera by checking Amcrest's default ports:
nc -z -w 3 192.168.1.X 80 && echo "HTTP open" # Web UI nc -z -w 3 192.168.1.X 554 && echo "RTSP open" # Video stream nc -z -w 3 192.168.1.X 37777 && echo "API open" # Amcrest API - Open the web UI at
http://CAMERA_IPand set a password (avoid special URL characters like?@:#&) - Assign a static IP on your router (Optional)
- Disable all on-camera smart features (Setup > Event):
- Motion Detection → uncheck Enable
- Video Tamper → uncheck Enable, Record, Snapshot
- Audio Detection → uncheck all
- Any AI/IVS/Smart Plan → disable
The camera should be a dumb video pipe, Frigate handles all the AI.
Amcrest RTSP URL Format
Main stream (high-res, for recording):
rtsp://admin:PASSWORD@CAMERA_IP:554/cam/realmonitor?channel=1&subtype=0
Substream (low-res, for AI detection):
rtsp://admin:PASSWORD@CAMERA_IP:554/cam/realmonitor?channel=1&subtype=1
Dual Stream Architecture
Each camera sends two RTSP streams to Frigate via go2rtc:
- Main stream (
subtype=0), full resolution (e.g. 2960x1668 or 3840x2160), used for recording. This is the high-quality footage you review. - Substream (
subtype=1), lower resolution (e.g. 704x480), used for AI detection. The AI doesn't need 4K to spot a person, lower res means less CPU/GPU work.
go2rtc proxies both streams locally so Frigate (and any other viewer) connects to 127.0.0.1:8554 instead of hitting the camera directly.
Recording Strategy
Here's where the 22TB NAS earns its keep. I wanted both: continuous recording as a safety net (in case AI misses something), plus longer retention for events that actually matter. Frigate 0.17 supports exactly this with layered retention:
record:
enabled: true
continuous:
days: 7 # Keep ALL footage for 7 days, then auto-delete
alerts:
retain:
days: 30 # Person/car detections kept 30 days
detections:
retain:
days: 14 # Dog/cat detections kept 14 days
- Continuous = everything, 24/7. Your safety net even if AI misses something, you have 7 days to go back and find it.
- Alerts = high-priority objects (person, car by default). Kept longer because these matter most.
- Detections = other tracked objects (dog, cat). Kept shorter but still longer than continuous.
Storage Estimates
Updated March 18, 2026 with real usage data after running three cameras for over a week.
These are real numbers from my setup, not theoretical calculations. After running three cameras for over a week, here's what the NAS is eating per stream:
| Stream | Resolution | Bitrate | Per Hour | Per Day | 7 Days |
|---|---|---|---|---|---|
| 4K main | 3840x2160 | ~15 Mbps | ~6.7 GB | ~64 GB | ~450 GB |
| 5MP main | 2960x1668 | ~8 Mbps | ~3.6 GB | ~33 GB | ~230 GB |
With all three cameras recording continuously, actual total usage is ~120 GB/day (~40 GB/day per camera). After a full week of 7-day retention, storage settled at ~650 GB on the 22TB NAS. With 22TB available, one could comfortably run 15+ cameras at 7-day continuous retention before storage becomes a concern. This is why I wanted NAS storage instead of a USB drive strapped to the Mac Mini.
Final Working Setup
docker-compose.yml
services:
frigate:
container_name: frigate
image: ghcr.io/blakeblackshear/frigate:stable-standard-arm64
restart: unless-stopped
stop_grace_period: 30s
shm_size: "512mb"
ports:
- "8971:8971"
- "8554:8554"
- "8555:8555/tcp"
- "8555:8555/udp"
volumes:
- ./config:/config
- frigate-media:/media/frigate
- type: tmpfs
target: /tmp/cache
tmpfs:
size: 1000000000
privileged: true
volumes:
frigate-media:
driver: local
driver_opts:
type: nfs
o: addr=192.168.1.7,rw,nfsvers=3,nolock,soft
device: ":/mnt/Storage/frigate"
config.yml
mqtt:
enabled: false
objects:
track:
- person
- dog
- cat
- car
record:
enabled: true
continuous:
days: 7
alerts:
retain:
days: 30
detections:
retain:
days: 14
go2rtc:
streams:
tree_camera_1_main:
- rtsp://admin:[email protected]:554/cam/realmonitor?channel=1&subtype=0
tree_camera_1_sub:
- rtsp://admin:[email protected]:554/cam/realmonitor?channel=1&subtype=1
garage_camera_main:
- rtsp://admin:[email protected]:554/cam/realmonitor?channel=1&subtype=0
garage_camera_sub:
- rtsp://admin:[email protected]:554/cam/realmonitor?channel=1&subtype=1
cameras:
garage_camera:
enabled: true
ffmpeg:
inputs:
- path: rtsp://127.0.0.1:8554/garage_camera_main
roles:
- record
- path: rtsp://127.0.0.1:8554/garage_camera_sub
roles:
- detect
detect:
enabled: true
tree_camera_1:
enabled: true
ffmpeg:
inputs:
- path: rtsp://127.0.0.1:8554/tree_camera_1_main
roles:
- record
- path: rtsp://127.0.0.1:8554/tree_camera_1_sub
roles:
- detect
detect:
enabled: true
version: 0.17-0
TrueNAS NFS Configuration Summary
- NFS Share path:
/mnt/Storage/frigate - NFS Service: "Allow non-root mount" enabled
- Share Mapall User:
root - Share Mapall Group:
wheel - TrueNAS version: 25.04.2.4
Verification
# Start Frigate
docker compose up -d
# Verify NFS mount inside container (should show NAS storage)
docker exec frigate df -h /media/frigate
# :/mnt/Storage/frigate 22T 0 22T 0% /media/frigate
# Check logs
docker logs frigate --tail 20
# Check storage usage
docker exec frigate du -sh /media/frigate/recordings/
Frigate web UI available at http://localhost:8971.
What's Next
The system is running. Two cameras are up, recording continuously to the NAS, detecting people and cars with AI. It works. But there's more to do:
- Facial recognition so I can tell the difference between "person I know" and "person I don't know"
Third camera to cover the front yard (the poop zone)✅ Done- Home Assistant integration so my wife can check cameras without logging into Frigate directly
M1 Neural Engine detector to replace CPU detection, which works but is slower than it needs to be.✅ Done - read about it here
Was this overkill for replacing Blink cameras? Absolutely. But I got to brush up on fishing cable through an attic and cementing PVC conduit, while picking up NFS volume configuration in Docker, RTSP authentication debugging across three layers of URL encoding, and setting up an AI surveillance system that doesn't phone home to anyone.
The Blink cameras are in a drawer. Good riddance.
What It Actually Looks Like
After all the YAML wrestling and permission-denied debugging, it's nice to have something physical to point at. Here's the real install.
The Cameras
First camera went up on a tree in the front yard, high enough that nobody can mess with it but low enough to actually see things. Cable runs down the trunk inside conduit so it doesn't look like a science fair project from the street.

The garage camera tucks into the soffit corner, watching the side yard and driveway. White housing on white trim, you forget it's there until a detection alert lights up your phone.

The Conduit
The least softwary part of the whole project. PVC conduit, a tube of adhesive, some fishing tape, and a willingness to climb a ladder. Honestly the most satisfying few hours of the build.

The PoE Switch
First time using a PoE switch and I'm a convert. One cable to each camera, no wall warts, no power injectors strapped to the wall. The TP-Link slots into the rack with the rest of the gear and quietly does its job.

The M1 mini mac (and Its New Roommate m4 max pro)

The Frigate workhorse is the bottom Mac Mini in this photo. The silver one stacked on top is an M4 that joined the rack later for unrelated reasons. The M1 didn't get demoted when the M4 showed up, it got promoted to "permanent appliance." It just sits there, sips watts, runs Frigate.