Configuration reference
NVR is configured by a single config.yaml. Start it with nvrd -config config.yaml — or skip the file entirely on macOS, where the app manages it for you. You rarely need to edit it by hand: Settings → Configuration in the web app is a live editor that validates changes, snapshots history, and restarts the daemon for you.
This page is a tour of the knobs that matter. The shipped config.example.yaml documents every option inline; these are the ones worth knowing.
server#
| Knob | Default | What it does |
|---|---|---|
addr | ":8085" | Listen address for the web UI and API. |
debug | false | Daemon-wide structured debug logging. NVRD_DEBUG=1 works too. |
log_file | stdout | Optional bounded, rotating log file (log_max_mb, log_max_backups). |
memory_mode | auto | Demand-aware Go memory sizing; grows with load, shrinks gradually. |
trust_proxy | false | Believe forwarded client-IP headers — only behind a known reverse proxy. |
server:
addr: ":8085" storage & retention#
The heart of the config. Footage lands as ~20-second fMP4 segments plus a SQLite index.
| Knob | Default | What it does |
|---|---|---|
path | ./recordings | Recording root (single drive). |
paths | — | Multi-drive list; each camera is pinned to one drive. Overrides path. |
index_path | primary root | Where the SQLite index lives — point at the boot SSD on multi-HDD setups. |
segment_seconds | 20 | Target segment length (keyframe-aligned). Keep near the default. |
max_age_hours | 720 | Keep everything 30 days. The recommended primary policy. 0 = off. |
total_gb | 10000 | Global size ceiling — a backstop, not the primary policy. |
per_camera_gb | 0 | Per-camera size cap. 0 = unlimited. |
min_free_gb | 0 | Evict a disk’s oldest if free space drops below this. |
record_streams | [main, sub] | Which stream IDs are recordable. |
record_by_default | true | Whether cameras without a record: field record. |
Retention knobs are independent strategies — a pass only runs when its knob is non-zero, so combine them. Recommended: time-first (max_age_hours) with a size ceiling as a backstop. A single camera can override the window with retention_days.
cameras#
One block per camera. Minimum viable: an id, a name, and a main stream URL.
cameras:
cam01:
name: "Front Door"
enabled: true
record: true # false = live-only, nothing written to disk
retention_days: 90 # optional per-camera override of max_age_hours
streams:
- id: main
url: "rtsp://admin:pass@192.168.10.201:554/Streaming/Channels/101"
enabled: true
audio: true
- id: sub
url: "rtsp://admin:pass@192.168.10.201:554/Streaming/Channels/102"
enabled: true See cameras & compatibility for URL formats per vendor and the devices shortcut for multi-channel DVRs.
thumbnails#
Scrub-preview sprites, decoded from the cheap sub stream. All optional — the defaults are sane.
| Knob | Default | What it does |
|---|---|---|
source_stream | sub | Which stream to decode preview frames from. |
cell_width / cell_height | 480x270 | Sprite cell size; match your sub stream’s native size for the crispest preview. |
ms_per_cell | 1000 | One preview cell per second of footage. |
cache_dir | beside recordings | Dedicated sprite directory — put it on a fast internal SSD. |
cache_max_gb | 0 (uncapped) | Total sprite cache size; oldest evicted first. |
playback#
| Knob | Default | What it does |
|---|---|---|
scrub_stream | sub | Which recorded stream the timeline scrub preview seeks. Must be a recorded, low-res stream for instant scrubbing. |
auth#
Off by default (LAN trust). Strongly recommended once the daemon is reachable from anywhere untrusted — including over the remote-access tunnel.
auth:
enabled: true On first run, a random admin password is written to <storage.path>/bootstrap_admin_password (mode 0600) — note it, sign in, then delete the file.
license#
Omit entirely for the built-in full-feature single-camera trial. Paid licenses are perpetual and unlock camera count and feature IDs; updates are never gated.
license:
token_path: "/usr/local/etc/nvrd/license.token" remote#
Managed remote access — see remote access for how it works before enabling.
remote:
enabled: true
device_name: "Office NVR"
tunnel:
enabled: true Everything else#
Less-frequently touched sections, all documented inline in config.example.yaml:
mqtt— publish daemon/camera state to Home Assistant.restream— re-serve streams asrtsp://<host>:8554/<camera>/<stream>without extra pulls to the camera.time_sync— keep camera clocks correct (older Hikvisions revert to 1970 after brownouts and corrupt the timeline).scrub_gop— shorten the scrub tier’s keyframe interval for lag-free scrubbing (control-plane writes, opt-in).detection.local_motion— pixel-change motion evidence for analog/DVR channels that emit no native smart events.updates— notify-only update checks against the signed release manifest.