From e9206bab0bbac15ed47d90fa492f85112cb6777b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 3 Sep 2026 02:12:55 +0200 Subject: [PATCH] fix(compose): forward the env vars the app reads but never received The environment: block is a whitelist. Compose substitutes ${VAR} from .env at compose-up, so a value set there looks configured, but a variable the block does not name never reaches the Node process and the default stands silently. Twenty-eight variables were in that state: DEMO_MODE, the four LOG_* knobs and the three LOKI_* shipping vars, DATABASE_STATEMENT_TIMEOUT_MS, four retention and alerting windows, TELEGRAM_WEBHOOK_SECRET, NIGHTSCOUT_PRIVATE_ORIGINS, three OAuth callback overrides, both Open-Meteo endpoints, four AI knobs, three APNs key-source vars, and DASHBOARD_SSR_PREFETCH. APNS_KEY and APNS_KEY_FILE are the sharpest of them: .env.production.example and the env manifest both document a precedence chain of APNS_KEY_B64 before APNS_KEY before APNS_KEY_FILE, and only the first arrived. Each is documented in .env.production.example in its neighbours' style. The VAPID section's note claiming those three names are absent from the whitelist is corrected; they have been on it since v1.17.1. --- .env.production.example | 136 ++++++++++++++++++++++++++++++++++++++-- docker-compose.yml | 99 +++++++++++++++++++++++++++++ 2 files changed, 231 insertions(+), 4 deletions(-) diff --git a/.env.production.example b/.env.production.example index c41bdeb91..ee3d19cf4 100644 --- a/.env.production.example +++ b/.env.production.example @@ -35,6 +35,40 @@ API_TOKEN_HMAC_KEY="" # 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 "/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 +# "/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="" + + +# ----------------------------------------------------------------------------- +# 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="" # ----------------------------------------------------------------------------- @@ -140,10 +174,9 @@ API_TOKEN_HMAC_KEY="" # 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). NOTE: these are -# NOT in docker-compose.yml's `environment:` whitelist -- if you go the env -# route under compose, add them there too or the values never reach the -# container (see docs/self-hosting/getting-started.md Troubleshooting). +# 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" @@ -164,6 +197,7 @@ API_TOKEN_HMAC_KEY="" # 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 # ----------------------------------------------------------------------------- @@ -242,6 +276,26 @@ API_TOKEN_HMAC_KEY="" # 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 @@ -267,6 +321,37 @@ API_TOKEN_HMAC_KEY="" # 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 @@ -389,3 +474,46 @@ API_TOKEN_HMAC_KEY="" # 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" diff --git a/docker-compose.yml b/docker-compose.yml index 36bd212bd..f09232067 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -64,7 +64,23 @@ x-healthlog-env: &healthlog_env # docker-compose reads .env for `${VAR}` substitution at compose-up time). SESSION_COOKIE_SECURE: "${SESSION_COOKIE_SECURE:-}" NATIVE_CANVAS: "${NATIVE_CANVAS:-}" + # Read-only showcase instance. DEMO_MODE=true makes the proxy refuse every + # mutation except login and paints the demo banner. Leave it unset on a + # real self-host — it is a public-demo switch, not a safety belt. + DEMO_MODE: "${DEMO_MODE:-}" + # Statement timeout applied to every database session, in milliseconds + # (default 60000; 0 disables it and restores the unbounded behaviour). + # Raise it if a very large account's analytics fallback is being cut off. + DATABASE_STATEMENT_TIMEOUT_MS: "${DATABASE_STATEMENT_TIMEOUT_MS:-}" WITHINGS_WEBHOOK_SECRET: "${WITHINGS_WEBHOOK_SECRET:-}" + # Overrides the derived `${NEXT_PUBLIC_APP_URL}/api/withings/callback`; + # set it when the URL registered with Withings differs from the app's own + # public URL. The WHOOP / Polar / Oura / Strava equivalents are below. + WITHINGS_REDIRECT_URI: "${WITHINGS_REDIRECT_URI:-}" + # Path-segment + HMAC secret for the Telegram bot webhook. Set it to the + # same value you register with BotFather; without it the settings page + # cannot install the webhook and inbound updates are refused. + TELEGRAM_WEBHOOK_SECRET: "${TELEGRAM_WEBHOOK_SECRET:-}" # v1.18.11 (W3) — client-IP resolution + geo lookup. Behind Cloudflare # the real visitor IP is in `cf-connecting-ip`; set # TRUST_CF_CONNECTING_IP=1 ONLY when Cloudflare is in front (off by @@ -125,6 +141,22 @@ x-healthlog-env: &healthlog_env # the block exists: `environment:` is a whitelist, and a value in .env that # is not named here never arrives. AUDIT_LOG_RETENTION_DAYS: "${AUDIT_LOG_RETENTION_DAYS:-}" + # How long Coach conversations are kept, in days (default 365, floor 30). + # Like the audit window this number is PUBLISHED to the user on the Data & + # Privacy page, so an instance running 90 has to be able to say 90. + COACH_MESSAGE_RETENTION_DAYS: "${COACH_MESSAGE_RETENTION_DAYS:-}" + # How long the host CPU/memory samples behind the admin charts are kept, + # in days (default 7). Raise it for a longer trend, lower it on a small + # disk. + HOST_METRIC_RETENTION_DAYS: "${HOST_METRIC_RETENTION_DAYS:-}" + # 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, at the cost of table growth. + DENSE_INTRADAY_RETENTION_ENABLED: "${DENSE_INTRADAY_RETENTION_ENABLED:-}" + # Consecutive failures before an integration is reported as persistently + # failing and notified about (default 3, minimum 1). Raise it if a flaky + # upstream is noisy. + INTEGRATION_FAILURE_ALERT_THRESHOLD: "${INTEGRATION_FAILURE_ALERT_THRESHOLD:-}" # Per-export ceiling on MedicationAdministration rows in the FHIR # health-record Bundle. Bounded 1..50000, default 5000. Listed here so # an operator override in .env actually reaches the container (the @@ -161,6 +193,23 @@ x-healthlog-env: &healthlog_env STRAVA_CLIENT_ID: "${STRAVA_CLIENT_ID:-}" STRAVA_CLIENT_SECRET: "${STRAVA_CLIENT_SECRET:-}" STRAVA_REDIRECT_URI: "${STRAVA_REDIRECT_URI:-}" + # Fitbit + Google Health are per-user BYO-key integrations, so there is no + # client id/secret env for either. These override the derived + # `${NEXT_PUBLIC_APP_URL}/api/{fitbit,google-health}/callback` when the URL + # registered with the provider differs from the app's public URL. Both are + # asserted same-origin with NEXT_PUBLIC_APP_URL, so they cannot redirect + # the authorization code somewhere else. + FITBIT_REDIRECT_URI: "${FITBIT_REDIRECT_URI:-}" + GOOGLE_HEALTH_REDIRECT_URI: "${GOOGLE_HEALTH_REDIRECT_URI:-}" + # Nightscout origins on your own network that the per-user connection + # validator should accept, comma-separated (e.g. "http://nightscout.lan"). + # Without it a LAN/private Nightscout URL is refused as an SSRF target. + NIGHTSCOUT_PRIVATE_ORIGINS: "${NIGHTSCOUT_PRIVATE_ORIGINS:-}" + # Open-Meteo endpoints for the environment/weather correlation. Default to + # the hosted service; point them at your own Open-Meteo deployment to keep + # the coordinate lookups off a third party. + OPENMETEO_BASE_URL: "${OPENMETEO_BASE_URL:-}" + OPENMETEO_GEOCODING_URL: "${OPENMETEO_GEOCODING_URL:-}" # OIDC SSO login (self-hosted IdP: Authentik/Keycloak/Authelia/etc). # All three of ISSUER_URL/CLIENT_ID/CLIENT_SECRET together enable the # "Sign in with SSO" button; set none to leave it disabled. OIDC_ONLY @@ -201,6 +250,22 @@ x-healthlog-env: &healthlog_env # in .env reaches the container (the `environment:` block is a whitelist). # See docs/integrations/ai-providers.md. ALLOW_LOCAL_AI_PRIVATE_HOSTS: "${ALLOW_LOCAL_AI_PRIVATE_HOSTS:-}" + # 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 only the admin + # setting, never the per-user BYO-key posture. + ADMIN_AI_BASE_URL_ALLOWLIST: "${ADMIN_AI_BASE_URL_ALLOWLIST:-}" + # Codex (ChatGPT sign-in) model slugs. Set CODEX_MODEL to pin one slug + # ahead of the built-in chain, or CODEX_MODEL_FALLBACK_CHAIN (comma- + # separated) to replace the chain outright. Reach for these when the + # provider rotates a slug and every request starts coming back 503 — + # without them a rotation needs a new release to fix. + CODEX_MODEL: "${CODEX_MODEL:-}" + CODEX_MODEL_FALLBACK_CHAIN: "${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 an explicit operator opt-in. Set to "1" to enable. + COACH_EXPERIMENT_VERDICT: "${COACH_EXPERIMENT_VERDICT:-}" # Daily-briefing output-token ceiling (optional; default 2500, clamped # 500-8000). Raise when a verbose model's briefing JSON is cut off. # Listed here so an operator value in .env reaches the container (the @@ -244,12 +309,46 @@ x-healthlog-env: &healthlog_env # PWA users need none of this (Web Push covers them). See # docs/self-hosting/notifications.md. APNS_KEY_B64: "${APNS_KEY_B64:-}" + # The other two key sources, in precedence order after APNS_KEY_B64: + # APNS_KEY is the raw PEM body, APNS_KEY_FILE a path to the .p8 inside the + # container (mount it yourself). Both are documented in + # .env.production.example and in the manifest check-env reads, and both + # were absent from this whitelist — so an operator who followed the + # documented precedence chain got a silently disabled APNs. + APNS_KEY: "${APNS_KEY:-}" + APNS_KEY_FILE: "${APNS_KEY_FILE:-}" APNS_KEY_ID: "${APNS_KEY_ID:-}" APNS_TEAM_ID: "${APNS_TEAM_ID:-}" APNS_BUNDLE_ID: "${APNS_BUNDLE_ID:-}" + # Force the production APNs gateway. Normally derived from NODE_ENV; set + # "true" when a production-signed build has to reach the production + # gateway from a non-production deployment. + APNS_PRODUCTION: "${APNS_PRODUCTION:-}" # Set "true" only if your own iOS build carries Apple's Critical Alerts # entitlement; otherwise urgent alerts use the time-sensitive level. APNS_CRITICAL_ENTITLEMENT: "${APNS_CRITICAL_ENTITLEMENT:-}" + # Structured logging. 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="false" strips stack traces from + # the emitted events. + LOG_LEVEL: "${LOG_LEVEL:-}" + LOG_SAMPLE_RATE: "${LOG_SAMPLE_RATE:-}" + LOG_SLOW_THRESHOLD_MS: "${LOG_SLOW_THRESHOLD_MS:-}" + LOG_INCLUDE_STACK: "${LOG_INCLUDE_STACK:-}" + # Optional log shipping to a Loki instance. Set LOKI_ENDPOINT to the push + # URL to turn it on; the username/password pair is only needed when your + # Loki is behind basic auth. Leave all three unset and logs stay on stdout + # for the Docker log driver to rotate. + LOKI_ENDPOINT: "${LOKI_ENDPOINT:-}" + LOKI_USERNAME: "${LOKI_USERNAME:-}" + LOKI_PASSWORD: "${LOKI_PASSWORD:-}" + # Server-side prefetch for the dashboard, insights, coach and checkups + # pages. On by default; set "false" to fall back to pure client fetching, + # which is the escape hatch when a prefetch is misbehaving on your host. + DASHBOARD_SSR_PREFETCH: "${DASHBOARD_SSR_PREFETCH:-}" services: # Single-container default (HEALTHLOG_PROCESS_TYPE=all). Existing