Skip to main content

Deploy

Heldar is built to run as one binary at one URL. The composing server (heldar-server, the heldar-core binary) serves the JSON API, the recorded media, the metrics/health endpoints, and the built dashboard from a single process.

Three ways to run it, in order of effort:

  • Docker (pull & run): docker compose -f deploy/compose.yml up -d — or the quickstart one-liner. Prebuilt open images, no toolchain.
  • Native binary — build from source and run it (this page).
  • Flashed appliance — a native-systemd OS image for a dedicated DVR box (no Docker on the appliance; see infra/systemd/ in the repo).

One binary, one URL

Build the dashboard, then point the server at it with HELDAR_WEB_DIR:

cd apps/web && npm install && npm run build # writes apps/web/dist
# in .env:
HELDAR_WEB_DIR=./apps/web/dist

When HELDAR_WEB_DIR is set and the directory exists, the server serves the SPA as a fallback. The explicit routes take precedence and the SPA is only a fallback for everything else:

  • /api/* - the JSON API.
  • /media/recordings, /media/clips, /media/snapshots, /media/playback, /media/archives - static media served from the data dir.
  • /healthz (liveness), /readyz (readiness, runs SELECT 1), /metrics (Prometheus exposition).
  • everything else - the dashboard, with unknown client-routed paths falling back to index.html so deep links return 200.

If HELDAR_WEB_DIR is unset it defaults to apps/web/dist relative to the binary's working directory. When neither exists, the server runs API-only and logs that the dashboard is not served.

Ports

PortService
8000Heldar Core HTTP API + dashboard (HELDAR_API_HOST / HELDAR_API_PORT)
5173Vite dev server (development only; not used in the single-binary deploy)
8554 / 8888 / 8889MediaMTX RTSP / HLS / WebRTC
9997MediaMTX control API (loopback)

Live view is published to MediaMTX by a kernel-owned, supervised ffmpeg per camera; camera credentials stay in the kernel and never reach MediaMTX or the browser, which only ever sees the non-credentialed HLS/WebRTC/RTSP URLs. The live-transcode engine defaults to HELDAR_LIVE_TRANSCODE_ENGINE (software) and can be switched at runtime (software / VAAPI / NVENC) from the System page or GET/PUT /api/v1/system/transcode.

Authentication

Auth and RBAC are opt-in via HELDAR_AUTH_ENABLED (default false).

  • false - open API, suitable for a single-tenant LAN appliance. The admin surface is reachable without a token and acts as admin.
  • true - every request needs a session (login) or an X-API-Key. Five roles are enforced (admin / manager / guard / viewer / integration) across capabilities, and every mutation is written to an immutable audit log. On first run with no users, an admin is seeded from the bootstrap env.

When auth is enabled, a defence-in-depth floor rejects every /api/v1/* request before it reaches its handler unless the caller is authenticated — the only exceptions are /api/v1/auth/login and /api/v1/auth/logout. Each handler still enforces its own role on top of the floor, so an authenticated-but-under- privileged caller is refused at the handler. The unauthenticated liveness and metrics endpoints (/healthz, /readyz, /metrics) sit outside /api/v1 and are unaffected.

Sessions use an HttpOnly, SameSite=Strict cookie. Set HELDAR_AUTH_COOKIE_SECURE=true behind TLS (keep false for plain-HTTP LAN or overlay access). Tune the session lifetime with HELDAR_SESSION_TTL_HOURS (default 12), and optionally expire idle sessions with HELDAR_SESSION_IDLE_TIMEOUT_MIN (default 0, i.e. no idle timeout).

Brute-force lockout is on by default: an account is locked after HELDAR_LOGIN_MAX_FAILURES (5) consecutive failed logins for HELDAR_LOGIN_LOCKOUT_MIN (15) minutes — refused even with the correct password, auto-unlocking when the window passes (an admin can clear it sooner via POST /api/v1/users/{id}/unlock).

Set HELDAR_AUTH_ENABLED=true for any multi-user or networked deployment.

:::tip Internet-exposed? Harden it first. For a deployment reachable from the public internet, the kernel fails loud on an unsafe configuration — it refuses to boot with auth off behind a rendezvous, and warns (or refuses, under HELDAR_STRICT_PROD=true) on a non-Secure cookie, no idle timeout, or plaintext camera credentials. Work through the Production hardening checklist — auth, TLS, camera-credential encryption (HELDAR_SECRET_KEY), and an optional Cloudflare Turnstile login challenge — before going live. :::

Storage and the data dir

Heldar uses SQLite only (WAL journal, embedded migrations). The default URL is sqlite://./data/heldar.db.

VarDefaultMeaning
HELDAR_DATABASE_URLsqlite://./data/heldar.dbSQLite only; a non-sqlite URL is rejected at boot
HELDAR_DATA_DIR./dataroot for the DB and media subdirs
HELDAR_RECORDINGS_DIR / CLIPS_DIR / SNAPSHOTS_DIR / FRAMES_DIRunder ./datamedia roots (created at boot)
HELDAR_MAX_RECORDINGS_GB20soft footprint cap; oldest unlocked segments are pruned past it
HELDAR_MIN_FREE_DISK_GB5hard host-protection floor; prunes unlocked segments while free space is below it
HELDAR_MAX_DB_GB4caps the heldar.db metadata DB itself; space is reclaimed online via incremental auto_vacuum

Recordings stay on the local disk and are served from there; nothing is pushed to a cloud by default. Evidence-locked segments are never deleted by retention. All three limits are also runtime-tunable without a restart — from the dashboard System page ("Recording limit" and "Database limit" panels) or via GET/PUT /api/v1/system/retention and GET/PUT /api/v1/system/db (PUT is admin-only); a stored override shadows the env value, which remains the default. A DB created before the cap existed is converted to incremental auto_vacuum in the background at boot (HELDAR_DB_AUTOVACUUM_CONVERT, default true; never boot-blocking) or on demand via POST /api/v1/system/db/convert.

Camera credentials live in this SQLite DB. Set HELDAR_SECRET_KEY (base64 of 32 bytes, e.g. openssl rand -base64 32) to encrypt them at rest with AES-256-GCM; unset keeps plaintext for a trusted LAN appliance. Existing plaintext credentials are sealed on the next boot — see the Production hardening guide.

CORS

HELDAR_CORS_ORIGINS controls cross-origin access. Empty or * allows all origins; otherwise it restricts to the configured list (the default allows the Vite dev server). In a single-binary deploy the dashboard is same-origin, so CORS is mostly relevant when a separate frontend or integration calls the API.

Operating a deployment

For sizing, commissioning, observability, and remote access — including bringing your own STUN/TURN via HELDAR_WEBRTC_ICE_SERVERS — see the Operate hub.