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, runsSELECT 1),/metrics(Prometheus exposition).- everything else - the dashboard, with unknown client-routed paths falling back
to
index.htmlso deep links return200.
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
| Port | Service |
|---|---|
| 8000 | Heldar Core HTTP API + dashboard (HELDAR_API_HOST / HELDAR_API_PORT) |
| 5173 | Vite dev server (development only; not used in the single-binary deploy) |
| 8554 / 8888 / 8889 | MediaMTX RTSP / HLS / WebRTC |
| 9997 | MediaMTX 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 anX-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.
| Var | Default | Meaning |
|---|---|---|
HELDAR_DATABASE_URL | sqlite://./data/heldar.db | SQLite only; a non-sqlite URL is rejected at boot |
HELDAR_DATA_DIR | ./data | root for the DB and media subdirs |
HELDAR_RECORDINGS_DIR / CLIPS_DIR / SNAPSHOTS_DIR / FRAMES_DIR | under ./data | media roots (created at boot) |
HELDAR_MAX_RECORDINGS_GB | 20 | soft footprint cap; oldest unlocked segments are pruned past it |
HELDAR_MIN_FREE_DISK_GB | 5 | hard host-protection floor; prunes unlocked segments while free space is below it |
HELDAR_MAX_DB_GB | 4 | caps 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.