Skip to main content

Environment reference

The keys Crumb reads from .env, grouped by area (a few generic ones like RUST_LOG and LOG_FORMAT are omitted). The authoritative copy lives in .env.example in the repository; this page mirrors it for browsing. Most installs never need to touch most of these, setup-env.sh fills in the values that matter for a first boot.

Every key below is wired into the stock docker-compose.yml (the GPU keys ride in via their opt-in overlay files), so the workflow is uniform: set the key in .env, restart the affected container, done. No docker-compose.override.yml is needed. Where a value is also editable in the admin console, the console value (stored in the database) wins over the env default; that's flagged in the notes.

Time zone

KeyDefaultNotes
TZUTCLocal wall-clock for the whole stack: quiet hours, the nightly DB backup schedule (DB_BACKUP_SCHEDULE), the offsite-sync cron, and every log timestamp. Set an IANA name like America/Los_Angeles or Europe/Berlin. setup-env.sh detects the host's zone and writes it; if it can't, the compose default is UTC (not any local zone), so the clock is at least predictable.
RECORDER_TZinherits TZIANA zone the recorder's per-camera archive-schedule cron runs in. Unset or empty means it inherits TZ above, which is what you want; set it only to run archive schedules in a different zone than the rest of the stack. A value that fails to parse is logged loudly and falls back rather than stopping the recorder.

PostgreSQL

KeyDefaultNotes
POSTGRES_USERcrumb
POSTGRES_PASSWORDgeneratedstrong random value from setup-env.sh
POSTGRES_DBcrumb
DATABASE_URLderivedfull connection string used by api + recorder
DB_POOL_SIZE32connection pool size. Fixed default of 32, not a per-camera formula. Raise it past ~16 cameras (rule of thumb 2 * cameras + 10), and raise Postgres max_connections to match. Forwarded by the stock docker-compose.yml to both api and recorder; set it in .env and restart both containers.

WebRTC live (iOS / browser)

KeyDefaultNotes
WEBRTC_CANDIDATEemptyThe server's own LAN IP that go2rtc advertises to WebRTC/iOS clients as an ICE candidate, in the form <server-LAN-ip>:8556. Required for the iOS/browser WebRTC live path: without it, LAN clients never complete ICE and live silently degrades to roughly 1fps snapshots. setup-env.sh detects and writes the host LAN IP; leave it blank only if you don't use WebRTC/iOS live view.

Streaming (go2rtc)

Crumb's own go2rtc restreamer runs embedded in the recorder container. The values below are fallbacks: once you set the server's address in the admin console's Server & streaming settings, that value wins.

KeyDefaultNotes
CRUMB_GO2RTC_API_BASEemptyleave blank, internal compose defaults are correct
CRUMB_GO2RTC_RTSP_BASEemptyleave blank; set the public RTSP address in the admin console instead
GO2RTC_USERgo2rtca fixed, non-secret Basic-auth username label (not generated); required, compose fails fast if unset
GO2RTC_PASSgeneratedrequired; required to be strong, rotate with care (needs a recorder + api restart)
GO2RTC_EMBEDDEDtrueset false only if running an external restreamer
GO2RTC_RTSP_BASE / GO2RTC_API_BASEemptya separate, external Frigate go2rtc instance, only used for cameras served by it

Recording

KeyDefaultNotes
SEGMENT_SECONDS42 to 6 seconds; short segments mean near-instant seek

Recorder internals

Tuning knobs for the recorder's supervision loops. The defaults are right for almost everyone; RECONCILE_PAUSED is the only one an operator normally touches, and only during a deliberate storage migration.

KeyDefaultNotes
CONFIG_POLL_SECONDS30how often the recorder diffs the database camera list against its running workers to pick up config changes
RECONCILE_INTERVAL_SECONDS900how often the reconcile pass re-runs (adopt orphan files on disk, repair size drift, prune dangling index rows); floored to 60
RECONCILE_PAUSEDfalsemaintenance switch: true runs no reconcile passes at all. Set it while deliberately moving footage files out-of-band (storage migration, disk swap), since reconcile would race the move. Recording, motion, and retention continue normally.

Motion-mode RAM cache

See Motion & Detection for the mechanism this configures.

KeyDefaultNotes
MOTION_CACHE_TMPFS_BYTES536870912 (512 MiB)tmpfs size for the pre/post-roll ring buffer; sizing rule of thumb in .env.example
MOTION_CACHE_DIR/cache/motiononly change alongside the compose tmpfs target
MOTION_RECORDING_SHADOW01 records everything as before but stamps each segment with the keep/discard verdict the buffer would have made, for validating before flipping a camera live

Timeline previews (scrubbing)

See Timeline scrubbing for what these do. All optional; the defaults work.

Five of these are also editable live from the admin console (Server → Scrub previews): the env value below is only the default until an operator sets it in the console, at which point the console value wins (no restart needed, takes effect within one scan interval / cache-sweep tick). The other three (THUMB_CACHE_DIR, THUMB_PREGEN_WIDTH, THUMB_EXTRACT_MAX_CONCURRENCY) are env-only, see the Notes column.

