mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
E1. Two halves of one correction. `.env.example` still said backup objects older than BACKUP_RETENTION_DAYS are pruned, which no code does, so a self-hoster reading that file alone still expected the worker to keep the bucket tidy. It now carries the same sentence as the production example. And that sentence overreached in the other direction: "the worker never deletes an object" is not true of `_healthcheck/<ts>.bin`, the 1-byte key the Test-connection button writes and then deletes. The delete swallows its own failure, so an operator who granted exactly the three documented actions leaves one orphan per press and is never told. Both files now say "a BACKUP object", and name the one key and what happens without DeleteObject on that prefix.
537 lines
28 KiB
Bash
537 lines
28 KiB
Bash
# =============================================================================
|
|
# HealthLog -- production .env example
|
|
# =============================================================================
|
|
# Mirrors `scripts/env-manifest.json`. The `pnpm check-env --file
|
|
# .env.production.example` CI step (see `.github/workflows/env-check.yml`)
|
|
# enforces every variable declared `required: true` in the manifest has a
|
|
# non-empty placeholder here, so a contributor who introduces a new
|
|
# required var without updating this file fails the workflow.
|
|
#
|
|
# Placeholders are syntactic only -- replace with real secrets before
|
|
# deploying. The check-env presence rule treats any non-empty,
|
|
# non-whitespace string as "set", so the `<placeholder>` style below
|
|
# satisfies the manifest without leaking a usable credential.
|
|
#
|
|
# Generate the two crypto secrets locally with:
|
|
# $ openssl rand -hex 32
|
|
# =============================================================================
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Core -- required for boot
|
|
# -----------------------------------------------------------------------------
|
|
DATABASE_URL="postgresql://healthlog:<password>@db:5432/healthlog?schema=public"
|
|
POSTGRES_PASSWORD="<openssl rand -hex 32>"
|
|
NEXT_PUBLIC_APP_URL="https://healthlog.example.com"
|
|
APP_URL="https://healthlog.example.com"
|
|
ENCRYPTION_KEY="<openssl rand -hex 32>"
|
|
API_TOKEN_HMAC_KEY="<openssl rand -hex 32>"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Withings -- optional integration
|
|
# -----------------------------------------------------------------------------
|
|
# The Withings OAuth client id/secret are per-user: each user registers their
|
|
# own Withings developer app and pastes the pair into Settings (stored
|
|
# encrypted in the DB). There is therefore no WITHINGS_CLIENT_ID/SECRET.
|
|
# WITHINGS_WEBHOOK_SECRET=""
|
|
# Overrides the derived "<NEXT_PUBLIC_APP_URL>/api/withings/callback". Set it
|
|
# when the callback registered with Withings differs from the app's own
|
|
# public URL -- e.g. a second hostname in front of the same instance.
|
|
# WITHINGS_REDIRECT_URI=""
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Google Health Connect -- optional integration (per-user BYO-keys)
|
|
# -----------------------------------------------------------------------------
|
|
# Like Fitbit, the client id/secret are per-user and live encrypted in the DB.
|
|
# This only overrides the derived
|
|
# "<NEXT_PUBLIC_APP_URL>/api/google-health/callback"; it must stay same-origin
|
|
# with NEXT_PUBLIC_APP_URL or the handshake is refused.
|
|
# GOOGLE_HEALTH_REDIRECT_URI=""
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Telegram -- optional notification channel
|
|
# -----------------------------------------------------------------------------
|
|
# Shared secret for the Telegram bot webhook. It is both the path segment the
|
|
# bot posts to and the value compared against the update header, so set it
|
|
# before installing the webhook from Settings -> Notifications. Without it the
|
|
# settings page cannot register the webhook and inbound updates are refused.
|
|
# TELEGRAM_WEBHOOK_SECRET="<openssl rand -hex 32>"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Nightscout -- optional integration
|
|
# -----------------------------------------------------------------------------
|
|
# Nightscout instances on your own network that the per-user connection form
|
|
# should accept, comma-separated (e.g. "http://nightscout.lan:1337"). The
|
|
# validator refuses private and internal hosts by default; this is the opt-in
|
|
# for a self-hosted Nightscout that is not reachable from the public internet.
|
|
# NIGHTSCOUT_PRIVATE_ORIGINS=""
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# WHOOP -- optional integration
|
|
# -----------------------------------------------------------------------------
|
|
# The WHOOP app client id/secret are per-user BYO-keys: each user registers
|
|
# their own WHOOP developer app and pastes the client id/secret into Settings
|
|
# (stored encrypted in the DB). There is therefore no WHOOP_CLIENT_ID/SECRET.
|
|
#
|
|
# WHOOP_WEBHOOK_SECRET is the instance-level secret carried as the trailing
|
|
# path segment of the webhook URL AND used to verify the WHOOP HMAC body
|
|
# signature. WHOOP_REDIRECT_URI overrides the derived
|
|
# `${NEXT_PUBLIC_APP_URL}/api/whoop/callback` when the public URL differs.
|
|
# WHOOP_WEBHOOK_SECRET="<openssl rand -hex 32>"
|
|
# WHOOP_REDIRECT_URI=""
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Fitbit -- optional integration (classic Web API, per-user BYO-keys)
|
|
# -----------------------------------------------------------------------------
|
|
# Each user registers their own app at https://dev.fitbit.com (self-serve, no
|
|
# brand verification) and pastes the OAuth Client ID + Client Secret into
|
|
# Settings (stored encrypted in the DB). There is therefore no
|
|
# FITBIT_CLIENT_ID/SECRET here. The OAuth flow uses Authorization Code + PKCE.
|
|
#
|
|
# FITBIT_REDIRECT_URI overrides the derived
|
|
# `${NEXT_PUBLIC_APP_URL}/api/fitbit/callback` when the public URL differs; it
|
|
# must exactly match the Callback URL registered in the dev.fitbit.com app.
|
|
# FITBIT_REDIRECT_URI=""
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Polar AccessLink -- optional integration (full OAuth, shared app from env)
|
|
# -----------------------------------------------------------------------------
|
|
# Register an app at https://admin.polaraccesslink.com and set its redirect URI
|
|
# to `${NEXT_PUBLIC_APP_URL}/api/polar/callback`. Both vars together enable the
|
|
# Polar connect button; set neither to leave the integration disabled. Polar
|
|
# access tokens do not expire and carry no refresh token. POLAR_REDIRECT_URI
|
|
# overrides the derived callback when the public URL differs.
|
|
# POLAR_CLIENT_ID=""
|
|
# POLAR_CLIENT_SECRET=""
|
|
# POLAR_REDIRECT_URI=""
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Oura Cloud v2 -- optional integration (full OAuth, shared app from env)
|
|
# -----------------------------------------------------------------------------
|
|
# Register an app at https://cloud.ouraring.com/oauth/applications and set its
|
|
# redirect URI to `${NEXT_PUBLIC_APP_URL}/api/oura/callback`. Both vars together
|
|
# enable the Oura connect button. Oura uses refresh tokens; the sync refreshes
|
|
# reactively on a 401. OURA_REDIRECT_URI overrides the derived callback.
|
|
# OURA_CLIENT_ID=""
|
|
# OURA_CLIENT_SECRET=""
|
|
# OURA_REDIRECT_URI=""
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Strava -- optional integration (full OAuth, shared app from env)
|
|
# -----------------------------------------------------------------------------
|
|
# Register an app at https://www.strava.com/settings/api and set its
|
|
# "Authorization Callback Domain" to your instance host. Both vars together
|
|
# enable the Strava connect button; set neither to leave the integration
|
|
# disabled. Per-user BYO client id/secret in Settings take precedence over these
|
|
# shared env values. Strava caps every new app at single-player mode (athlete
|
|
# capacity 1) until you request a capacity increase — fine for a personal
|
|
# self-host. Strava rotates its refresh token on every refresh; the sync
|
|
# refreshes reactively on a 401. STRAVA_REDIRECT_URI overrides the derived
|
|
# `${NEXT_PUBLIC_APP_URL}/api/strava/callback`.
|
|
# STRAVA_CLIENT_ID=""
|
|
# STRAVA_CLIENT_SECRET=""
|
|
# STRAVA_REDIRECT_URI=""
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# OIDC SSO login -- optional (self-hosted identity provider)
|
|
# -----------------------------------------------------------------------------
|
|
# Register a confidential OIDC client for your IdP (Authentik, Keycloak,
|
|
# Authelia, Google Workspace, etc.) with redirect URI
|
|
# `${NEXT_PUBLIC_APP_URL}/api/auth/oidc/callback`. All three of ISSUER_URL/
|
|
# CLIENT_ID/CLIENT_SECRET together enable the "Sign in with SSO" button on
|
|
# the login page; set none to leave it disabled. First-time sign-in
|
|
# auto-provisions (or links, by verified email) a HealthLog account. OIDC_ONLY
|
|
# hides password/passkey login entirely once set to "true" -- but only takes
|
|
# effect when the three vars above are also fully set, so a typo can't lock
|
|
# every user out. Enforced server-side, not just a hidden button. The native
|
|
# iOS client signs in through the native OIDC SSO handoff (the app opens the
|
|
# login URL in an in-app browser and completes at a custom-scheme callback), so
|
|
# OIDC_ONLY="true" is supported for iOS -- but only on the app build that ships
|
|
# that flow. An older password/passkey-only iOS build is locked out of sign-in
|
|
# by OIDC_ONLY; confirm the app version before enabling it on a deployment with
|
|
# active iOS users.
|
|
# Serve the issuer over HTTPS wherever possible: sign-in material (the
|
|
# authorization code, the token-endpoint response carrying the ID token)
|
|
# crosses this connection. Plain HTTP is defensible only on a fully trusted
|
|
# private network (IdP and app on the same box, a Tailscale mesh, a VPN).
|
|
# OIDC_ISSUER_URL=""
|
|
# OIDC_CLIENT_ID=""
|
|
# OIDC_CLIENT_SECRET=""
|
|
# OIDC_SCOPES="openid email profile"
|
|
# OIDC_BUTTON_LABEL="Single Sign-On"
|
|
# OIDC_ONLY="false"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Web Push -- VAPID keys (optional; the admin panel is the easier path)
|
|
# -----------------------------------------------------------------------------
|
|
# Web Push needs NO Apple account and no paid service -- just a VAPID keypair.
|
|
# The EASIEST path is the admin panel: /admin -> Web Push VAPID -> "Generate
|
|
# keys" mints and stores the pair for you (private key encrypted at rest). Use
|
|
# these env vars only if you prefer env-config over the DB. The loader reads
|
|
# DB first, then env (src/lib/notifications/vapid-config.ts). These three
|
|
# names are the only ones it reads, and all three are on docker-compose.yml's
|
|
# `environment:` whitelist, so a value set here reaches the container.
|
|
# VAPID_PUBLIC_KEY=""
|
|
# VAPID_PRIVATE_KEY=""
|
|
# VAPID_SUBJECT="mailto:you@example.com"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# APNs -- iOS push (optional, all-or-none)
|
|
# -----------------------------------------------------------------------------
|
|
# Only the native iOS app uses APNs, and it needs a paid Apple Developer
|
|
# Program membership. Web Push (above) needs NO Apple account -- it covers
|
|
# every browser and the installed PWA on iPhone. Leave this whole block unset
|
|
# unless you ship the native app. Key-source precedence when more than one is
|
|
# set: APNS_KEY_B64 > APNS_KEY > APNS_KEY_FILE.
|
|
# APNS_KEY_ID=""
|
|
# APNS_TEAM_ID=""
|
|
# APNS_BUNDLE_ID=""
|
|
# APNS_KEY_B64="" # PEM body base64-encoded -- recommended (escapes newlines)
|
|
# APNS_KEY="" # OR APNS_KEY (raw PEM) OR APNS_KEY_FILE -- one satisfies the manifest
|
|
# APNS_KEY_FILE=""
|
|
# APNS_CRITICAL_ENTITLEMENT="" # "true" only if your build has Apple's Critical Alerts entitlement; else urgent = time-sensitive
|
|
# APNS_PRODUCTION="" # "true" forces the production APNs gateway; normally derived from NODE_ENV
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# SMTP -- email notification channel (optional, all-or-none)
|
|
# -----------------------------------------------------------------------------
|
|
# Operator SMTP transport for the email channel. SMTP_HOST + SMTP_PORT +
|
|
# SMTP_FROM together enable it; SMTP_USER/SMTP_PASS are optional (omit for an
|
|
# unauthenticated relay). SMTP_SECURE=true uses implicit TLS (port 465);
|
|
# unset/false uses STARTTLS (port 587). Per-user recipient address is set in
|
|
# Settings -> Notifications, not here. Missing the core trio disables the
|
|
# channel silently. Listed in docker-compose.yml's environment whitelist so
|
|
# these reach the container.
|
|
# SMTP_HOST=""
|
|
# SMTP_PORT="587"
|
|
# SMTP_FROM="HealthLog <noreply@example.com>"
|
|
# SMTP_USER=""
|
|
# SMTP_PASS=""
|
|
# SMTP_SECURE="false"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Session cookie Secure flag -- the most common self-host snag
|
|
# -----------------------------------------------------------------------------
|
|
# Unset, the flag tracks NODE_ENV, so a production image sets `Secure` and a
|
|
# browser on plain HTTP silently drops the session cookie: login appears to
|
|
# succeed and every following request is anonymous. Set it to false ONLY on a
|
|
# LAN, Tailscale or VPN-only host that deliberately serves HTTP. Behind a TLS
|
|
# reverse proxy, leave it alone.
|
|
# SESSION_COOKIE_SECURE="false"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Deploy webhook -- Coolify -> HealthLog auto-deploy feedback
|
|
# -----------------------------------------------------------------------------
|
|
# DEPLOY_WEBHOOK_SECRET=""
|
|
# Optional: deployment-dashboard URL (e.g. the Coolify UI) linked in
|
|
# deploy-failure notifications. When unset the notification omits the link.
|
|
# DEPLOY_LOGS_URL=""
|
|
# Optional replay hardening: require a fresh X-Deploy-Webhook-Timestamp
|
|
# header (unix seconds / ms or ISO-8601, +/- 5 minutes) on every webhook
|
|
# call. Leave unset for Coolify's stock sender (it attaches no
|
|
# timestamp); see docs/ops/deploy.md.
|
|
# DEPLOY_WEBHOOK_REQUIRE_TIMESTAMP="false"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# TLS leaf pin monitor -- alarm when the pinned leaf certificate rotates
|
|
# -----------------------------------------------------------------------------
|
|
# The native client SPKI-pins the served TLS leaf. Set this to the
|
|
# known-good leaf pin(s) so the monitor can alarm on a rotation. The value
|
|
# is base64(sha256(DER subjectPublicKeyInfo)); comma-separate to hold both
|
|
# the current and the next pin during a dual-pin renewal window. See
|
|
# docs/ops/tls-cert-pin.md for extraction + the re-pin runbook.
|
|
# TLS_LEAF_SPKI_PINS=""
|
|
|
|
# Local-AI SSRF opt-in (optional). The local AI provider rejects private/
|
|
# internal hosts by default. Set to an exact host allowlist (comma-separated,
|
|
# e.g. "ollama.lan,10.0.0.5") or "true" for any private host — intended for a
|
|
# self-hosted Ollama / LM Studio endpoint. See docs/integrations/ai-providers.md.
|
|
# ALLOW_LOCAL_AI_PRIVATE_HOSTS=""
|
|
|
|
# Daily-briefing output-token ceiling (optional). Default 2500, clamped to
|
|
# 500-8000. Raise it when a verbose model's briefing JSON gets cut off
|
|
# ("AI response was cut off"). See docs/integrations/ai-providers.md.
|
|
# INSIGHTS_MAX_TOKENS=""
|
|
|
|
# Per-user hourly ceiling for POST /api/insights/generate (optional).
|
|
# Default 10. Lower it on a tight LLM budget; the 24h cache already
|
|
# short-circuits read traffic. See docs/integrations/ai-providers.md.
|
|
# INSIGHTS_RATE_LIMIT_PER_HOUR=""
|
|
|
|
# Per-user hourly ceiling for the document-AI actions (optional). Read/index,
|
|
# extract, suggest and summarise share one bucket per user. Default 6,
|
|
# clamped to 1-1000. Raise it when users batch-read many documents against a
|
|
# generous provider budget; lower it on a tight one. See
|
|
# docs/integrations/ai-providers.md.
|
|
# DOCUMENT_AI_LIMIT_PER_HOUR=""
|
|
|
|
# Extra hosts the ADMIN-configured AI base URL may point at, on top of the
|
|
# built-in public providers (comma-separated bare hostnames). Set it when you
|
|
# run a gateway this instance should trust. It widens the admin setting only;
|
|
# per-user BYO keys keep the pinned public-host posture either way.
|
|
# ADMIN_AI_BASE_URL_ALLOWLIST=""
|
|
|
|
# Codex (ChatGPT sign-in) model slugs. CODEX_MODEL pins one slug ahead of the
|
|
# built-in chain; CODEX_MODEL_FALLBACK_CHAIN replaces the chain outright
|
|
# (comma-separated, first match wins). Reach for these when the provider
|
|
# rotates a slug and requests start failing with 503 -- without them a
|
|
# rotation needs a new release to fix.
|
|
# CODEX_MODEL=""
|
|
# CODEX_MODEL_FALLBACK_CHAIN=""
|
|
|
|
# Surfaces reviewed n-of-1 experiment outcomes in the Coach snapshot. Off by
|
|
# default: the read-back is the most overclaim-prone Coach surface, so it is a
|
|
# deliberate operator opt-in. Set to "1" to enable; anything else keeps it
|
|
# dormant while the review worker still records outcomes.
|
|
# COACH_EXPERIMENT_VERDICT="1"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Off-host backups -- all-or-none
|
|
# -----------------------------------------------------------------------------
|
|
# BACKUP_S3_ENDPOINT=""
|
|
# BACKUP_S3_BUCKET=""
|
|
# BACKUP_S3_ACCESS_KEY=""
|
|
# BACKUP_S3_SECRET_KEY=""
|
|
# BACKUP_ENCRYPTION_KEY=""
|
|
|
|
# The two optional members of the set, both with defaults, so leaving them
|
|
# unset does not disable backups.
|
|
#
|
|
# Region sent with the SigV4 signature. Default "auto", which is what
|
|
# Cloudflare R2 expects; AWS S3 and most MinIO deployments want their real
|
|
# region name here.
|
|
# BACKUP_S3_REGION="auto"
|
|
#
|
|
# How long backup objects are meant to live, in days. Default 30, minimum 1.
|
|
# The worker never deletes a BACKUP object -- its grant is PutObject, GetObject
|
|
# and AbortMultipartUpload -- so this number is what you set the bucket's own
|
|
# lifecycle rule to. The one key it does delete is its own connection test's
|
|
# `_healthcheck/<ts>.bin`, and that delete is best-effort: without DeleteObject
|
|
# on that prefix every Test-connection press leaves one 1-byte orphan behind,
|
|
# silently. See docs/ops/backup-restore.md.
|
|
# BACKUP_RETENTION_DAYS="30"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Audit-trail retention -- optional
|
|
# -----------------------------------------------------------------------------
|
|
# How long rows in `audit_logs` survive, in days. Default 365. The floor is 7:
|
|
# a smaller value is ignored rather than obeyed, so a window entered in seconds
|
|
# by mistake cannot empty a fresh table. A daily job does the pruning.
|
|
#
|
|
# This number is shown to users, not just enforced. Settings -> Shared access
|
|
# prints the window under the activity list, and the dialog that ends somebody
|
|
# else's access states how long "who entered what" stays answerable. Both take
|
|
# the value the server resolved from this variable, so an instance running 90
|
|
# says 90 on both surfaces.
|
|
# AUDIT_LOG_RETENTION_DAYS="365"
|
|
|
|
# How long Coach conversations survive, in days. Default 365, floor 30. Like
|
|
# the audit window above this number is shown to the user -- the Data &
|
|
# Privacy page states it -- so an instance running 90 has to be able to say
|
|
# 90 rather than repeat the default.
|
|
# COACH_MESSAGE_RETENTION_DAYS="365"
|
|
|
|
# How long the host CPU/memory samples behind the admin monitoring charts are
|
|
# kept, in days. Default 7. Raise it for a longer trend; lower it on a host
|
|
# where disk is the scarce resource.
|
|
# HOST_METRIC_RETENTION_DAYS="7"
|
|
|
|
# Thinning of dense intraday samples (heart rate, glucose) once they age out
|
|
# of the detail window. On by default. Set "false" to keep every raw sample
|
|
# forever -- accurate to the second, at the cost of unbounded table growth.
|
|
# DENSE_INTRADAY_RETENTION_ENABLED="false"
|
|
|
|
# Consecutive failures before an integration is reported as persistently
|
|
# failing and notified about. Default 3, minimum 1. Raise it when a flaky
|
|
# upstream makes the notification noisy.
|
|
# INTEGRATION_FAILURE_ALERT_THRESHOLD="3"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Database session timeout -- optional
|
|
# -----------------------------------------------------------------------------
|
|
# Statement timeout applied to every database session, in milliseconds.
|
|
# Default 60000, which bounds a genuinely runaway query while leaving headroom
|
|
# for the heaviest legitimate read (the live-aggregate analytics fallback on a
|
|
# large account). Set "0" to disable it and restore unbounded behaviour.
|
|
# DATABASE_STATEMENT_TIMEOUT_MS="60000"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# FHIR health-record export -- optional
|
|
# -----------------------------------------------------------------------------
|
|
# Per-export ceiling on the number of MedicationAdministration rows in the
|
|
# FHIR Bundle. The report window already scopes the set; this is a coarse
|
|
# safety ceiling on the serialised Bundle size. Bounded 1..50000; any unset
|
|
# or out-of-range value falls back to the default of 5000. Raise it only for
|
|
# an unusual multi-year, many-medication workload.
|
|
# FHIR_MAX_MEDICATION_ADMINISTRATIONS="5000"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Dashboard snapshot -- BUILD-TIME toggle (default ON)
|
|
# -----------------------------------------------------------------------------
|
|
# The dashboard hydrates every above-the-fold tile from the single
|
|
# `GET /api/dashboard/snapshot` cell so the whole strip shares one
|
|
# completion moment. This is the default and the desired behaviour.
|
|
#
|
|
# `NEXT_PUBLIC_*` vars are inlined into the client bundle at BUILD time,
|
|
# not read at runtime -- so this has NO effect on the prebuilt GHCR
|
|
# image and is intentionally absent from the `docker-compose.yml`
|
|
# runtime `environment:` whitelist. To fall back to the legacy
|
|
# four-cell path you must rebuild the image with the var set to "false":
|
|
#
|
|
# $ NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false pnpm build # or a Dockerfile ARG
|
|
#
|
|
# NEXT_PUBLIC_DASHBOARD_SNAPSHOT="false"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Client-IP resolution -- optional (required behind Cloudflare)
|
|
# -----------------------------------------------------------------------------
|
|
# When the stack sits behind Cloudflare, the real visitor IP arrives in the
|
|
# `cf-connecting-ip` header; the X-Forwarded-For chain otherwise yields the
|
|
# Cloudflare edge IP, so the audit-log overview stores the CF datacentre
|
|
# address and the geo lookup resolves the wrong location. Set this to "1"
|
|
# ONLY when Cloudflare is in front -- a direct-exposed self-host must leave
|
|
# it unset, or an attacker could forge the header. Off by default.
|
|
# Number of trusted reverse-proxy hops in front of the app; the client IP is
|
|
# read that many entries from the right of the X-Forwarded-For chain. Unset
|
|
# for a directly-exposed host, 1 behind a single reverse proxy. A wrong
|
|
# value makes the app refuse XFF and collapse anonymous callers into one
|
|
# rate-limit bucket.
|
|
# TRUST_PROXY_HOPS="1"
|
|
# TRUST_CF_CONNECTING_IP="1"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# IP geolocation lookup -- optional
|
|
# -----------------------------------------------------------------------------
|
|
# The admin login/audit overview enriches auth events with a coarse
|
|
# "City, CC" location + carrier. The default online provider is ipwho.is
|
|
# (free, no key, HTTPS). Override the base URL to point at a different
|
|
# provider; the response must match either the ipwho.is shape (`success` +
|
|
# `country_code`) or the ip-api.com shape (`status` + `countryCode`). The
|
|
# `/<ip>` segment is appended automatically.
|
|
# IP_GEO_LOOKUP_URL="https://ipwho.is"
|
|
#
|
|
# By default a non-HTTPS URL is refused (audit-event IPs must never egress over
|
|
# plaintext HTTP). The free ip-api.com endpoint is HTTP-only (its HTTPS form
|
|
# needs a paid key) but is often more accurate. To use it, set BOTH the HTTP
|
|
# URL and the explicit opt-in below -- the looked-up IP then travels in clear
|
|
# over your server's own egress; this is your call to make.
|
|
# IP_GEO_LOOKUP_URL="http://ip-api.com/json"
|
|
# IP_GEO_ALLOW_INSECURE="true"
|
|
#
|
|
# Set to "1" to disable all third-party IP egress (GDPR Art. 44). With egress
|
|
# off the resolver uses only the offline GeoLite2 MMDB tier, if present.
|
|
# IP_GEO_LOOKUP_DISABLED="1"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Offline GeoLite2 databases -- optional
|
|
# -----------------------------------------------------------------------------
|
|
# Where the runtime looks for GeoLite2-City.mmdb and GeoLite2-ASN.mmdb.
|
|
# Unset it reads /opt/geolite2, the path the published image bakes its own
|
|
# copy into -- but only if the image build had a MaxMind licence key, so on
|
|
# a stock `:latest` the directory is usually empty and every login IP goes
|
|
# to the online provider instead.
|
|
#
|
|
# Three ways to get the databases:
|
|
#
|
|
# 1. RUNTIME FETCH (issue #659) -- set MAXMIND_LICENSE_KEY below and the
|
|
# worker downloads both databases into GEOLITE2_DIR itself on the next
|
|
# boot and monthly thereafter. No image rebuild, no manual mount. This is
|
|
# the way for a stock `:latest` pull.
|
|
# 2. Bring your own -- create a free MaxMind account, pull both databases
|
|
# with `geoipupdate`, mount the directory into the container read-only
|
|
# (the commented `volumes:` block in docker-compose.yml shows the line),
|
|
# and point GEOLITE2_DIR at the container path. Refreshing the files on
|
|
# the host is enough -- the reader picks them up on the next restart.
|
|
# 3. Bake them at build time (build your own image with a licence key).
|
|
#
|
|
# Pair any of them with IP_GEO_LOOKUP_DISABLED="1" above for zero IP egress.
|
|
# See docs/self-hosting/geolite2.md.
|
|
# GEOLITE2_DIR="/var/lib/geolite2"
|
|
#
|
|
# MaxMind licence key for the runtime fetch (option 1 above). Free: sign up at
|
|
# https://www.maxmind.com/en/geolite2/signup, then My Account -> Manage
|
|
# License Keys. Unset, nothing is fetched and the online provider answers.
|
|
# MAXMIND_LICENSE_KEY="your-maxmind-licence-key"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Environmental-context module (Open-Meteo) -- optional
|
|
# -----------------------------------------------------------------------------
|
|
# The opt-in Environment module fetches daily weather / daylight for the user's
|
|
# coarse home (or a travel override) from Open-Meteo. The hosted endpoints are
|
|
# keyless and free for non-commercial use; data is CC BY 4.0. Both URLs are
|
|
# optional -- unset uses the hosted defaults below. A privacy-maximalist or
|
|
# commercial self-hoster can run their own Open-Meteo instance and point these
|
|
# at it (a private/LAN host is allowed -- the fetch is server-side and does NOT
|
|
# force the public-host guard, so an internal instance is not blocked).
|
|
# OPENMETEO_BASE_URL="https://archive-api.open-meteo.com"
|
|
# OPENMETEO_GEOCODING_URL="https://geocoding-api.open-meteo.com"
|
|
|
|
# ── Native canvas renderer (document thumbnails, scanned-PDF rasterization) ──
|
|
# The bundled renderer's x64 build uses AVX2 CPU instructions. On x86-64 hosts
|
|
# WITHOUT AVX2 (Celeron/Atom-class NAS CPUs) it is disabled automatically and
|
|
# those features degrade cleanly (no thumbnails; scanned PDFs read as text
|
|
# only). Override only if auto-detection misfires:
|
|
# off — force-disable the renderer
|
|
# on — force-enable (crashes the container if the CPU truly lacks AVX2)
|
|
# NATIVE_CANVAS="off"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Logging -- optional
|
|
# -----------------------------------------------------------------------------
|
|
# LOG_LEVEL is one of debug / info / warn / error. Default info: drop to debug
|
|
# while chasing a problem, raise to warn on a busy instance. LOG_SAMPLE_RATE
|
|
# keeps that fraction of SUCCESSFUL events (default 1.0 = all; 0.1 thins a
|
|
# chatty log without losing errors). LOG_SLOW_THRESHOLD_MS is the duration
|
|
# above which a request is flagged slow (default 3000). LOG_INCLUDE_STACK set
|
|
# to "false" strips stack traces from the emitted events.
|
|
# LOG_LEVEL="info"
|
|
# LOG_SAMPLE_RATE="1.0"
|
|
# LOG_SLOW_THRESHOLD_MS="3000"
|
|
# LOG_INCLUDE_STACK="false"
|
|
|
|
# Optional log shipping to a Loki instance. LOKI_ENDPOINT is the push URL and
|
|
# is what turns shipping on; the username/password pair is only needed when
|
|
# your Loki sits behind basic auth. Leave all three unset and logs stay on
|
|
# stdout for the Docker log driver to rotate.
|
|
# LOKI_ENDPOINT=""
|
|
# LOKI_USERNAME=""
|
|
# LOKI_PASSWORD=""
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Demo mode -- read-only showcase instance
|
|
# -----------------------------------------------------------------------------
|
|
# "true" makes the proxy refuse every mutation except login and paints a demo
|
|
# banner. This is the switch behind a public try-it-out deployment, not a
|
|
# safety belt for a real one -- leave it unset on an instance holding real
|
|
# data.
|
|
# DEMO_MODE="true"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Server-side prefetch -- diagnostics escape hatch
|
|
# -----------------------------------------------------------------------------
|
|
# The dashboard, insights, coach and checkups pages prefetch their data on the
|
|
# server so the first paint arrives filled in. Set "false" to fall back to
|
|
# pure client fetching. Reach for it only when a prefetch is misbehaving on
|
|
# your host and you want to see whether the client path is healthy.
|
|
# DASHBOARD_SSR_PREFETCH="false"
|