mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Connect, status, places, settings, test, sync and disconnect replace their stubs. A nightly job (03:20) and the connect backfill read the timeline, visits and residency endpoints through safeFetch and store one row of daily values per local day: distances, places visited, minutes away from the home place and the sealed country with its travel-day verdict. A private instance needs its exact origin in DAWARICH_PRIVATE_ORIGINS. The connection joins the shared ledger and the connection health model, watched for errors only. The day carries a movement block, the model projection drops the travel country, the two movement channels join the correlation matrix under the environment module, travel days become trip suggestions, and the days travel in the environment backup section.
690 lines
37 KiB
Bash
690 lines
37 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=""
|
|
|
|
# Dawarich instances on your own network that the movement-context connection
|
|
# may reach, comma-separated exact origins (e.g. "http://dawarich.lan:3000").
|
|
# Each entry is one scheme://host[:port], never a suffix or a range. A public
|
|
# Dawarich instance needs nothing here.
|
|
# DAWARICH_PRIVATE_ORIGINS=""
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Notification targets on a private network -- optional
|
|
# -----------------------------------------------------------------------------
|
|
# Webhook (Gotify, Home Assistant, n8n, a Discord relay) and ntfy targets on
|
|
# your own network, comma-separated exact origins (scheme://host[:port], no
|
|
# path). A target that resolves to a private address is otherwise refused at
|
|
# send time; the test button reports it as private_origin_not_approved. A
|
|
# listed origin is still dialled through the pinned resolver with redirects
|
|
# forbidden; link-local, metadata and unspecified addresses stay refused even
|
|
# when listed, and a malformed entry is logged once and grants nothing.
|
|
# See docs/self-hosting/notifications.md.
|
|
# NOTIFICATION_PRIVATE_ORIGINS=""
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Document picker: Paperless-ngx and Papra -- optional
|
|
# -----------------------------------------------------------------------------
|
|
# The Paperless-ngx and Papra instances people may connect to pick documents
|
|
# from, comma-separated exact origins (scheme://host[:port], no path), e.g.
|
|
# "https://paperless.example.com,http://papra.lan:1221". Unset, the document
|
|
# picker is switched off. Only a listed origin is ever contacted, public or
|
|
# private; requests are pinned to it, redirects are not followed, and
|
|
# link-local, metadata and unspecified addresses stay refused even when
|
|
# listed. A malformed entry is logged once and grants nothing.
|
|
# See docs/self-hosting/document-sources.md.
|
|
# DOCUMENT_SOURCE_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"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Platform hint for the encryption key backup step
|
|
# -----------------------------------------------------------------------------
|
|
# Where this server's environment lives: compose, truenas, coolify, unraid or
|
|
# portainer. The admin's "Back up your encryption key" step opens on the
|
|
# instructions for that platform. Unset means compose. Catalog templates set
|
|
# it for you.
|
|
# HEALTHLOG_PLATFORM="compose"
|
|
|
|
# Start-up encryption key check: enforce (default) refuses to serve when the
|
|
# key does not open the stored data; warn only logs it, reports it as a
|
|
# warning on /api/health, and serves anyway. A last resort for a refusal you
|
|
# are certain is false (a first start, with no recorded key to reset). It
|
|
# disables a safety check: with a wrong key every encrypted value fails and
|
|
# new rows are written under that key. Remove it once the key is confirmed.
|
|
# ENCRYPTION_KEY_CHECK="enforce"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 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=""
|
|
|
|
# AI endpoints on your own network (optional). An AI base URL on a private
|
|
# address is refused unless its exact origin is listed here, comma-separated
|
|
# (e.g. "http://ollama.lan:11434,http://10.0.0.5:4000") — for a self-hosted
|
|
# Ollama, LM Studio or LiteLLM gateway. Link-local, metadata and unspecified
|
|
# addresses stay refused even when listed. See docs/integrations/ai-providers.md.
|
|
# AI_PRIVATE_ORIGINS=""
|
|
# Older form, kept for existing installs: an exact host list still works
|
|
# (e.g. "ollama.lan,10.0.0.5"). "true" is deprecated: it now covers only the
|
|
# admin AI key and admin accounts' own AI settings, and will be removed.
|
|
# 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=""
|
|
|
|
# Minimum hours between two regenerations of a status card after a sync
|
|
# (optional). A watch or an integration delivers readings all day, and each
|
|
# delivery would otherwise regenerate the cards it touches every hour. Hand
|
|
# entries keep a one-hour gap. Default 6, whole hours from 1 to 48. The value
|
|
# under Admin > Coach wins over this one. See docs/integrations/ai-providers.md.
|
|
# AI_SYNC_REGEN_GAP_HOURS=""
|
|
|
|
# Hourly upload ceiling for each document-upload token (optional). Applies
|
|
# only to uploads made with a token minted under Settings > API & Tokens >
|
|
# Document tokens: a Paperless workflow or the import script. Every upload
|
|
# that sends a file counts, duplicates included. Default 120, clamped to
|
|
# 1-1000. Uploads from the web app and the phone keep their own bucket. Raise
|
|
# it to import a large archive faster; the per-user storage quota still
|
|
# applies. See docs/integrations/document-import.md.
|
|
# DOCUMENT_UPLOAD_LIMIT_PER_HOUR=""
|
|
|
|
# Per-user hourly ceiling for lab-report scans (optional). The photo/PDF scan
|
|
# and the browser-OCR text scan share one bucket per user. Default 6, clamped
|
|
# to 1-1000. Raise it when users scan a stack of reports at once against a
|
|
# generous provider budget; cost stays bounded by the daily AI budget. See
|
|
# docs/integrations/ai-providers.md.
|
|
# LABS_OCR_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 app does not expire objects by age: this number is what you set the
|
|
# bucket's own lifecycle rule to. It does delete objects in two cases, so the
|
|
# credential needs DeleteObject: every copy of an account that is deleted or
|
|
# whose data is wiped, and its own connection test's `_healthcheck/<ts>.bin`.
|
|
# See docs/ops/backup-restore.md.
|
|
# BACKUP_RETENTION_DAYS="30"
|
|
#
|
|
# Off-host keys no longer written with, comma-separated, kept so older
|
|
# objects stay readable during a rotation of BACKUP_ENCRYPTION_KEY. Remove a
|
|
# key once the lifecycle rule has retired every object written under it.
|
|
# BACKUP_ENCRYPTION_PREVIOUS_KEYS=""
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Stored backups in the database (optional)
|
|
# -----------------------------------------------------------------------------
|
|
# The largest weekly or uploaded backup of one account the app keeps, in
|
|
# megabytes of encrypted copy. Default 2048. It does not depend on the app's
|
|
# memory: a copy is written and read in pieces of about a megabyte. The limit
|
|
# only stops a runaway copy from filling the database volume, which holds the
|
|
# old and the new copy side by side while a backup runs. An account with 2.6
|
|
# million readings stores a copy of about 100 MB.
|
|
# BACKUP_MAX_STORED_MB="2048"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 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"
|
|
#
|
|
# Postgres `work_mem` applied to every app session, as a Postgres size string.
|
|
# Default 16MB. Large sorts and aggregates that exceed it spill to disk; each
|
|
# sort step of each pooled connection may use this much memory, so raise it
|
|
# with the pool size in mind.
|
|
# DATABASE_WORK_MEM="16MB"
|
|
#
|
|
# Both settings above reach Postgres as the libpq `options` startup
|
|
# parameter. PgBouncer refuses that parameter ("unsupported startup
|
|
# parameter: options") unless it is listed in its ignore_startup_parameters.
|
|
# Behind such a pooler, set this to 1, true or yes to send no session
|
|
# settings at all; the pooler's or the database's own defaults then apply.
|
|
# DATABASE_SESSION_OPTIONS_DISABLED="1"
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 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. 0 for a
|
|
# directly-exposed host, 1 (the default when unset) behind a single reverse
|
|
# proxy; X-Real-IP is read only at 1. A wrong value makes the app refuse the
|
|
# headers, log one warning, and collapse anonymous callers into one
|
|
# rate-limit bucket.
|
|
# TRUST_PROXY_HOPS="1"
|
|
# TRUST_CF_CONNECTING_IP="1"
|
|
|
|
# Memory cap of the app container (read by docker-compose.yml, not by the app).
|
|
# Node sizes its heap from it: 1536m (the default) gives 792 MB, 1g only 524 MB,
|
|
# which a long-used account fills in normal operation. Lower it on a host with
|
|
# 2 GB or less, raise it on a busy instance.
|
|
# APP_MEMORY_LIMIT="1536m"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Outbound requests HealthLog makes on its own -- optional switches
|
|
# -----------------------------------------------------------------------------
|
|
# Besides the services you connect yourself (AI provider, integrations,
|
|
# notification channels, off-host backups), the server contacts three third
|
|
# parties by default. Each has an off-switch that accepts "1", "true" or "yes":
|
|
#
|
|
# - HaveIBeenPwned, when a password is set (registration, change, admin
|
|
# reset). Only the first five hex characters of the password's SHA-1 are
|
|
# sent (k-anonymity); the password and its full hash never leave. With the
|
|
# switch on, known-breached passwords are no longer refused.
|
|
# PASSWORD_BREACH_CHECK_DISABLED="1"
|
|
#
|
|
# - The GitHub releases API, at most once a day per browser, for the "update
|
|
# available" badge on the About page and the admin version tile.
|
|
# UPDATE_CHECK_DISABLED="1"
|
|
#
|
|
# - The online IP geolocation provider for login events; see
|
|
# IP_GEO_LOOKUP_DISABLED in the next section.
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 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" (or "true" / "yes") to disable all third-party IP egress (GDPR
|
|
# Art. 44). With egress off the resolver uses only the offline GeoLite2 MMDB
|
|
# tier, if present, and the monthly MaxMind download is skipped too.
|
|
# 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"
|
|
#
|
|
# Air quality, pollen and UV come from the Open-Meteo air-quality API, which
|
|
# serves the Copernicus Atmosphere Monitoring Service (CAMS) models. CAMS data
|
|
# is free to use with attribution, which the app shows next to the values.
|
|
# Like the URLs above this one is optional and can point at a self-hosted
|
|
# Open-Meteo instance. Set ENVIRONMENT_AIR_QUALITY_DISABLED to 1, true or yes
|
|
# to stop the air-quality requests for the whole instance; weather and
|
|
# daylight keep working, and no user can turn air quality back on.
|
|
# OPENMETEO_AIR_QUALITY_URL="https://air-quality-api.open-meteo.com"
|
|
# ENVIRONMENT_AIR_QUALITY_DISABLED="1"
|
|
|
|
# ── 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 turns shipping on.
|
|
# Either form works: the base URL (http://loki:3100) or the full push URL
|
|
# (http://loki:3100/loki/api/v1/push). A Loki on your own network is fine,
|
|
# private addresses and internal hostnames included; only link-local,
|
|
# cloud-metadata and the unspecified address (0.0.0.0, ::) are refused. The
|
|
# username/password pair is only needed when your Loki sits behind basic
|
|
# auth. When a push fails (Loki down, wrong path, rejected credentials) the
|
|
# app writes one line to stderr naming the Loki origin, the reason and how
|
|
# many events were dropped, at most once every five minutes per reason; the
|
|
# password never appears in it. 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"
|