All of these are forwarded by the stock docker-compose.yml into the api container: set them in .env and restart the api. For the five console-editable knobs, prefer the console anyway; once an operator sets a value there, the database copy wins and the env value is just the default.

One honest footnote: the compose file also forwards a few more THUMB_* names (THUMB_INTERVAL_SECS, THUMB_MAX_ATTEMPTS, THUMB_MAX_WIDTH, THUMB_MIN_WIDTH, THUMB_NEAR_BLACK_LUMA, THUMB_EXTRACT_TIMEOUT_SECS) that the current release treats as fixed built-in constants (a 4-second preview grid, widths clamped 48-640, a 12-second extract timeout, and the black-frame retry logic). Setting those in .env today has no effect; they're plumbed through for a future release, so they don't get rows here.

KeyDefaultConsole-editable?Notes
THUMB_PREGEN_ENABLEDfalseyesbuild scrub previews in the background so the first drag is instant too; costs some ongoing CPU + disk
THUMB_PREGEN_LOOKBACK_HOURS2yeshow far back to build previews when the worker starts (console clamps 0-168h)
THUMB_PREGEN_SCAN_SECS60yeshow often to build previews for newly-recorded footage (console clamps 5-3600s)
THUMB_PREGEN_WIDTH480no, env-onlypreview width in pixels; must equal the playback clients' scrub-still width or pre-generated previews go unused (silently wasted CPU/storage), which is why this one stays a deployment-time setting, not a console toggle
THUMB_CACHE_DIR(EXPORT_DIR)no, env-onlywhere the preview cache lives; point at an SSD/NVMe mount to keep scrubbing fast on a spinning-disk system (a filesystem mount, not a preference)
THUMB_EXTRACT_MAX_CONCURRENCYscales with coresnohow many previews Crumb builds at once; default is roughly half the CPU cores
THUMB_CACHE_MAX_BYTES21474836480 (20 GiB)yespreview cache size budget; oldest previews are dropped past this (console floors it at 100 MiB)
THUMB_CACHE_TTL_SECONDS2592000 (30 days)yespreview cache age budget (console clamps 1 hour-1 year)

Storage

KeyDefaultNotes
MEDIA_HOST_PATH./_datahost directory bind-mounted into both containers
MEDIA_ROOT/datacontainer-side root; all storage paths must live under it
LIVE_STORAGE_PATH/data/livedefault live bucket
ARCHIVE_STORAGE_PATH/data/archivedefault archive bucket; unset means archive shares the live disk

GPU / motion decode

KeyDefaultNotes
MOTION_HWACCELautoauto probes for NVDEC and falls back to CPU; cuda forces NVDEC, vaapi forces an Intel/AMD iGPU, cpu forces software decode
MAX_GPU_DECODE_SESSIONS4global cap on concurrent NVDEC decode sessions; a camera past the cap decodes on CPU instead of failing
MOTION_VAAPI_DEVICE/dev/dri/renderD128DRI render node used when MOTION_HWACCEL=vaapi; wired in by the docker-compose.vaapi.example.yml overlay, ignored otherwise
RENDER_GID993host render group GID, read by the VAAPI overlay's group_add (not by Crumb itself) so the uid-1001 container user can open the render node; find yours with getent group render

See Hardware decode for enabling this.

API auth

KeyDefaultNotes
JWT_SECRETgeneratedat least 32 bytes; the API refuses to boot on the placeholder value
JWT_EXPIRY_SECONDS86400token lifetime, 24 hours

API server

KeyDefaultNotes
API_BIND0.0.0.0:8080Leave this at 0.0.0.0:8080. Docker already gates host exposure through the compose ports: mapping. Setting 127.0.0.1:8080 here does not lock the API 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.

Export

KeyDefaultNotes
EXPORT_DIR/exportsits own volume, not under the read-only /data mount
EXPORT_TTL_SECONDS86400how long a completed export survives before cleanup

Database backup

See Backups for the full picture.

KeyDefaultNotes
DB_BACKUP_ENABLEDtrue
DB_BACKUP_HOST_PATH./backupsmust be writable by uid 1001
DB_BACKUP_SCHEDULE03:15local wall-clock time
DB_BACKUP_KEEP_DAYS7
DB_BACKUP_KEEP_WEEKS4
DB_BACKUP_KEEP_MONTHS00 disables the monthly tier

Off-host backup copy (optional)

KeyDefaultNotes
BACKUP_OFFSITE_REMOTEemptyan rclone remote:path; leaving this empty makes the optional sidecar idle even if started
BACKUP_OFFSITE_SCHEDULE15 5 * * *5-field cron, not the HH:MM form the main backup uses
BACKUP_OFFSITE_RCLONE_CONF./rclone.confkeep this file out of the repository, same trust level as .env

