-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy path.env.example
More file actions
267 lines (239 loc) · 15.8 KB
/
Copy path.env.example
File metadata and controls
267 lines (239 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# CrumbVMS — environment reference.
#
# You normally do NOT edit this file. Run `./scripts/setup-env.sh` to generate a
# .env with strong secrets and sane defaults (zero-edit first run), then sign in
# at /admin as 'admin' with the memorable SEED_ADMIN_PASSWORD it printed. Copy
# this to .env only for advanced/manual setups; never commit .env. Full
# explanations of every knob: docs/COMPOSE.md.
# --- Time zone ---
# Local wall-clock used across the stack: quiet hours, the nightly DB backup
# schedule (DB_BACKUP_SCHEDULE), the offsite-sync cron, and every log timestamp.
# An IANA name like Europe/Berlin or America/New_York. setup-env.sh detects the
# host's zone and writes it here; if unset the compose default is UTC (NOT any
# local zone), so an operator who never runs setup-env still gets a predictable,
# non-surprising clock.
TZ=UTC
# RECORDER_TZ= # unset → inherits TZ. Only set it to run the
# per-camera archive-schedule cron in another zone.
# --- Logging ---
# RUST_LOG=info # tracing filter, e.g. info,crumb_recorder=debug
# LOG_FORMAT=json # 'json' (default) or 'text' for a human-readable console
# --- PostgreSQL ---
POSTGRES_USER=crumb
POSTGRES_PASSWORD=change-me # setup-env.sh generates a strong one
POSTGRES_DB=crumb
# api + recorder read this. For a REMOTE Postgres, change the host and use
# docker-compose.override.example.yml. Host 'postgres' = the compose service name.
DATABASE_URL=postgresql://crumb:change-me@postgres:5432/crumb
# DB_POOL_SIZE=42 # optional; unset = code default (32). Forwarded by compose to api + recorder
# --- Streaming (fallbacks; the admin "Server & streaming" settings win) ---
# Leave both blank: Crumb's go2rtc runs embedded in the recorder and the compose
# defaults are correct. Set the public RTSP address in the admin UI, not here. Only
# set these to point at an EXTERNAL restreamer (with GO2RTC_EMBEDDED=false). Full
# go2rtc security model: docs/COMPOSE.md § go2rtc.
CRUMB_GO2RTC_API_BASE=
CRUMB_GO2RTC_RTSP_BASE=
# GO2RTC_EMBEDDED=true
# go2rtc Basic/RTSP auth. REQUIRED (compose fails fast if unset); setup-env.sh
# generates a strong GO2RTC_PASS. GO2RTC_USER is just the Basic-auth username slot.
GO2RTC_USER=go2rtc
GO2RTC_PASS=change-me
# OPTIONAL restream auth opt-out (issue #398). Default (unset/anything but 'off')
# keeps the LAN RTSP restream AUTHENTICATED, the secure default. Set to 'off' ONLY
# if you deliberately want an open, credential-free restream on a trusted LAN:
# GO2RTC_AUTH=off
# means anything on your LAN can pull every camera from rtsp://<host>:18554/<name>
# with no password. The internal go2rtc REST API (:1984) stays authenticated
# regardless, so GO2RTC_USER/PASS are still required. docs/COMPOSE.md § go2rtc.
# GO2RTC_AUTH=off
# External BYO-Frigate go2rtc (only cameras served_by='frigate'); separate creds.
GO2RTC_RTSP_BASE=
GO2RTC_API_BASE=
# --- WebRTC live (iOS/browser) ---
# The server's own LAN IP that WebRTC hands to clients as an ICE candidate
# (form: <server-LAN-ip>:8556). REQUIRED for iOS/WebRTC live view: without it
# go2rtc only advertises its container IP + the host's public IP, LAN clients
# never complete ICE, and live silently falls back to ~1fps snapshots
# (docs/IOS-LIVE-VIDEO.md). setup-env.sh detects and writes the host LAN IP;
# leave blank only if you don't use the WebRTC/iOS live path.
WEBRTC_CANDIDATE=
# --- Recording ---
SEGMENT_SECONDS=4 # 2-6s; shorter = snappier seek. Starts on a keyframe.
# SEGMENT_RECEIPT_TIMEOUT_SECS=90 # stall watchdog before a worker reconnects; clamped to [20, 3600]
# --- Recorder supervision loops (optional; defaults are right for almost everyone) ---
# CONFIG_POLL_SECONDS=30 # how often the recorder re-diffs the DB camera list
# RECONCILE_INTERVAL_SECONDS=900 # reconcile pass interval (adopt orphans, repair drift); floored to 60
# RECONCILE_PAUSED=false # true = run NO reconcile passes. Set this while deliberately
# moving footage out-of-band; recording/motion/retention continue.
# MOTION_UNHEALTHY_ALERT_SECS=180 # how long a detector must stay CONTINUOUSLY unhealthy before
# an alert. Hysteresis for cameras that blip and self-heal;
# it never delays the fail-open recording rail.
# --- Motion-mode RAM cache (docs/MOTION-RECORDING.md) ---
# Motion cameras buffer in tmpfs (RAM) and persist to disk only on motion; idle
# footage never touches disk. Counts against the recorder's memory limit, not disk.
# 512 MiB covers ~10 cameras at 30s pre-roll; full sizing rule in docs/COMPOSE.md.
MOTION_CACHE_TMPFS_BYTES=536870912
# MOTION_CACHE_DIR=/cache/motion # only change alongside the tmpfs target in compose
MOTION_RECORDING_SHADOW=0 # 1 = stamp keep/discard verdict without changing behavior
# --- Storage ---
# One broad media root, bind-mounted into both containers. Add a disk by mounting it
# under MEDIA_HOST_PATH and adding '/data/<subdir>' as a storage path in the admin UI.
# MEDIA_HOST_PATH must be writable by uid 1001 (the recorder's user) or nothing
# records while live view still looks fine. setup-env.sh preflights it, and honours
# this key as an INPUT: MEDIA_HOST_PATH=/mnt/tank/crumb ./scripts/setup-env.sh
MEDIA_HOST_PATH=./_data # host dir → /data
MEDIA_ROOT=/data # container root; storage paths live under it
LIVE_STORAGE_PATH=/data/live
ARCHIVE_STORAGE_PATH=/data/archive # unset → archive shares the live disk
# LIVE_STORAGE_NAME=Live # display label only, read when the bucket is first created
# ARCHIVE_STORAGE_NAME=Archive # same
# --- GPU / motion decode ---
# 'cpu' is the safe default: software motion decode works on any host (including
# GPU-less NUCs/VMs) and is fast enough for Crumb's fps-capped motion. Enable a GPU
# explicitly: 'vaapi' for an Intel/AMD iGPU (docker-compose.vaapi.example.yml),
# 'cuda' for NVIDIA NVDEC (docker-compose.gpu.example.yml), or 'auto' to use NVDEC
# only when the recorder can really open an NVIDIA device (no device → cpu).
# A hardware backend that can't decode a camera drops that camera to cpu.
# This only affects motion DECODE; recording is always stream-copy.
MOTION_HWACCEL=cpu
# MAX_GPU_DECODE_SESSIONS=4 # global cap on concurrent NVDEC sessions; past the cap a camera uses cpu
# MOTION_VAAPI_DEVICE=/dev/dri/renderD128 # DRI render node for MOTION_HWACCEL=vaapi (vaapi overlay wires it in)
# NVIDIA_VISIBLE_DEVICES=all # read by the NVIDIA container runtime, not by Crumb (gpu overlay)
# --- API auth ---
# openssl rand -hex 32 (>= 32 bytes; the API refuses to boot on the weak placeholder).
JWT_SECRET=change-me-generate-with-openssl-rand-hex-32
JWT_EXPIRY_SECONDS=86400 # 24h token lifetime
# --- API server ---
# The address the api binds INSIDE its container. Leave 0.0.0.0:8080 — Docker
# already gates host exposure via the compose `ports:` mapping. Setting this to
# 127.0.0.1:8080 does NOT restrict access to the host: it binds container-local,
# so the published port answers nothing while the healthcheck still passes
# (a silently dead API). To restrict the API to localhost, change the compose
# port mapping to "127.0.0.1:8080:8080" instead (docker-compose.yml `api` ports).
API_BIND=0.0.0.0:8080
# --- Exports ---
EXPORT_DIR=/exports # top-level (own volume), NOT under the /data:ro mount
EXPORT_TTL_SECONDS=86400
# EXPORT_CACHE_MAX_BYTES=21474836480 # 20 GiB budget for the export cache
# --- Server-generated streams + caches (optional) ---
# MOBILE_STREAM_ENABLED=true # on-demand H.264 transcode mobile clients fall back to when a
# camera won't decode on-device (e.g. H.265 all the way down).
# Built lazily, only while something is watching.
# MOBILE_STREAM_WIDTH=640 # transcode width in px (floored at 160)
# MAIN_REPAIR_TRANSCODE_ENABLED=false # opt-in, per-camera FULL-RES H.265->H.264 transcode of a main
# whose SDP lacks fmtp (Android/Media3 rejects it with "missing
# attribute fmtp", e.g. some Uniview LPR cams). A cheap copy-remux
# does NOT work here (go2rtc then emits an HEVC aggregation packet
# Media3 can't read), so this is a real re-encode: leave off and
# Android just uses the H.264 sub (SD); on = HD at recorder CPU cost.
# SEGMENT_LOW_CACHE_MAX_BYTES=2147483648 # 2 GiB budget for the low-res playback segment cache
# --- Scrub-preview cache (optional; five of these are also editable in the console, which wins) ---
# THUMB_PREGEN_ENABLED=false # build previews in the background so the FIRST drag is instant too
# THUMB_PREGEN_LOOKBACK_HOURS=2
# THUMB_PREGEN_SCAN_SECS=60
# THUMB_PREGEN_WIDTH=480 # env-only; must match the clients' scrub-still width or previews go unused
# THUMB_CACHE_DIR= # env-only; unset → EXPORT_DIR. Point at an SSD on a spinning-disk system
# THUMB_EXTRACT_MAX_CONCURRENCY= # env-only; unset → about half the CPU cores
# THUMB_CACHE_MAX_BYTES=21474836480 # 20 GiB
# THUMB_CACHE_TTL_SECONDS=2592000 # 30 days
# --- Built-in DB backup (ON by default; docs/BACKUP.md) ---
# The api runs a nightly pg_dump of the segments index (lose it and footage is
# unplayable). Put this on a DIFFERENT disk than the footage; must be writable by
# uid 1001. setup-env.sh creates ./backups and tries the chown, which only succeeds
# when it runs as root; it tells you when it couldn't. Unwritable → api warns and
# disables the job.
DB_BACKUP_HOST_PATH=./backups
# BACKUP_DIR=/backups # container-side mount point. Leave it: unset/empty
# DISABLES the backup job entirely.
DB_BACKUP_SCHEDULE=03:15 # local wall-clock HH:MM in TZ
DB_BACKUP_KEEP_DAYS=7
DB_BACKUP_KEEP_WEEKS=4
DB_BACKUP_KEEP_MONTHS=0
DB_BACKUP_ENABLED=true
# --- Off-host backup copy (OPTIONAL; docs/BACKUP.md) ---
# On-host backups still die with the host in a fire/theft event. Either point
# DB_BACKUP_HOST_PATH at a NAS/NFS mount, OR use the `backup-offsite` rclone service
# (opt-in): docker compose --profile offsite up -d
# BACKUP_OFFSITE_REMOTE=mynas:crumb-backups
# BACKUP_OFFSITE_SCHEDULE=15 5 * * * # 5-field cron (busybox), not the HH:MM form
# BACKUP_OFFSITE_RCLONE_CONF=./rclone.conf
# --- Alerting (optional) ---
# JSON webhook for recorder-down paging (Discord reads "content", Slack "text").
# Empty = recorder death is silent (it still records). Set this for production.
# ALERT_WEBHOOK_URL=https://discord.com/api/webhooks/xxxxx/yyyyy
ALERT_WEBHOOK_URL=
# CAMERA_OFFLINE_BOOT_GRACE_SECS=180 # grace after a restart before camera-offline alerts
# MAINTENANCE_UNTIL= # unix-seconds; suppress all health alerts until then
# --- Retention low-disk safety floor (optional) ---
# Eviction always keeps at least this much free on the physical disk holding the
# footage, whichever of the two is larger. Unset = 5% / 50 GiB defaults.
# MIN_FREE_FRACTION=0.05 # fraction of the disk kept free
# MIN_FREE_BYTES=53687091200 # absolute floor in bytes (50 GiB)
# --- Reverse proxy / TLS (optional) ---
# Behind the bundled Caddy (:8443) or your own reverse proxy, set TRUST_PROXY=1
# so the API rate limiter keys on the real client IP from X-Forwarded-For
# instead of the proxy's address. Leave unset when clients hit :8080 directly.
# TRUST_PROXY=1
# CRUMB_HTTPS_PORT=8443 # the bundled Caddy's HTTPS port (docs/TLS.md)
# --- Hardware decode (optional; used with docker-compose.vaapi.example.yml) ---
# RENDER_GID=993 # GID of /dev/dri/renderD128 on the host (scripts/enable-hwaccel.sh sets this)
# --- Update-available check (optional, issue #7; OFF by default) ---
# When enabled, the api periodically asks github.com for the latest CrumbVMS
# release tag (version number only, nothing sent) so clients can show an
# "update available" notice. The admin console toggle (Server settings) wins
# over this env once set. false = zero github.com requests, ever.
UPDATE_CHECK_ENABLED=false
# --- Seed (admin bootstrap) ---
# Default: setup-env.sh generates a memorable SEED_ADMIN_PASSWORD passphrase,
# prints it once, and the api seeds the admin with it (argon2-hashed) at first
# boot; you sign in at /admin with it. SEED_ADMIN_PASSWORD_HASH (a PHC string,
# recorder path) is an alternative. Both idempotent. Blanking SEED_ADMIN_PASSWORD
# opts into the browser create-admin wizard instead, which reopens the short
# unauthenticated /auth/bootstrap window until you create the admin.
SEED_ADMIN_USERNAME=admin
SEED_ADMIN_PASSWORD=
SEED_ADMIN_PASSWORD_HASH=
SEED_DEFAULT_CAMERAS=false # dev-only prototype cameras; keep false
# --- ONVIF / PTZ (optional, legacy fallback) ---
# Per-camera ONVIF creds live in the admin camera editor now; this base64 JSON map
# (keyed by go2rtc_name) is a one-time fallback only.
ONVIF_CONFIG_B64=
# --- Image source: pull (default) vs build ---
# Unset → pulls ghcr.io/badbread/crumbvms/{api,recorder}:latest. Pin a version for
# reproducible upgrades. Build from source: add -f docker-compose.build.yml. docs/IMAGES.md
# CRUMB_IMAGE_PREFIX=ghcr.io/badbread/crumbvms
# CRUMB_VERSION=v0.2.0
# --- Frigate detection (ALL OPTIONAL; bring your own Frigate) ---
# Crumb bundles no detection. Point at your own Frigate's MQTT broker and set each
# camera's "Frigate camera name" in the admin UI. Empty FRIGATE_MQTT_URL = disabled.
# PLAINTEXT mqtt:// ONLY — Crumb's MQTT client is built with no TLS transport, so an
# mqtts:// URL is REJECTED (loudly) rather than silently connected in the clear.
# Keep the broker on a trusted LAN.
# FRIGATE_MQTT_URL=mqtt://<broker-host>:1883
# FRIGATE_MQTT_USER=frigate
# FRIGATE_MQTT_PASSWORD=changeme # or FRIGATE_MQTT_PASSWORD_B64 to survive $-escaping
# FRIGATE_MQTT_PASSWORD_B64=
# FRIGATE_MQTT_PREFIX=frigate
# FRIGATE_API_BASE=http://<frigate-host>:5000
# FRIGATE_MIN_SCORE=0.3
# FRIGATE_CATCHUP_HOURS=24
# --- Crumb-native LPR worker (ALL OPTIONAL; opt-in `alpr` compose profile) ---
# Crumb's own local plate OCR (fast-alpr) — no cloud, no third-party agent. Only
# used when you run: docker compose --profile alpr up -d --build crumb-alpr
# First enable LPR + mint an ingest token in Admin -> LPR (rotate token), then:
# LPR_INGEST_TOKEN=<paste the rotated token>
# LPR_CAMERA_ID=<the Crumb camera UUID this worker reads>
# LPR_RTSP_URL=rtsp://<go2rtc-user>:<go2rtc-pass>@recorder:8554/<stream-name>
# LPR_MIN_CONFIDENCE=0.80 # drop reads below this mean OCR confidence
# LPR_SAMPLE_FPS=5 # analysis rate while a pass is active
# LPR_API_BASE=http://api:8080 # override if the worker runs off-host
# LPR_LOG_LEVEL=info # worker log verbosity
# --- Home Assistant integration (ALL OPTIONAL; self-hosted, off by default) ---
# Only a fallback: normally you configure this in the admin console (Detection &
# clips -> Home Assistant), which stores it in the DB (DB wins over these). Use a token
# from a dedicated NON-admin HA user (Settings -> People), profile -> Long-Lived
# Access Tokens. HA_TOKEN is a secret; prefer HA_TOKEN_FILE (a Docker secret path)
# in production. The integration stays dormant until enabled in the console.
# HA_BASE_URL=http://<home-assistant-host>:8123
# HA_TOKEN=<long-lived-access-token>
# HA_TOKEN_FILE=/run/secrets/ha_token