Environment reference¶
What this is¶
The knobs that live in .env (not the Settings UI): encryption keys, public URL, ports, Celery, metrics, feature toggles.
Why .env vs Settings¶
Secrets and process-level config must be available before the app boots. Policy that belongs in the database (timezone, force 2FA, console timeouts, schedules) lives under Settings so it rides along with self-backup.
Full commented catalog: .env.example in the repo. Copy to .env.
Compose injects matching keys into web and celery-worker. Caddy mainly needs PIHERDER_HOSTNAME.
Required¶
| Variable | Purpose |
|---|---|
PIHERDER_MASTER_KEY | Fernet key — SSH keys, integration tokens, template secrets, VAPID private |
SECRET_KEY | Session / JWT signing — long random in production (not the compose default). Web refuses to start if the value looks weak/default unless PIHERDER_ALLOW_INSECURE=true or DEMO_MODE (lab only) |
Generate master key:
Public URL / TLS¶
| Variable | Purpose |
|---|---|
PIHERDER_HOSTNAME | Caddy site hostname; must match cert SANs; WebAuthn RP ID |
PIHERDER_PUBLIC_URL | Canonical origin (include :8443 if mapped); HTTPS enables Secure cookies; OIDC redirect base {PUBLIC_URL}/auth/oidc/callback; CSP upgrade-insecure-requests when https |
PIHERDER_CSP | true (default) — send Content-Security-Policy. Scripts are self-hosted (compiled Tailwind, no Play CDN, no unsafe-eval). connect-src is 'self' plus PIHERDER_PUBLIC_URL / its wss: — no wildcard ws:/wss:. Inline scripts get a per-request nonce (script-src drops 'unsafe-inline'). script-src-attr is 'none' (no onclick). Styles stay 'unsafe-inline'. |
PIHERDER_CSP_REPORT_ONLY | false (default) — if true, send Report-Only CSP instead of enforcing. Public demo compose sets this true |
PIHERDER_CSP_ENFORCE | false (default) — set true on a demo host only when you want that public site to enforce the nonce policy. Until then, demo mode stays Report-Only |
PIHERDER_SSH_CONSOLE | false (default) — master enable for web SSH (operator+ / 2FA; in-app only). Not a Settings checkbox. Per-host Console mux is Edit → Features (console_mux_enabled), not an env flag. |
PIHERDER_HOST_FILES | false (default) — master enable for host Files (operator+; jailed SFTP). Not a Settings checkbox. Public demo uses a canned tree instead (leave this off on the demo VPS). Host Files. Optional PIHERDER_HOST_FILES_MAX_BYTES locks the cap (otherwise Settings → Files, default 512 MiB, ceiling 32 GiB) — do not inject a compose default. |
PIHERDER_SERVICE_MIGRATE | true (default) — master enable for Docker Move to another host… (stop → copy → dest up → DNS/NPM). Set false to turn Move off, then recreate web. Job runs on celery-worker (v1.5); recycle web is safe. Host lock / unlock is always available (operator+). Optional leftover: leave stopped, compose down, or remove source project + named volumes (extra ack). Optional Adopt into fabric on preflight (default off). A finished Move has no Undo. Hosted /mcp start_move calls POST /api/v1/servers/{id}/moves with confirm: true and leaves the source stopped. POST /jobs with service_migrate is refused. There is no MCP undo. The Home Assistant card uses that route when health service_migrate is true. Demo never copies. After toggling, recreate web (and celery-worker if the image changed). Move a service. |
Idle, max session, concurrency, ticket, hold, bind, revalidate, scrollback, grant, and 2FA factor knobs live in Settings (Console + Security). Set a non-empty env value to lock that knob (air-gap). Bundled compose does not inject defaults for these, or Settings cannot apply. Names if you lock:
| Optional lock | Settings default | Locks |
|---|---|---|
PIHERDER_SSH_CONSOLE_REQUIRE_2FA_EVERY_SHELL | false | Security — every new shell |
PIHERDER_SSH_CONSOLE_ALLOW_BACKUP_CODES | false | Security — backup codes |
PIHERDER_SSH_CONSOLE_PREFER_PASSKEY | true | Security |
PIHERDER_SSH_CONSOLE_REQUIRE_PASSKEY | false | Security |
PIHERDER_SSH_CONSOLE_BIND_IP | true | Console |
PIHERDER_SSH_CONSOLE_BIND_DEVICE | true | Console |
PIHERDER_SSH_CONSOLE_REVALIDATE_SEC | 10 | Console |
PIHERDER_SSH_CONSOLE_TICKET_SEC | 60 | Console |
PIHERDER_SSH_CONSOLE_IDLE_SEC | 900 | Console |
PIHERDER_SSH_CONSOLE_MAX_SEC | 3600 | Console |
PIHERDER_SSH_CONSOLE_MAX_PER_USER | 4 | Console |
PIHERDER_SSH_CONSOLE_MAX_GLOBAL | 20 | Console |
PIHERDER_SSH_CONSOLE_SCROLLBACK | 2000 | Console |
PIHERDER_SSH_CONSOLE_HOLD_SEC | 0 | Console |
PIHERDER_SSH_CONSOLE_GRANT_MIN | 10 | Security — grant minutes |
PIHERDER_SSH_CONSOLE_PRIVILEGED_ROLE | admin | Console — who may Connect as… privileged (admin or operator) |
PIHERDER_SSH_CONSOLE_AUDIT_MODE | off | Console — off / commands / commands_output |
PIHERDER_SSH_CONSOLE_AUDIT_REQUIRED | false | Console — force command recording on every live shell |
PIHERDER_SSH_CONSOLE_AUDIT_RETENTION_DAYS | 14 | Console — drop transcript bodies after N days (1–90) |
| Variable | Purpose |
|---|---|
PIHERDER_BACKUP_VANISHED_RETRIES | 1 — extra rsync attempts on vanished files |
PIHERDER_BACKUP_VANISHED_RETRY_DELAY_SEC | 5 — delay before vanished retry |
PIHERDER_BACKUP_VANISHED_SOFT_OK | true — treat final vanished exit as soft success |
PIHERDER_DEMO_MODE | false (default) — demo sandbox (banner, hard blocks, canned jobs). Leave false on real fleets |
PIHERDER_DEMO_EMAIL | Shared demo login email when seeding (demo mode only; public demo uses demo@hacknow.info) |
PIHERDER_DEMO_PASSWORD | Shared demo login password when seeding (demo mode only). Public demo: keep in sync with Public demo (password may rotate; wiki is source of truth) |
PIHERDER_TURNSTILE_SITE_KEY | Cloudflare Turnstile site key (empty = widget off) |
PIHERDER_TURNSTILE_SECRET_KEY | Turnstile secret; required with site key for login verification |
Public try-the-demo: Public demo. Maintainer VPS runbook (repo): DEMO_SITE.md.
Host paths¶
| Variable | Default | Purpose |
|---|---|---|
PIHERDER_BACKUP_HOST_PATH | ./backups | Host side of /backups mount |
PIHERDER_NMAP_VULN_PATH | ./piherder_nmap_vuln | Host dir for LAN Discovery vuln pack (profile nmap) |
Other mounts fixed in docker-compose.yml: piherder_backups, piherder_data, certs.
Database / Redis / Celery¶
| Variable | Default idea |
|---|---|
DATABASE_URL | postgresql://piherder:piherder@db:5432/piherder |
CELERY_BROKER_URL | redis://redis:6379/0 |
CELERY_RESULT_BACKEND | same Redis |
CELERY_CONCURRENCY | 2 pool slots (compose → celery-worker) |
PIHERDER_SERVER_LOCK_TTL | 7200 backup mutex TTL |
PIHERDER_EXCLUSIVE_HOST_WAIT_SEC | Unset — Settings → General → Jobs (default 30 minutes). When this env var is set it locks that field. Probe every 30s. Floor 30, ceiling 86400 |
PIHERDER_INSTANCE_NAME | Unset — Settings → General → Instance. When set, locks the header wordmark. Empty Settings name keeps Pi / Herder. The mark image stays |
PIHERDER_ACCENT | Unset — same card. #RRGGBB locks the accent (--color-accent and the subtle accent background). Primary red #e60012 stays. Demo ignores name, accent, and the Catalog checkbox. Hiding Catalog has no env var |
REDIS_URL | Optional alias used in some deploy notes — broker/result URLs are authoritative |
LAN Discovery (nmap) — opt-in¶
Default docker compose up does not start the nmap worker. See LAN Discovery · install.
Worker fence (compose-owned — usually not in .env)¶
| Where | PIHERDER_NMAP_WORKER | Meaning |
|---|---|---|
| web + main celery-worker | 0 (hard-coded in docker-compose.yml x-piherder-app-env) | Tasks refuse to run nmap (worker_guard) |
| celery-worker-nmap | 1 (overrides anchor) | Only allowed scan / vuln-pack executor |
Dockerfile.nmap | 1 (ENV) | Image default for the nmap worker |
You normally do not set PIHERDER_NMAP_WORKER in .env — compose owns it. Never add -Q nmap to the main celery-worker command.
Task code also refuses when the nmap binary is missing (main image has no nmap).
Optional overrides (.env / shell)¶
| Variable | Default idea | Purpose |
|---|---|---|
PIHERDER_NMAP_VULN_PATH | ./piherder_nmap_vuln | Host bind for vuln pack volume |
PIHERDER_NMAP_VULN_ROOT | /var/lib/piherder/nmap-vuln | In-container path (web :ro, nmap worker rw) |
PIHERDER_NMAP_IMAGE | piherder:nmap-local | Image tag for profile nmap |
PIHERDER_NMAP_DATABASE_URL | loopback Postgres | Host-network worker → 127.0.0.1:5432 |
PIHERDER_NMAP_REDIS_URL | loopback Redis | Host-network worker → 127.0.0.1:6379 |
docker build -f Dockerfile.nmap -t piherder:nmap-local .
docker compose --profile nmap up -d celery-worker-nmap
Agents (MCP)¶
The herder has no MCP env var and no MCP port. PIHERDER_URL and PIHERDER_TOKEN belong on the computer that runs uvx piherder-mcp. That program does not use the hosted browser sign-in. A client pointed at https://your-herder/mcp can paste a bearer or sign in in the browser, and it does not need these two variables. See Agents (MCP).
Auth / sessions / cookies¶
| Variable | Purpose |
|---|---|
ALLOW_OPEN_REGISTRATION | Default false. Empty DB allows first admin via Register; then closed unless true (later open-reg users are operator) |
COOKIE_SECURE | Empty = auto (Secure when PIHERDER_PUBLIC_URL is https://…); true/false to force |
PIHERDER_ALLOW_INSECURE | Default false. If true, allow boot with a weak/default SECRET_KEY (lab only) |
ACCESS_TOKEN_EXPIRE_MINUTES | Session JWT lifetime (default 10080 = 7 days) |
ALGORITHM | JWT algorithm (default HS256) |
TRUSTED_DEVICE_DAYS | 2FA “trust this device” cookie age (default 30) |
PIHERDER_DISABLE_AUTH_RATE_LIMIT | Set 1/true only for E2E/lab — disables login/2FA/register rate limits (never in production) |
AVATAR_MAX_BYTES | Max avatar upload size (default 2 MiB) |
Metrics / CORS / webhooks¶
| Variable | Purpose |
|---|---|
METRICS_TOKEN | Bearer for GET /metrics — set in production (empty = open scrape on app port) |
METRICS_BACKUP_STALE_HOURS | Stale backup gauge (default 36) |
CORS_ORIGINS | Exact browser origins for /api/v1 (empty = off) |
PIHERDER_TRUSTED_PROXY_CIDRS | CIDRs whose TCP peer may supply CF-Connecting-IP / X-Forwarded-For / X-Real-IP. Empty = never trust those headers (peer only). Compose default: RFC1918 + loopback so bundled Caddy is trusted |
WEBHOOK_URL / WEBHOOK_NUMBER | Fallback outbound webhook (e.g. Signal via n8n) when Settings → Alerts has no URL — Alerts |
WEBHOOK_RECIPIENTS | Optional JSON list of recipients for some webhook paths |
VAPID_* | Optional pin; auto-gen is default |
PIHOLE_URL | Dashboard quick-link (legacy single URL; multi Pi-hole lives under Catalog) |
PIHERDER_UPDATE_CHECK | Default true. Check GitHub Releases for a newer version (About + banner). Set false for air-gapped |
PIHERDER_UPDATE_CHECK_TTL_HOURS | Cache TTL for update check (default 12) |
Herder schedule (optional seed)¶
| Variable | Purpose |
|---|---|
HERDER_BACKUP_SCHEDULE | Optional cron seed for self-backup; Settings UI / DB wins after first save |
Inside-container paths (rarely change)¶
BACKUP_ROOT, HERDER_BACKUP_ROOT, DATA_ROOT, DEFAULT_DOCKER_BASE, …
Related¶
.env.example— full commented catalog- Install — nmap worker
- LAN Discovery
- Volumes
- ADMIN.md — production env
- v1.4.0 QA (maintainer
docs/— not this wiki) - v1.3.0 QA (maintainer
docs/— not this wiki) - v1.2.0 QA / sign-off (prior)