Alerting

KeyDefaultNotes
ALERT_WEBHOOK_URLemptya generic JSON webhook (Discord/Slack-compatible) for recorder-death paging; empty means silent
CAMERA_OFFLINE_BOOT_GRACE_SECS180holds camera-offline alerts for this long after a recorder restart. Forwarded by the stock docker-compose.yml; set it in .env and restart the api container.
MAINTENANCE_UNTILemptyunix-seconds timestamp to pre-arm a maintenance window at boot. Forwarded by the stock docker-compose.yml; set it in .env and restart the api container.
MOTION_UNHEALTHY_ALERT_SECS180how long a camera's motion detector must stay continuously unhealthy before the recorder raises a system alert. This is alert hysteresis for flaky cameras that blip and self-heal; it delays only the alert, never the fail-open recording safety rail.

Update-available check (issue #7)

KeyDefaultNotes
UPDATE_CHECK_ENABLEDfalseopt-in; when true, 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. false means zero github.com requests, ever. The admin console's "Enable update checks" toggle (Server section) overrides this once set, DB wins over env.

Seed (admin bootstrap)

KeyDefaultNotes
SEED_ADMIN_USERNAMEadmin
SEED_ADMIN_PASSWORDgenerated by setup-env.shplaintext; setup-env.sh generates a memorable passphrase and the api seeds the admin with it by default. Blank it to opt into the browser create-admin wizard instead
SEED_ADMIN_PASSWORD_HASHemptyprecomputed argon2id hash, an alternative to the plaintext var above
SEED_DEFAULT_CAMERASfalsedev-only; keep false in any real deployment

Image source

KeyDefaultNotes
CRUMB_IMAGE_PREFIXghcr.io/badbread/crumbvmspoint at a different registry/namespace
CRUMB_VERSIONlatestpin a specific tag for reproducible upgrades

Frigate integration (optional, bring your own)

All of these are unset by default. See Integrations for the full setup.

KeyDefaultNotes
FRIGATE_MQTT_URLemptyleaving this unset disables the Frigate MQTT provider (no broker connection, no background task). It does not touch Crumb's other detection paths, Home Assistant motion sources and the crumb-alpr LPR ingest work independently of it.
FRIGATE_MQTT_USER / FRIGATE_MQTT_PASSWORD / FRIGATE_MQTT_PASSWORD_B64emptybroker auth, only if required
FRIGATE_MQTT_PREFIXfrigate
FRIGATE_API_BASEemptyfallback; the admin console setting overrides it
FRIGATE_MIN_SCORE0.3detection confidence floor
FRIGATE_CATCHUP_HOURS24how far back to backfill on startup

Crumb-native LPR worker (optional, alpr profile)

Crumb's own local plate OCR (fast-alpr), no cloud and no third-party agent. It's opt-in: nothing runs until you start the alpr compose profile with docker compose --profile alpr up -d --build crumb-alpr. First enable LPR and mint an ingest token in Admin → LPR (Rotate ingest token), then set the keys below. One worker instance per camera. See Integrations for the full setup.

KeyDefaultNotes
LPR_INGEST_TOKENemptythe rotated token from Admin → LPR; the worker authenticates its POST /lpr/reads calls with it
LPR_CAMERA_IDemptythe Crumb camera UUID this worker reads
LPR_RTSP_URLemptythe go2rtc restream RTSP for that camera, e.g. rtsp://<go2rtc-user>:<go2rtc-pass>@recorder:8554/<stream-name>
LPR_MIN_CONFIDENCE0.80drop reads below this mean OCR confidence
LPR_SAMPLE_FPS5analysis frame rate while a pass is active
LPR_API_BASEhttp://api:8080override only if the worker runs off-host (mapped to the worker's CRUMB_API_BASE)
LPR_LOG_LEVELinfoworker log verbosity

The worker reads several more tuning knobs (detector/OCR model names, motion gating, pass timing) with sensible defaults, see services/alpr-worker/worker.py and its README if you need to tune them.

Home Assistant (optional, off by default)

A self-hosted integration, off until you enable it. Normally you configure this in the admin console (Detection & clips → Home Assistant), which stores it in the database, and the DB value wins. The keys below are only a read-time fallback used when the matching DB field is empty. Use a long-lived token from a dedicated non-admin HA user.

KeyDefaultNotes
HA_BASE_URLemptye.g. http://<home-assistant-host>:8123; fallback for the console's Home Assistant base URL
HA_TOKENemptya long-lived access token; a secret. Prefer HA_TOKEN_FILE in production.
HA_TOKEN_FILEemptypath to a Docker-secret file holding the token, e.g. /run/secrets/ha_token; read in preference to HA_TOKEN

All three are forwarded by the stock docker-compose.yml into both the api and recorder containers: set them in .env and restart both. The console path (which writes to the database) is still the supported way to configure Home Assistant, and the DB value wins whenever both are set.