mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
The runtime has always read GEOLITE2_DIR, but the variable was absent from the compose `environment:` whitelist, from both env examples, and from the docs. A variable that is not on that whitelist never reaches the container even when the operator's `.env` sets it, so the override existed in code and was unreachable from the documented path. Put it on the whitelist, document it in both examples and the manifest, and add `docs/self-hosting/geolite2.md`: fetch the databases with a free MaxMind account, mount the directory read-only, point the variable at it. The compose file carries the bind-mount line commented out beside the app service. The admin notification told the reader to set MAXMIND_LICENSE_KEY in this repository's Actions secrets — something nobody running the published image can do, about a fallback that sends their sign-in addresses to a third party. It now names the mount, names the online provider it is actually querying, and links the runbook. Six locales. It also no longer claims the carrier column stays empty; the online tier has filled the carrier from the ISP field since v1.25.8. A blank GEOLITE2_DIR now resolves to the default instead of the process working directory: compose substitutes an unset variable to the empty string, which is not nullish. Refs #659
247 lines
10 KiB
JSON
247 lines
10 KiB
JSON
{
|
|
"$schema": "./env-manifest.schema.json",
|
|
"description": "Manifest of env vars required by a HealthLog production deployment. Used by `pnpm check-env` to catch missing/empty vars BEFORE a deploy ships. v1.4.42 W6 — born out of the v1.4.40 AP-2 .p8 install gap that shipped silently for 3 days.",
|
|
"groups": [
|
|
{
|
|
"name": "Core",
|
|
"description": "Database, app URL, encryption keys. The app refuses to boot without these.",
|
|
"required": true,
|
|
"variables": [
|
|
{
|
|
"name": "DATABASE_URL",
|
|
"purpose": "Postgres connection string. App boot fails if missing.",
|
|
"example": "postgresql://healthlog:<pw>@db:5432/healthlog?schema=public"
|
|
},
|
|
{
|
|
"name": "POSTGRES_PASSWORD",
|
|
"purpose": "Database password for the bundled `db` service in docker-compose.yml.",
|
|
"example": "<openssl rand -hex 32>"
|
|
},
|
|
{
|
|
"name": "NEXT_PUBLIC_APP_URL",
|
|
"purpose": "Public URL embedded in OAuth redirects, webhook callbacks, APNs deep-links."
|
|
},
|
|
{
|
|
"name": "APP_URL",
|
|
"purpose": "Server-side mirror of NEXT_PUBLIC_APP_URL; used by background workers."
|
|
},
|
|
{
|
|
"name": "ENCRYPTION_KEY",
|
|
"purpose": "AES-256-GCM key for at-rest encryption of provider tokens. 64 hex chars (32 bytes). Required unless ENCRYPTION_KEYS is set."
|
|
},
|
|
{
|
|
"name": "API_TOKEN_HMAC_KEY",
|
|
"purpose": "HMAC-SHA256 key for hashing external Bearer tokens AND web session cookies. 64 hex chars. Rotating it signs every browser session out on top of invalidating every issued token."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Withings OAuth",
|
|
"description": "Withings API credentials are entered per user in Settings → Integrations (no env-var path). Only the webhook shared secret is operator-level.",
|
|
"required": false,
|
|
"variables": [
|
|
{
|
|
"name": "WITHINGS_WEBHOOK_SECRET",
|
|
"purpose": "Random secret carried as a path segment in the Withings webhook callback URL. Generate with `openssl rand -hex 32`."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Polar OAuth",
|
|
"description": "Required to enable the Polar AccessLink integration. App boots without these but the Polar connect button stays disabled. Register an app at https://admin.polaraccesslink.com and set the redirect URI to ${NEXT_PUBLIC_APP_URL}/api/polar/callback.",
|
|
"required": false,
|
|
"allOrNone": true,
|
|
"variables": [
|
|
{
|
|
"name": "POLAR_CLIENT_ID",
|
|
"purpose": "Polar AccessLink OAuth client id."
|
|
},
|
|
{
|
|
"name": "POLAR_CLIENT_SECRET",
|
|
"purpose": "Polar AccessLink OAuth client secret."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Oura OAuth",
|
|
"description": "Required to enable the Oura Cloud v2 integration. App boots without these but the Oura connect button stays disabled. Register an app at https://cloud.ouraring.com/oauth/applications and set the redirect URI to ${NEXT_PUBLIC_APP_URL}/api/oura/callback.",
|
|
"required": false,
|
|
"allOrNone": true,
|
|
"variables": [
|
|
{
|
|
"name": "OURA_CLIENT_ID",
|
|
"purpose": "Oura Cloud OAuth client id."
|
|
},
|
|
{
|
|
"name": "OURA_CLIENT_SECRET",
|
|
"purpose": "Oura Cloud OAuth client secret."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "OIDC SSO login",
|
|
"description": "Required to enable SSO login against a self-hosted identity provider (Authentik, Keycloak, Authelia, Google Workspace, etc.). App boots without these but the login page's SSO button stays hidden. Register a confidential client with redirect URI ${NEXT_PUBLIC_APP_URL}/api/auth/oidc/callback.",
|
|
"required": false,
|
|
"allOrNone": true,
|
|
"variables": [
|
|
{
|
|
"name": "OIDC_ISSUER_URL",
|
|
"purpose": "OIDC provider issuer URL (discovery doc served at <issuer>/.well-known/openid-configuration)."
|
|
},
|
|
{
|
|
"name": "OIDC_CLIENT_ID",
|
|
"purpose": "OIDC confidential client id."
|
|
},
|
|
{
|
|
"name": "OIDC_CLIENT_SECRET",
|
|
"purpose": "OIDC confidential client secret."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "OIDC SSO options",
|
|
"description": "Optional tuning for an enabled OIDC provider. Each stands alone (deliberately NOT allOrNone — any subset is valid) and each has a safe default when unset.",
|
|
"required": false,
|
|
"variables": [
|
|
{
|
|
"name": "OIDC_SCOPES",
|
|
"purpose": "Space-separated scopes requested at authorization. Default: \"openid email profile\"."
|
|
},
|
|
{
|
|
"name": "OIDC_BUTTON_LABEL",
|
|
"purpose": "Label on the login page's SSO button. Default: \"Single Sign-On\"."
|
|
},
|
|
{
|
|
"name": "OIDC_ONLY",
|
|
"purpose": "\"true\" disables password/passkey login server-side (only effective when the OIDC provider group is fully configured). Default: additive login. Locks the native iOS app out of sign-in."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "AI Provider",
|
|
"description": "Optional runtime policy and output tuning for user-configured AI providers. Each variable stands alone and has a safe default when unset.",
|
|
"required": false,
|
|
"variables": [
|
|
{
|
|
"name": "ALLOW_LOCAL_AI_PRIVATE_HOSTS",
|
|
"purpose": "Local-AI SSRF opt-in. Unset rejects private/internal hosts; set an exact comma-separated host allowlist or \"true\" to allow any private host."
|
|
},
|
|
{
|
|
"name": "INSIGHTS_MAX_TOKENS",
|
|
"purpose": "Daily-briefing output-token ceiling. Default 2500, clamped to 500-8000."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Offline geolocation",
|
|
"description": "Optional offline GeoLite2 tier for the admin login overview. Unset, the runtime reads /opt/geolite2 (the path the image bakes its own copy into) and falls back to the online provider when the databases are absent.",
|
|
"required": false,
|
|
"variables": [
|
|
{
|
|
"name": "GEOLITE2_DIR",
|
|
"purpose": "Directory holding GeoLite2-City.mmdb + GeoLite2-ASN.mmdb. Point it at a read-only mount to bring your own databases. Default /opt/geolite2."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "APNs (iOS push)",
|
|
"description": "All four core APNS_* vars together. Missing any disables APNs silently — the v1.4.40 AP-2 gap that motivated this manifest. allOrNone trips the synthetic <all-or-none> required row whenever 1-3 of the 4 are set.",
|
|
"required": false,
|
|
"allOrNone": true,
|
|
"variables": [
|
|
{
|
|
"name": "APNS_KEY_ID",
|
|
"purpose": "10-char Key ID from Apple Developer."
|
|
},
|
|
{
|
|
"name": "APNS_TEAM_ID",
|
|
"purpose": "10-char Team ID from Apple Developer."
|
|
},
|
|
{
|
|
"name": "APNS_BUNDLE_ID",
|
|
"purpose": "iOS app bundle id, e.g. dev.healthlog.ios."
|
|
},
|
|
{
|
|
"name": "APNS_KEY",
|
|
"purpose": "APNs auth key (.p8 contents). Use APNS_KEY for 12-factor env-block deploys; APNS_KEY_FILE is the filesystem alternative — at least one of the two must be set.",
|
|
"anyOf": ["APNS_KEY", "APNS_KEY_FILE"]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Deploy webhook",
|
|
"description": "Coolify -> HealthLog deploy-result webhook. Without this, the auto-deploy success/failure feedback loop is broken.",
|
|
"required": false,
|
|
"variables": [
|
|
{
|
|
"name": "DEPLOY_WEBHOOK_SECRET",
|
|
"purpose": "Shared secret for the X-Deploy-Webhook-Secret header on POST /api/internal/deploy-webhook. 64 hex chars."
|
|
},
|
|
{
|
|
"name": "DEPLOY_LOGS_URL",
|
|
"purpose": "URL of the deployment dashboard (e.g. the Coolify UI) linked in deploy-failure notifications. Optional; when unset the notification omits the link."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "TLS leaf pin monitor",
|
|
"description": "Baseline for the TLS leaf SPKI-change alarm. The native client pins the served leaf certificate; without this the monitor logs the served pin but cannot alarm on a rotation. See docs/ops/tls-cert-pin.md.",
|
|
"required": false,
|
|
"variables": [
|
|
{
|
|
"name": "TLS_LEAF_SPKI_PINS",
|
|
"purpose": "Comma-separated known-good leaf SPKI pins, base64(sha256(DER subjectPublicKeyInfo)) — the same value the native client pins. Hold both the current and the next pin during a dual-pin renewal window."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "SMTP (email channel)",
|
|
"description": "Operator SMTP transport for the email notification channel (v1.17.1). All of SMTP_HOST, SMTP_PORT, SMTP_FROM together enable the channel; SMTP_USER/SMTP_PASS are optional (unauthenticated relay). Missing any of the three core vars disables email silently — the channel is never offered and the Settings card hides itself.",
|
|
"required": false,
|
|
"allOrNone": true,
|
|
"variables": [
|
|
{
|
|
"name": "SMTP_HOST",
|
|
"purpose": "SMTP server hostname, e.g. smtp.example.com."
|
|
},
|
|
{
|
|
"name": "SMTP_PORT",
|
|
"purpose": "SMTP port. 587 for STARTTLS (SMTP_SECURE unset/false), 465 for implicit TLS (SMTP_SECURE=true)."
|
|
},
|
|
{
|
|
"name": "SMTP_FROM",
|
|
"purpose": "From address for outbound mail, e.g. \"HealthLog <noreply@example.com>\"."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Off-host backups",
|
|
"description": "Required ALL-OR-NONE: any of these without the rest disables backups silently.",
|
|
"required": false,
|
|
"allOrNone": true,
|
|
"variables": [
|
|
{
|
|
"name": "BACKUP_S3_ENDPOINT",
|
|
"purpose": "S3-compatible endpoint URL."
|
|
},
|
|
{
|
|
"name": "BACKUP_S3_BUCKET",
|
|
"purpose": "Bucket name."
|
|
},
|
|
{
|
|
"name": "BACKUP_S3_ACCESS_KEY",
|
|
"purpose": "S3 access key."
|
|
},
|
|
{
|
|
"name": "BACKUP_S3_SECRET_KEY",
|
|
"purpose": "S3 secret key."
|
|
},
|
|
{
|
|
"name": "BACKUP_ENCRYPTION_KEY",
|
|
"purpose": "Separate AES key for the backup body — MUST NOT match ENCRYPTION_KEY."
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|