Frigate NVR in a Proxmox LXC with Tailscale and offline TP-Link VIGI cameras
IP cameras are always on, rarely updated and hold a live view into your home or business, so the safest place for one is a network that goes nowhere. My cameras have no external internet access on purpose. The awkward part is that I still want to see the footage when I'm away.
This is the setup: TP-Link VIGI cameras on a wired PoE network, Frigate in a Proxmox LXC container from the community helper scripts, YOLOv9 for detection, Qwen3.8-27B on a GPU machine for event summaries, and Tailscale to get in from outside. Nothing in the chain, including the AI, calls a cloud service.
I went with wired PoE on purpose, no Wi-Fi anywhere on the camera network. One cable gives the camera power and data, there's no radio to attack or interfere with, and the switch is the only thing that needs a UPS to keep every camera up through a power cut. A managed switch can also put the camera ports in their own VLAN and power cycle a hung camera by toggling PoE on the port. VIGI cameras are fine with this, the VIGI C240 datasheet1.0.pdf) for example lists PoE (802.3af/at, class 0, max 6 W) next to a 12V DC option, and a 10/100M port, plenty for a stream capped at 4 Mbps (check the datasheet for your model). One thing to check on the switch is that some switches budget class 0 devices at 15.4 W per port even though the camera draws 6 W at most, so see whether yours budgets by class or by actual draw before counting ports, and keep each cable run under 100 m.
The cameras and the network
Step 1: Prepare the cameras before cutting off their internet. Create the device account in the VIGI app, that username and password are what RTSP uses, give each camera a fixed IP with a DHCP reservation, and set the main stream to H264 with Smart Coding off, which the Frigate docs recommend for VIGI cameras. Point the camera NTP at a local server (your router or the Proxmox host), because with no internet it can't reach a public one and the clock will drift.
According to TP-Link's documentation the streams are on the default RTSP port 554. rtsp://IP-ADDRESS/stream1 is the high quality main stream and rtsp://IP-ADDRESS/stream2 is the low quality sub stream. Frigate works best with both, the sub stream for detection and the main stream for recording.
Step 2: Cut the cameras off. How depends on your network, a dedicated camera VLAN with a firewall rule blocking it from the WAN works, or a network whose router has no default gateway. On a managed PoE switch, put the camera ports in the camera VLAN as untagged access ports and only carry that VLAN to the Proxmox host. Then test it, nothing outside the local network should be reachable from a camera. The Frigate container still needs internet itself (Tailscale and the installer need it) while also reaching the cameras, so either give it a second network interface on the camera VLAN or allow its IP through the firewall to the camera subnet only. The cameras never get an outbound rule. Firmware updates become manual, download from TP-Link and upload through the camera's web UI.
Frigate, YOLOv9 and Qwen3.8
Step 3: Install Frigate with the Proxmox VE Helper-Scripts. Run this in the shell on the Proxmox host, not inside a container
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/frigate.sh)"
Read the script first, it runs as root on your hypervisor. The defaults are 8 vCPU, 6 GB RAM and a 25 GB disk on Debian 12, a privileged container with GPU passthrough switched on, the UI on port 5000, and updating means a new container and transferring your config. 25 GB is tiny for recordings. Frigate writes them under /media/frigate, so bind mount a proper disk or dataset there and set retention to match.
Step 4: Add the cameras. The script creates /config/config.yml with a test camera that loops a sample video. Get in with pct enter CTID from the Proxmox host (the Frigate UI also has a config editor). The pattern from the restream docs is to let the bundled go2rtc pull each stream from the camera once and have Frigate read it locally
go2rtc:
streams:
front_door:
- rtsp://USER:PASSWORD@192.168.50.11:554/stream1
front_door_sub:
- rtsp://USER:PASSWORD@192.168.50.11:554/stream2
cameras:
front_door:
ffmpeg:
inputs:
- path: rtsp://127.0.0.1:8554/front_door
input_args: preset-rtsp-restream
roles:
- record
- path: rtsp://127.0.0.1:8554/front_door_sub
input_args: preset-rtsp-restream
roles:
- detect
Swap in your own IP and credentials (if the password has special characters, go2rtc expects it URL-encoded) and add one stream pair and one cameras entry per camera. The script's default config also sets detect to enabled: false globally, because the test camera is only a video, so turn it on or nothing reaches the detector
detect:
enabled: true
Then restart, and if a camera doesn't show up the logs are in /dev/shm/logs/frigate/current and /dev/shm/logs/go2rtc/current
systemctl restart go2rtc frigate
Step 5: Switch the detector to YOLOv9. The script sets up a small SSDLite MobileNet model, but Frigate can also run YOLOv9 on its ONNX, OpenVINO, TensorRT, RKNN and Hailo-8 detectors. The script installs OpenVINO and onnxruntime, but onnxruntime is the plain CPU build and the docs say CPU only isn't recommended, so on an Intel iGPU (6th generation or newer) use OpenVINO with device: GPU.
Export the model with the Docker build in Frigate's docs, which builds an ONNX from the WongKinYiu/yolov9 repository. It takes MODEL_SIZE (t, s, m, c or e) and IMG_SIZE (320 or 640). The docs suggest starting at 320 with the s size, and the C240 sub stream only goes up to 640x480 anyway. The script doesn't install Docker, so export on any machine that has it, then copy the file in from the Proxmox host
pct exec CTID -- mkdir -p /config/model_cache
pct push CTID yolov9-s-320.onnx /config/model_cache/yolo.onnx
Replace the script's detectors and model sections in /config/config.yml. These values follow the current Frigate docs, so if anything is rejected compare with the model config dropdown in the docs for your version
detectors:
ov:
type: openvino
device: GPU
model:
model_type: yolo-generic
width: 320
height: 320
input_tensor: nchw
input_dtype: float
path: /config/model_cache/yolo.onnx
labelmap_path: /opt/frigate/docker/main/rootfs/labelmap/coco-80.txt
The docs use /labelmap/coco-80.txt for the label map, but that's a path inside their Docker image. This install unpacks Frigate to /opt/frigate and never creates /labelmap, which is why the path above is longer. Check that /dev/dri exists inside the container before using device: GPU, restart Frigate, and look at the detector inference speed on the System page to confirm it's running on the GPU.
Step 6: Add Qwen3.8-27B. Frigate can send frames to a vision model and keep the answer, which gives you summaries of alerts (a title, a short summary, a scene description and a threat level of 0, 1 or 2), descriptions of tracked objects that help semantic search, and a chat in the UI. The Frigate 0.18 docs recommend qwen3.8 for the descriptions and chat roles, and the helper script pins v0.18.0. Qwen3.8-27B is a dense multimodal model released in August 2026 under Apache 2.0. The docs say it's weaker than qwen3-vl at small details like an object in someone's hand, so try both on your own footage.
Running these models on CPU is impractical and the docs' rule of thumb is 24 GB of RAM or VRAM for the 33B class, so run it on a machine with a GPU on the wired LAN instead of inside the Frigate container. With llama.cpp you need a quantised GGUF plus the matching multimodal projector (--mmproj) for it to read images. The file names here are placeholders
llama-server -m MODEL-QUANT.gguf --mmproj MMPROJ.gguf --host 0.0.0.0 --port 8080 -c 16384 -ngl 99
The context size (-c) matters, because Frigate works out how many frames to send from it, so more context means more frames and more VRAM. Check it's reachable from the Frigate container with curl http://LLM-HOST:8080/health. On Ollama, set OLLAMA_NUM_PARALLEL=1 and pull the model first, Frigate won't do it for you. Qwen3.8 has a thinking mode and Simon Willison says it defaults to wildly overthinking, but Frigate turns thinking off for description tasks by itself, so no extra config, and chat has a toggle.
Add the provider and turn on review summaries
genai:
local_llm:
provider: llamacpp
base_url: http://192.168.10.20:8080
model: qwen3.8-27b
roles:
- descriptions
- chat
provider_options:
context_size: 16384
review:
genai:
enabled: true
image_source: recordings
additional_concerns:
- unfamiliar vehicles at the gate
Set model to the name your server reports and make context_size match -c. The roles are listed on purpose. A provider takes the embeddings role by default, and the docs warn a chat model there still returns vectors, just poor ones for search, with no error to tell you. Embeddings need a dedicated model, so leave that role off unless you run one. The recordings image source gives higher resolution frames than the default previews. For descriptions of tracked objects, enable them per camera
cameras:
front_door:
objects:
genai:
enabled: true
objects:
- person
Summaries are requested once an alert's activity has ended, so they arrive after the event and not in real time, and the threat level is a hint about what to look at first, not a verdict, because vision models get things wrong. If nothing is generated the docs have a section on debugging GenAI issues. Cloud providers need an internet connection to send images and prompts and local ones don't, so with both models local the frames only ever go to a machine on your own LAN.
Remote access and locking it down
Step 7: Add Tailscale with a second helper script. It also runs on the Proxmox host, because an LXC container has no access to /dev/net/tun by default and Tailscale needs it
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/add-tailscale-lxc.sh)"
Choose the Frigate container from the list. Reading the script, it appends lxc.cgroup2.devices.allow: c 10:200 rwm and a bind mount of /dev/net/tun to /etc/pve/lxc/CTID.conf, installs Tailscale from the official package repository and tags the container tailscale. Reboot the container as it tells you, then run this inside it
tailscale up
Open the login URL it prints and authenticate, and the container joins your tailnet. With the Tailscale app on your phone or laptop, Frigate is reachable at the container's Tailscale IP or MagicDNS name from anywhere, with nothing opened on the router. Only put the Frigate container on the tailnet and don't advertise the camera subnet as a route, so the cameras are only reachable through Frigate.
One thing to change. The helper script's default config contains this
auth:
enabled: false
and it points you at port 5000, which the Frigate authentication docs call the internal unauthenticated UI and API, with access that should be limited. The authenticated one is port 8971. Tailscale limits it to devices on your tailnet, but that's still every device on it, and a live camera view shouldn't be without a login. Set auth.enabled: true, restart Frigate and use the admin credentials from its startup logs, then change that password after your first login. Use ss -tlnp inside the container to see which ports are actually listening, and Tailscale ACLs to limit which of your devices can reach it at all.
The trade-offs are manual camera firmware updates, a PoE switch and cabling to plan for, and a GPU machine for the vision model. For something watching the property, cameras that can't call home, remote access without a single port forward and AI that stays on the LAN is a good deal. If this helps anyone set up something similar, great, and the Frigate docs are excellent. Read the helper scripts before you run them.