Draft: not yet reviewed by a clinician
Use this page to prepare questions for your care team. It does not replace their advice. Your own targets and doses come from your nephrologist, diabetes team and renal dietitian.
Configuration¶
kidney-health reads its configuration from environment variables once, at start-up. A wrong value stops the app with a message that names the variable; it never starts half-configured (design note 01).
Rules that apply to every variable¶
- Where to set them:
Environment=lines in the Quadlet unit,deploy/.envfor compose, orenv:in the Kubernetes Deployment (deployment guide). - Secrets are files. Every secret has a
NAME_FILEform that points to a file (a Podman secret, composesecrets:, a Kubernetes Secret volume). Setting bothNAMEandNAME_FILEis a start-up error; an empty file means "unset"; trailing newlines are ignored. Environment variables leak throughinspectoutput and crash dumps, so use files (operator security guide). - Set by the server means locked. A setting given here shows in the app as "Set by the server" and
cannot be changed in Settings (design note 07), unless you list it in
SETTINGS_INITIAL_ONLY(below). - Booleans accept
true/false,1/0,yes/no,on/off.
Address and network¶
| Variable | Default | Purpose |
|---|---|---|
PUBLIC_URL |
unset | The address people type, scheme included, for example https://food.home.example.net. Its host passes the Host check, browsers' Origin must match it, and invite and reset links use it. Set it whenever you use a host name. |
ALLOWED_HOSTS |
localhost, IP literals and the PUBLIC_URL host |
Extra host names (comma list; *.example.org wildcards). Any other Host gets 400 Unknown host (DNS-rebinding defense). |
TRUSTED_PROXIES |
127.0.0.1,::1 |
Addresses whose X-Forwarded-For and -Proto are believed. Depends on your engine (Security). FORWARDED_ALLOW_IPS is ignored. |
MAX_BODY_BYTES |
1 MiB | Largest JSON request. |
MAX_IMAGE_BYTES |
4 MiB | Largest photo upload (v0.3 photo features). |
HSTS_MAX_AGE |
31536000 | Sent only over HTTPS; 0 turns HSTS off. |
ENABLE_API_DOCS |
false |
Serves /docs, /redoc and /openapi.json (alias DOCS_ENABLED). For a development machine only: they answer without a sign-in and load scripts from a CDN. |
PWA_ENABLED |
true |
false is the kill switch for the offline service worker. |
LOG_LEVEL |
INFO |
DEBUG, INFO, WARNING, ERROR, CRITICAL. Secrets are redacted either way. |
DATA_DIR |
/data in the image, ./data-local outside |
Where kidney.db lives. |
FOODS_JSON |
the image's data/foods.json |
The builtin food list. |
Secrets¶
| Variable | Default | Purpose |
|---|---|---|
SECRET_KEY_FILE |
auto-generated $DATA_DIR/secret.key, with a warning |
Encrypts stored API keys. One key per line, at least 32 characters; the first line is current. Mount it from the engine's secret store, outside the data volume (Backups). |
USDA_API_KEY_FILE |
unset | The shared USDA FoodData Central key; enables "Search USDA" (free key; DEMO_KEY allows 30 requests an hour) (USDA). |
Sign-in and accounts¶
| Variable | Default | Purpose |
|---|---|---|
AUTH_MODE |
local |
local: accounts with passwords. proxy: identity from your reverse proxy. none: no sign-in; everything is one admin user and a red banner says so. |
ADMIN_USERNAME + ADMIN_PASSWORD_FILE |
unset | First run only: create the admin from a file instead of the setup code (GitOps). Ignored once an admin exists. |
APP_PASSWORD_FILE |
unset | Deprecated (v0.2 HTTP Basic). On the first v0.3 start with no admin it becomes the admin's password; then it is ignored. |
SETUP_CODE_TTL_MINUTES |
60 | Lifetime of the first-run setup code (5–1440). |
SESSION_IDLE_DAYS / SESSION_MAX_DAYS |
14 / 30 | Sign-in lifetime (1–30 each). |
REAUTH_MINUTES |
10 | How long a password re-entry unlocks sensitive actions. |
BACKUP_REMINDER_DAYS |
7 | Server administration reminds an admin when the last backup is older than this (0–365; 0 turns it off). See Backups. |
PASSWORD_MIN_LENGTH |
15 | 8–64. Below 15 logs a warning: 15 is the NIST minimum for a password used alone (NIST). |
PASSWORD_BREACH_CHECK |
false |
true checks new passwords against Have I Been Pwned (5 hash characters leave the server). |
PASSWORD_HASH |
argon2id |
scrypt only where Argon2id is unavailable. |
LOGIN_IP_MAX_FAILURES |
20 | Failed sign-ins per address per 10 minutes before a 10-minute block. |
ALLOW_INSECURE_HTTP |
automatic | Plain-HTTP sign-in from other machines is allowed while one account exists and refused from the second. |
INSTANCE_NAME |
Kidney Health |
Shown in the app. |
REGISTRATION_MODE |
invite |
invite (single-use links), closed (admin creates accounts), open (anyone who reaches the page; HTTPS required). |
AUDIT_RETENTION_DAYS |
365 | How long security events are kept. |
USDA_SHARED_DAILY_LIMIT |
200 | Shared-key USDA lookups per person per day (0 = unlimited). |
Proxy sign-in (AUTH_MODE=proxy)¶
| Variable | Purpose |
|---|---|
TRUSTED_PROXY_USER_HEADER |
The identity header your proxy sets, for example Remote-User. Required. |
TRUSTED_PROXY_SECRET_FILE |
A long random value your proxy sends as X-Proxy-Secret. Required: without it the app refuses to start. |
TRUSTED_PROXY_SECRET_OPTIONAL |
true only for tailscale serve, which cannot add headers; logged as a warning. |
TRUSTED_PROXY_GROUPS_HEADER, TRUSTED_PROXY_ADMIN_GROUP |
Members of the admin group become admins. |
TRUSTED_PROXY_NAME_HEADER |
Display name. |
PROXY_AUTO_CREATE_USERS |
false: the admin creates each account first. |
PROXY_LOGOUT_URL |
Where "Sign out" goes. |
Proxy mode refuses to start if TRUSTED_PROXIES contains 0.0.0.0/0 or ::/0
(design note 07).
Food data¶
| Variable | Default | Purpose |
|---|---|---|
OFF_ENABLED |
false |
Barcode lookups contact Open Food Facts. The admin can also switch it on in Settings or on the setup screen. |
OFF_CONTACT |
the project URL | Goes in the User-Agent, as Open Food Facts asks (Open Food Facts); an email address is better. |
OFF_BASE_URL |
https://world.openfoodfacts.org |
Env only (never editable in the app); https:// required. |
OFF_RATE_PER_MINUTE |
10 (at most 15) | Server-wide lookup pace. |
BARCODE_NEGATIVE_TTL_HOURS |
24 | How long "not found" is remembered. |
USDA_BRANDED_BARCODE |
true |
Also look barcodes up in USDA FoodData Central (needs a USDA key). |
Each of these except OFF_BASE_URL is also a server setting an admin can change in Settings; setting
the variable locks it. Each person still decides whether their own scans go to Open Food Facts
(Settings → Food data).
AI¶
| Variable | Default | Purpose |
|---|---|---|
AI_ENABLED |
false |
Master switch (also Server administration → Server settings). While off, every AI route answers "not found" and nothing is sent anywhere. |
AI_PROVIDER, AI_BASE_URL, AI_MODEL |
unset | The server's shared provider: a preset (openai, openrouter, nous_portal, ollama, lmstudio, llamacpp, vllm, litellm, hermes, openai_compatible). Admins can add more in Settings → AI ideas. |
AI_API_KEY_FILE |
unset | The shared provider's key (AI_API_KEY also works; OPENAI_API_KEY_FILE for openai). Never stored in the database. |
AI_VISION_MODEL |
empty | Set it to enable label and plate photos. |
AI_PROVIDER_ROLE |
unset | local or cloud: which role the AI_PROVIDER provider has. local needs a private host (listed in AI_PRIVATE_HOSTS, a name by name) and is checked at start-up. Each AI feature (photos, meal ideas, describing a meal, Ask) uses the local or the cloud AI, whichever you pin it to; the default is local, so a feature stays off until a local AI exists. |
AI_CLOUD_PROVIDER, AI_CLOUD_BASE_URL, AI_CLOUD_MODEL, AI_CLOUD_VISION_MODEL |
unset | A second shared provider that is always the cloud AI, set the same way as AI_PROVIDER (any preset except hermes). With it set, AI_PROVIDER_ROLE must be local, and the cloud role cannot be changed in the app. It may sit on your LAN (a LiteLLM proxy with a cloud-only key); it is then listed in AI_PRIVATE_HOSTS too, and the start stops if it is not. Lab report photos never go to it. |
AI_CLOUD_API_KEY_FILE |
unset | Its key (AI_CLOUD_API_KEY also works). Never stored in the database. |
AI_CLOUD_UPSTREAM |
unset | Required when the cloud AI is a server you run (litellm, vllm, openai_compatible and the other non-cloud presets): the plain name of the company it forwards to, for example Anthropic. People are told "Goes through your admin's LiteLLM proxy to Anthropic. Anthropic receives what is sent, under its own terms; your admin can read it too." A new name asks everyone again. Ignored, with a warning, for openai, openrouter and nous_portal. |
AI_CLOUD_TIMEOUT_S, AI_CLOUD_MAX_TOKENS, AI_CLOUD_CONTEXT_TOKENS, AI_CLOUD_STRUCTURED_OUTPUT, AI_CLOUD_REASONING_EFFORT |
the preset's, 1500, 32768, auto, the preset's |
Like the AI_ variables of the same name. The prompt budget is 32,768 tokens even for a self-hosted preset such as litellm. |
AI_ROUTE_RECIPES |
unset (local) |
AI recipes, which send a recipe's foods and amounts (never its name, note, steps or link). The same values as AI_ROUTE_MEAL_IDEAS. |
AI_RECIPE_STEPS |
false |
Cooking steps written by AI in AI recipes. Leave it off until a live run of scripts/ai_eval.py on your model shows no unsafe step. |
AI_ROUTE_LABS |
unset (local) |
Reading a photo of a lab report: local or off only (cloud and any stop the start): a report can show a name and a date of birth, so it is read only by your local AI and is off without one. |
AI_ROUTE_PHOTOS, AI_ROUTE_MEAL_IDEAS, AI_ROUTE_DESCRIBE, AI_ROUTE_CHAT |
unset (local) |
Pin a feature to local, cloud, off or any (the older rule: whichever shared provider a person chose). Photos, meal ideas and describing a meal also take local_then_cloud: the local AI first, and the cloud AI only when the local answer fails the schema or the app's rules (never when the local AI is unreachable, slow, over quota or declines; only for people who agreed to the cloud host); Ask does not (a value of local_then_cloud for AI_ROUTE_CHAT stops the start). Leave empty to choose in Server administration → AI providers → Which AI does each feature use?; a value here locks the choice. A server that already had a provider keeps any until an admin chooses. |
AI_EVAL_REPORTS_DIR |
unset | A folder, mounted read-only, with the *.json reports that scripts/ai_eval.py writes. Server administration → AI providers → How well each AI is doing then says, for each feature that is on, whether the model it uses passed the evaluation for the current prompts, and the start-up log warns about each one that failed or has no report. Unset: the screen says there is no reports folder. Mount the folder with Volume=...:ro (Quadlet), volumes: (compose) or a read-only volumeMount (Kubernetes); see the AI guide. |
AI_PRIVATE_HOSTS |
empty | host:port list that shared providers may reach on private addresses, for example ollama:11434. No wildcard; the old ALLOW_PRIVATE_AI_HOSTS is refused. |
AI_DENY_CIDRS |
empty | Addresses AI calls may never reach: add your own public (WAN) address. |
AI_ALLOW_USER_KEYS / AI_ALLOW_USER_BASE_URL |
true / false |
Whether people may add their own key, or their own (public, HTTPS, port 443) address. |
AI_SHARED_DAILY_LIMIT |
30 | Shared AI calls per person per day (photos and connection tests count). |
AI_MAX_CONCURRENCY |
2 | AI calls at the same time on the whole server (one per person). |
AI_AUDIT_RETENTION_DAYS |
30 | How long request and response bodies are kept (0 = metadata only). |
AI_VISION_PLATE_ENABLED / AI_VISION_ALLOW_AGENT |
false / false |
Plate photos; photos to a Hermes agent (which must also pass its tool check before every photo). |
AI_CHAT_ENABLED |
false |
The Ask chat (also Server administration → Server settings): needs AI on; people who opted in get an Ask tab where the AI looks up their own numbers and the app checks every answer. One question can use up to 7 AI calls, each counted in the daily limit. While off, the Ask tab is not shown. |
AI_VOICE_ENABLED |
false |
The Ask microphone (also Server administration → Server settings): needs Ask on and a transcription server (STT_BASE_URL). While off, there is no microphone. |
STT_BASE_URL, STT_MODEL, STT_API_KEY_FILE |
unset | The speech-to-text server for the microphone: one OpenAI-compatible server (a local container such as deploy/compose.stt-whisper.yaml, or a cloud service), its model name and, if it wants one, its key as a file. A server on your network must be listed in AI_PRIVATE_HOSTS, like an Ollama host. Recordings are turned into text and deleted, never stored. STT_TIMEOUT_S (60), STT_DAILY_LIMIT (100 per person per day) and STT_MAX_AUDIO_BYTES (1.5 MiB) tune it. |
AI_HTTP_PROXY |
unset | Explicit egress proxy; HTTP(S)_PROXY is ignored for AI on purpose. |
ASSISTANT_ENABLED |
false |
Assistant access (also Server administration → Server settings): each person may make personal tokens for a chat assistant that reads their own food log and targets and, if the person ticks "Can also save food and recipes" for that token, saves food and recipes, and, if the person ticks "Also let it remember tastes" for that token, remembers likes and dislikes (at most 30 changes a day per token; never anything about treating a low). While off, no token works and none can be made. |
Also AI_TIMEOUT_S, AI_VISION_TIMEOUT_S (120), AI_MAX_TOKENS (1500), AI_STRUCTURED_OUTPUT (auto),
AI_REASONING_EFFORT, AI_CONTEXT_TOKENS, AI_MAX_RESPONSE_BYTES, AI_OPENROUTER_ZDR and MAX_IMAGE_BYTES
(design note 04). Setup recipes (Ollama next to the app, Kubernetes, a dedicated tool-free Hermes
Agent profile, OpenAI, OpenRouter), the address rules and troubleshooting are in the repository's
AI guide.
Phone shortcuts¶
| Variable | Default | Purpose |
|---|---|---|
PUSH_ENABLED |
false |
Phone shortcuts (also Server administration → Server settings): each person may make device tokens (Settings → Account → Your data → Phone shortcuts) for an iPhone Shortcut that adds their weight and blood pressure to their own log at POST /push/v1/body. A token can only add; it reads nothing. While off, the route answers 401 to everyone. The phone must reach the server (home network or your VPN); if a reverse proxy lists the paths it passes on, add /push/v1/. Nothing leaves the server for it (design note 12; From a phone shortcut). |
AUTH_REQUIRE_TWO_STEP_ADMINS |
false |
Admins must use two-step sign-in (also Server administration → Server settings): an admin opens Server administration only after turning it on in Settings → Account → Two-step sign-in. You can switch it on there only once your own two-step sign-in is on. No effect with a sign-in proxy (design note 17). |
Meal guidance¶
GUIDANCE_ENABLED (true), GUIDANCE_POOL_PER_ROLE (200), GUIDANCE_BEAM_WIDTH (16)
(design note 06).
The handbook at /learn¶
| Variable | Default | Purpose |
|---|---|---|
HANDBOOK_DIR |
/app/learn in the image; handbook/site in a checkout |
The built handbook, served at /learn/ with no sign-in. Without an index.html there, /learn answers 404 and the app's Learn links go to HANDBOOK_PUBLIC_URL, or are hidden when that is not set. |
HANDBOOK_PUBLIC_URL |
empty | A published copy (for example on GitHub Pages), https:// or http:// only: where the Learn links go when this server has no handbook, and the "public copy" link in Settings → About. |
The image builds the handbook in a separate stage, so it needs no internet at runtime (design note 08; Building the handbook). The app reads the site once when it starts, to compute the handbook's security policy, so restart it after rebuilding the handbook in place. A broken build (a page that is not UTF-8, or far more inline scripts than the theme uses) is not served: the log says why, and the food log itself keeps working. To serve the handbook from its own host name instead, see Security and the operator security guide.
Set it up by code¶
Every setting in Server administration → Server settings has a variable, so a script or an AI agent
can set a server up with no clicks. With ADMIN_USERNAME and ADMIN_PASSWORD_FILE the first admin is
made from a file too (Users and keys).
A variable locks its setting. To give only a first value, list the variable in
SETTINGS_INITIAL_ONLY (a comma list, or * for all). On the first start that sees it, the server
writes the value and notes it in the activity log. After that an admin can change it in the app, and a
restart keeps their choice; changing the variable later does nothing (design note 20).
| Variable | Default | Purpose |
|---|---|---|
SETTINGS_INITIAL_ONLY |
empty | The variables that are first values, not locks. A name that is not a setting's variable stops the start. |
INSTANCE_NAME, REGISTRATION_MODE, INVITE_TTL_DAYS, AUDIT_RETENTION_DAYS |
Kidney Health, invite, 7, 365 |
The server's name, who may make an account, how long an invite link works, how long the activity log is kept. |
USDA_SHARED_ENABLED, USDA_ALLOW_USER_KEYS, USDA_SHARED_DAILY_LIMIT |
true, true, 200 |
The server's USDA key: shared or not, people's own keys allowed or not, searches per person per day. |
LAB_RULES_ENABLED, LAB_FRESH_DAYS_POTASSIUM, _PHOSPHATE, _ALBUMIN, _BICARBONATE, DEFAULT_ACTIVITY |
true, 90, 90, 180, 180, inactive |
Whether lab results change suggested targets, how long each result counts, and the activity level used until a person picks theirs. |
UI_THEME, LAB_UNITS, WEIGHT_UNITS |
system, us, lb |
The theme and units for people who have not picked their own. A lock stops people picking, so list these in SETTINGS_INITIAL_ONLY. |
python -m app.admin env-template prints all of these as a .env file to fill in, with each one's values and
default (--initial-only starts it with SETTINGS_INITIAL_ONLY=*). Run python -m app.admin check in the container
before the first start: it stops on any mistake and
lists which settings are locked and which are first values. Each person's own choices, like their
targets and whether they use AI, stay theirs to make in the app.
Example: Quadlet behind Caddy on the same host¶
Environment=SECRET_KEY_FILE=/run/secrets/secret_key
Environment=PUBLIC_URL=https://food.home.example.net:8443
Environment=TRUSTED_PROXIES=<address> # the peer address the app's log shows for Caddy
Environment=AUTH_MODE=local
If something goes wrong¶
- The app exits at start with a message naming a variable: fix that variable; the message says how.
SECRET_KEY_FILE points to ..., which does not exist: create the secret, or fix its file mode.- A setting is grayed out in the app: it is set here; change it here and restart, or list it in
SETTINGS_INITIAL_ONLYto make it a first value. - More: Troubleshooting.
Related pages¶
Sources¶
- Design note 01 §5.5 and design note 07 §4.3: the variables and their rules.
- Design note 03 R10; design note 04 R4; design note 06 §4.14; design note 08 §4.6.
- Design note 12 §4.4: the
PUSH_ENABLEDswitch. - Design note 20: first values from the environment.
- Deployment guide; operator security guide.
- NIST SP 800-63B-4; USDA FoodData Central API guide; Open Food Facts API.
Not yet reviewed by a clinician. Facts and sources last checked 2026-10-29.