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.
This commit is contained in:
Marc-André Bombeck committed 2026-09-03 02:12:55 +02:00
1 parent 98597232f2
commit e9206bab0b
2 files changed
+231 -4

No files matched your search

+132 -4
View File
@@ -35,6 +35,40 @@ API_TOKEN_HMAC_KEY="<openssl rand -hex 32>"
# 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=""
# -----------------------------------------------------------------------------
@@ -140,10 +174,9 @@ API_TOKEN_HMAC_KEY="<openssl rand -hex 32>"
# 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="<openssl rand -hex 32>"
# 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="<openssl rand -hex 32>"
# 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="<openssl rand -hex 32>"
# 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="<openssl rand -hex 32>"
# 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"
+99
View File
@@ -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