The Argus software stack
Argus runs on a set of open-source tools that were chosen for specific reasons: they work well together, they're actively maintained, they don't require cloud accounts, and they can be understood and repaired by the person running them. This page explains what each piece does, why we chose it over alternatives, and what you'd use instead if you wanted to swap something out.
Why containers?
All of the Argus services run as Docker containers on the NVR host. This is worth explaining because it's not obvious why you'd run a camera system this way rather than installing software directly on the machine.
Isolation. Each service lives in its own container. A Frigate update can't break ntfy. A misconfigured Caddy can't crash Portainer. When something goes wrong, the blast radius is contained.
Clean updates. Updating a service means pulling a new container image and restarting one container. The host OS and every other service keep running. Rolling back is equally simple: point the container at an older image tag.
Reproducibility. LAN Foundry ships Argus systems with the full stack already running and configured. Docker makes it possible to produce the same environment every time, on every system, without manual steps that could vary.
Separation of data from software. Configuration and recorded footage live in named Docker volumes, separate from the container images. When you update Frigate, your recordings and config stay untouched because they're not inside the container.
The main tradeoff is a layer of abstraction to learn. When something goes wrong, you're checking container logs and Docker networks rather than system logs directly. That's why Portainer and Dozzle are in the stack; they make that layer navigable without living in the terminal.
The services
Frigate — NVR and AI Object Detection
Frigate handles everything camera-related: ingesting RTSP streams, running object detection on live footage, writing recordings to disk, and serving the web interface where you review events.
Why Frigate: It's the only mature open-source NVR that ships with real local AI object detection built in. Detection runs entirely on the NVR, with no cloud API and no subscription. It supports hardware acceleration via Hailo-8, NVIDIA GPU, or Intel QuickSync, which keeps CPU load manageable even on modest hardware. The configuration model (zones, masks, roles) is thoughtful, and the project is actively developed.
This is roughly what the Frigate service looks like in our Docker Compose stack. shm_size scales with camera count and resolution; the device passthrough lines only apply if you're using Hailo acceleration:
services:
frigate:
container_name: frigate
image: ghcr.io/blakeblackshear/frigate:stable
restart: unless-stopped
networks:
- nvr-network
shm_size: "256mb" # scale up with camera count/resolution
ports:
- "5000:5000" # web UI / API
- "8554:8554" # RTSP restream
- "8555:8555/tcp" # WebRTC
- "8555:8555/udp"
environment:
FRIGATE_RTSP_PASSWORD: "change-me"
volumes:
- /opt/lanfoundry/config/frigate:/config
- /mnt/recordings:/media/frigate
- type: tmpfs
target: /tmp/cache
tmpfs:
size: 1000000000
# devices: # uncomment for Hailo/iGPU passthrough
# - /dev/hailo0:/dev/hailo0
# - /dev/dri:/dev/dri
Alternatives:
| Option | Trade-off |
|---|---|
| MotionEye | Simpler setup, motion-only detection with no object classification |
| ZoneMinder | Long-established, but complex config and dated UI |
| Shinobi | Lighter weight, JavaScript-based, but less active development |
| Scrypted | Modern, plugin-based, good HomeKit support, but less mature for NVR use |
| Agent DVR / iSpy | Windows-native, good UI, not open-source |
Docker — Containerization
Docker is the runtime that runs all other services as isolated containers.
Why Docker: It's the most widely understood container platform with the largest ecosystem of pre-built images. Every service in the Argus stack ships an official Docker image. Portainer, the container management UI we include, is built around Docker. The community resources, troubleshooting forums, and documentation for Frigate, ntfy, and Caddy all assume Docker.
Alternatives:
| Option | Trade-off |
|---|---|
| Podman | Docker-compatible CLI, rootless by default, good for security-focused setups |
| LXC / Proxmox | OS-level isolation rather than process-level, more overhead, better for running full OS environments |
| Bare metal | No abstraction layer, but updates are manual per-service and harder to reproduce across systems |
| NixOS | Fully declarative, highly reproducible, but steep learning curve |
Caddy — Reverse Proxy
Caddy sits in front of all the services and routes requests to them by hostname. Instead of typing 192.168.1.100:5000 to reach Frigate, you type https://frigate.internal. Caddy also handles HTTPS using its own internal certificate authority, so all connections are encrypted even on the local network.
Why Caddy: Its local certificate authority support (local_certs + tls internal) requires no external infrastructure or DNS challenges. It just works on a private network. The configuration is minimal: five lines per service compared to twenty or more with nginx. It handles automatic certificate issuance and renewal without certbot or cron jobs.
The Caddy service in the stack:
services:
caddy:
container_name: caddy
image: caddy:2-alpine
restart: unless-stopped
networks:
- nvr-network
depends_on:
- frigate
- ntfy
- dozzle
ports:
- "80:80"
- "443:443"
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- /opt/lanfoundry/config/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
And the Caddyfile it mounts:
{
local_certs
}
frigate.internal {
tls internal
reverse_proxy frigate:5000
}
portainer.internal {
tls internal
reverse_proxy https://portainer:9443 {
transport http {
tls_insecure_skip_verify
}
}
}
cockpit.internal {
tls internal
reverse_proxy https://host.docker.internal:9090 {
transport http {
tls_insecure_skip_verify
}
}
}
ntfy.internal {
tls internal
reverse_proxy ntfy:80
}
dozzle.internal {
tls internal
reverse_proxy dozzle:8080
}
Alternatives:
| Option | Trade-off |
|---|---|
| nginx Proxy Manager | Web UI for managing proxy rules, good for non-technical users, more overhead |
| Traefik | Automatic service discovery from Docker labels, powerful but complex configuration |
| HAProxy | High-performance, widely used, but no built-in HTTPS certificate management |
| nginx (raw) | Highly configurable, no cert automation without certbot |
ntfy — Push Notifications
ntfy is a self-hosted push notification server. When frigate-notify sends it a notification, ntfy queues it and delivers it to subscribed devices: your phone, a browser tab, anything running the ntfy app or subscribed via the API.
Why ntfy: It requires no account, no third-party service, and no ongoing subscription. You run it, you control it. It has well-maintained apps for iOS and Android. On Android, it supports UnifiedPush, which routes notifications entirely within your own infrastructure without passing through Google's servers. For iOS, notifications must transit Apple's servers regardless of what notification service you use. This is an Apple platform constraint, not an ntfy limitation.
The ntfy service:
services:
ntfy:
container_name: ntfy
image: binwiederhier/ntfy:latest
restart: unless-stopped
networks:
- nvr-network
ports:
- "2586:80"
volumes:
- ntfy-cache:/var/cache/ntfy
- /opt/lanfoundry/config/ntfy:/etc/ntfy:ro
command: serve
# ntfy server.yml
# Upstream relay to ntfy.sh is intentionally disabled for privacy-first
# operation — notifications never leave your network by default. To enable
# remote push later (notification text transits ntfy.sh), add:
# upstream-base-url: "https://ntfy.sh"
# then restart the ntfy container.
base-url: https://ntfy.internal
attachment-cache-dir: /var/cache/ntfy/attachments
attachment-total-size-limit: 5G
attachment-file-size-limit: 15M
Alternatives:
| Option | Trade-off |
|---|---|
| Gotify | Simpler, self-hosted, but no native iOS push support without external relay |
| Pushover | Polished apps, reliable delivery, but not self-hosted; requires account and one-time $5 per platform |
| Telegram bot | Free and widely used, but requires a Telegram account and routes through Telegram's servers |
| Home Assistant | If HA is already in your stack, its notification system covers most of the same ground |
| Frigate's built-in ntfy | Simpler config, no frigate-notify needed, but less flexible filtering and no snapshot attachments in older versions |
frigate-notify — Event Routing
frigate-notify runs alongside Frigate and bridges its event API to notification providers. It polls Frigate every 30 seconds for new events, formats a notification, attaches the event snapshot, and sends it to ntfy.
Why frigate-notify: Frigate's built-in notification support is improving but has historically been limited in filtering options and attachment support. frigate-notify gives finer-grained control over which events trigger alerts, supports multiple notification providers simultaneously, and handles snapshot attachment reliably.
The frigate-notify service:
services:
frigate-notify:
container_name: frigate-notify
image: ghcr.io/0x2142/frigate-notify:latest
restart: unless-stopped
networks:
- nvr-network
depends_on:
- frigate
- ntfy
volumes:
- /opt/lanfoundry/config/frigate-notify/config.yml:/app/config.yml:ro
# frigate-notify config.yml
app:
mode: events
frigate:
server: http://frigate:5000
ignoressl: false
webapi:
enabled: true
interval: 30
startup_check:
attempts: 5
interval: 30
alerts:
ntfy:
enabled: true
server: http://ntfy:80
topic: your-topic-name
ignoressl: false
Other providers (Discord, Gotify, Pushover, Telegram, SMTP, and more) can be enabled alongside or instead of ntfy — see the frigate-notify docs for the full provider list.
Alternatives:
| Option | Trade-off |
|---|---|
| Frigate's built-in ntfy | No extra container, simpler config, sufficient for most setups |
| Home Assistant automations | Very flexible if HA is already in the stack, but requires HA setup |
| Custom scripts | Full control, but you maintain them |
| Frigate webhooks | Trigger any HTTP endpoint on an event, no intermediary needed |
Portainer — Container Management
Portainer is a web UI for managing Docker. It shows running containers, lets you start and stop services, browse volume contents, view logs, and manage Docker Compose stacks, all from a browser rather than the command line.
Why Portainer: It makes the Docker layer navigable for people who aren't comfortable with the CLI. Being able to restart a container, browse a config volume, or check container status in a browser without SSH access significantly lowers the barrier for day-to-day management. It's also how we recommend editing configuration files; Portainer's volume browser handles this without requiring SSH or docker exec.
Portainer is bootstrapped first, before the rest of the stack, so it can then deploy everything else as a managed stack:
services:
portainer:
container_name: portainer
image: portainer/portainer-ce:lts
restart: always
networks:
- nvr-network
ports:
- "9443:9443"
- "8000:8000"
command: --no-setup-token
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
networks:
nvr-network:
name: nvr-network
external: true
Alternatives:
| Option | Trade-off |
|---|---|
| Dockge | Focused on Compose stacks, modern UI, lighter weight, less full-featured |
| Yacht | Simple, minimal, less active development |
| Lazydocker | Terminal UI, fast, no web access, requires CLI comfort |
| Raw Docker CLI | No overhead, full control, requires CLI comfort and SSH access |
Cockpit — Host System Management
Cockpit provides a browser-based dashboard for the NVR host operating system. CPU, memory, disk usage, running services, system logs, and basic network info, all accessible at https://cockpit.internal without SSH.
Why Cockpit: It's maintained by Red Hat and packaged in Ubuntu's standard repositories, which means it gets security updates through the normal OS update process. It adds almost no overhead, and covers the most common reason you'd want to SSH into the host: checking that the system is healthy. It doesn't retain history or alert on thresholds, but for a dedicated NVR it doesn't need to.
Alternatives:
| Option | Trade-off |
|---|---|
| Webmin | More features, older codebase, heavier |
| Glances | Lightweight real-time system view, no persistent management features |
| netdata | Beautiful dashboards, historical data, more setup and resource use |
| Grafana + Prometheus | Full observability stack, significant setup and overhead for a single-node NVR |
| SSH only | No overhead, requires CLI comfort |
Dozzle — Container Log Viewer
Dozzle is a real-time Docker log viewer. It streams logs from all running containers to a browser tab, without storing them or requiring any persistent configuration.
Why Dozzle: The most common reason to look at logs is to diagnose something that just went wrong. Dozzle makes that instant: open https://dozzle.internal, log in, click a container, and watch the logs live. It's read-only, stateless, and adds no meaningful overhead. It covers the 90% of log-checking cases that don't require log retention or search. It ships behind basic auth by default, since raw container logs can include IPs and other operational details you may not want open to anyone on the LAN.
services:
dozzle:
container_name: dozzle
image: amir20/dozzle:latest
restart: unless-stopped
networks:
- nvr-network
environment:
DOZZLE_NO_ANALYTICS: "true"
DOZZLE_USERNAME: admin
DOZZLE_PASSWORD: "change-me"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
Alternatives:
| Option | Trade-off |
|---|---|
| Portainer's log view | Already in the stack, slightly less ergonomic for live tailing |
docker logs CLI |
No extra container, requires SSH access |
| Grafana Loki | Full log aggregation and search, significant setup and storage overhead |
Tailscale — Remote Access (optional)
Tailscale is included in the stack but inactive by default. It's the answer for anyone who wants to check their footage away from home but doesn't have a router capable of running a proper WireGuard VPN gateway.
Why Tailscale: It requires no port forwarding, no static IP, and no firewall rules on the router. It builds a private mesh network between your devices using WireGuard under the hood, so remote access to the NVR host works the same whether you're on your phone's cellular connection or someone else's Wi-Fi. Because we can't assume every customer's router supports VLANs and site-to-site VPN the way OPNsense does, having a container-level option keeps remote access possible without touching the router.
The container ships without a pre-auth key, so it sits idle until you opt in:
services:
tailscale:
container_name: tailscale
image: tailscale/tailscale:stable
restart: unless-stopped
network_mode: host
cap_add:
- NET_ADMIN
- NET_RAW
devices:
- /dev/net/tun:/dev/net/tun
environment:
TS_HOSTNAME: "argus-nvr"
TS_STATE_DIR: /var/lib/tailscale
TS_USERSPACE: "false"
TS_AUTHKEY: "" # blank = inactive until you authenticate
volumes:
- tailscale-state:/var/lib/tailscale
With no auth key, the container runs but never joins a network. To activate it, either pass a pre-auth key generated from the Tailscale admin console at install time, or authenticate manually after the fact:
This is the option for customers without router-level VPN — it's not a replacement for VLAN segmentation at the network edge. See OPNsense below for the more capable router-based approach.
Alternatives:
| Option | Trade-off |
|---|---|
| OPNsense WireGuard | Router-level VPN, no extra container, requires router hardware capable of running OPNsense |
| Ubiquiti UniFi VPN | Polished, but cloud-account dependent |
| ZeroTier | Similar mesh model to Tailscale, smaller community |
| Manual WireGuard | Full control, no coordination server, more setup |
OPNsense — Firewall and Router
OPNsense is the open-source firewall and router that sits at the edge of the network. It handles DHCP, DNS (via Unbound and Dnsmasq), VLAN segmentation, and firewall rules that keep cameras isolated from the rest of the network.
Why OPNsense: It's actively developed, has a clear UI, and supports the features needed for a proper camera network: VLANs, local DNS with host overrides, firewall rules between network segments, and WireGuard VPN. The Unbound DNS integration is how frigate.internal resolves correctly on every device on the network without editing a hosts file.
OPNsense is not part of the Argus container stack. It runs on a separate device, a dedicated mini-PC or router hardware. Argus does not ship with a router.
Alternatives:
| Option | Trade-off |
|---|---|
| pfSense | Very similar feature set, different license history and community governance |
| OpenWRT | Runs on consumer router hardware, lighter, less UI polish |
| Ubiquiti UniFi | Polished hardware and software, but cloud-account dependent for full features |
| Consumer router | Simple setup, but limited or no VLAN and local DNS support; not recommended for camera network isolation |
Swapping components
The stack is not tightly coupled. The services communicate over a shared Docker network using container names, and the only real dependency chain is:
- Frigate needs cameras on the network to pull RTSP from
- frigate-notify needs Frigate to be running to poll for events
- ntfy needs
base-urlset if you want snapshot attachments - Caddy needs DNS entries to exist before hostnames resolve
Everything else is independent. You can run the stack without Cockpit, without Dozzle, or without Portainer if you prefer CLI management. You can replace ntfy with Gotify by changing the provider block in frigate-notify's config. You can skip frigate-notify entirely and use Frigate's built-in notification support.
The guides on this site describe the default Argus configuration. Where an alternative is straightforward to substitute, the relevant guide notes it.
Where to go from here
- Setting up network segmentation and VLANs — the network layer the stack runs on
- Configuring Caddy as a reverse proxy — how hostname routing works
- Setting up push notifications — the Frigate → frigate-notify → ntfy chain in detail
- Going Further — Home Assistant integration and community resources
Was this page helpful?
Found something wrong or out of date? Let us know below, or open a GitHub issue if you have a GitHub account.