-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy path.env.production.example
More file actions
391 lines (350 loc) · 20.8 KB
/
Copy path.env.production.example
File metadata and controls
391 lines (350 loc) · 20.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# =============================================================================
# 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=""
# -----------------------------------------------------------------------------
# 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). 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).
# 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
# -----------------------------------------------------------------------------
# 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=""
# -----------------------------------------------------------------------------
# Off-host backups -- all-or-none
# -----------------------------------------------------------------------------
# BACKUP_S3_ENDPOINT=""
# BACKUP_S3_BUCKET=""
# BACKUP_S3_ACCESS_KEY=""
# BACKUP_S3_SECRET_KEY=""
# BACKUP_ENCRYPTION_KEY=""
# -----------------------------------------------------------------------------
# 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"
# -----------------------------------------------------------------------------
# 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"