Self-hosted personal & family health tracker. Medications, conditions, allergies, labs, vitals, procedures, vaccines, appointments and notes — for you and your dependents — in a single Docker container. All data stays in one SQLite database and an uploads folder on your server. No cloud services required.
- Medications — dosages, schedules, active/inactive history, AI interaction checks*
- Conditions, allergies, procedures, vaccines — full clinical history
- Labs — visits and results with reference ranges, plus AI-powered PDF parsing of lab and vaccine reports*
- Vitals — blood pressure, heart rate, weight, glucose and more, with reference ranges and trend charts
- Family & dependents — track children or family members under your account, with a transition flow when they grow up
- Sharing — share selected sections of your (or a dependent's) health record with another user, with expiry
- Delegates — grant another user read-only or read-write access to manage a record
- Medical-history import* — upload a doctor-provided history PDF (yours or a dependent's) and AI extracts medications, conditions, allergies, procedures, vaccines, and lab results in one pass; large documents are processed in page chunks, and a review screen flags anything already on record before import
- Fitness tracking — log workouts manually or via the API (set shorthand like
185x5 x3), with history, trend charts (est. 1RM, PR badges), weekly rollups, goals, and an exercise catalog - API access — personal access tokens with scoped permissions for the REST API (
/api/v1/...), including read endpoints for every domain - Device integrations / bring your own bridge — push metrics from any device (CPAP, smart scale, watch, CGM, …) via
POST /api/v1/vitalsand/api/v1/vitals/batchwith idempotent upserts; see docs/API.md. Every instance self-documents at/docs/api(human cookbook) and/api/v1/openapi.json(OpenAPI 3.1) — both public, API-shape only - Health Connect ingestion (Android) — receive Health Connect data from the Life Dashboard companion app over an HMAC-signed webhook: daily activity totals land in vitals and food-tracker nutrition becomes daily calories and macros. A new integration starts in inventory mode — every record is stored and catalogued, but nothing is written to your health data until you approve the exact apps you trust; see docs/health-connect.md
- AI health assistant* — natural-language questions about your data, health summaries
- Oura Ring sync* — sleep, heart rate and activity data
- Sign in with Google* — alongside built-in email/password auth
* Optional. AI features need an ANTHROPIC_API_KEY, Google login needs a Google OAuth client, Oura sync needs an Oura OAuth client. Each feature is hidden in the UI until its keys are configured — the core tracker is fully functional without any of them.
All screenshots show a demo account with fictional data.
Vitals & wearables — every metric grouped by domain, with trend sparklines, reference-range bars, and per-device source badges. Data flows in from bridges, the API, or manual entry:
Focus view — the day-to-day question ("how am I doing?") answered at a glance: recovery verdict, body-composition trend, and weekly activity vs. your 30-day norms:
Medications — active/past history with categories, plus AI interaction checks that run automatically when your med list changes:
Fitness — workout history with per-set detail, working-set summaries, trends with estimated 1RM and PR badges, weekly rollups and goals:
Medical-history import — upload a doctor-provided PDF; AI extracts medications, conditions, allergies, procedures, vaccines and labs in one pass, deduplicates against what's already on record, and imports only what you approve:
docker run -d \
--name healthtrack \
-p 3000:3000 \
-v ./data:/data \
-e APP_URL=http://localhost:3000 \
ghcr.io/ezekielthemad/healthtrack:latestOpen http://localhost:3000 and register — the first user becomes the instance admin. After that, registration is invite-only by default: create single-use invite links under Settings → Invites to add family members. (Set SIGNUPS_ENABLED=true to open registration to anyone, or false to close it completely.)
Port 3000 already in use? Map a different host port and point
APP_URLat it, e.g.-p 3005:3000with-e APP_URL=http://localhost:3005. The container always listens on 3000 internally; only the host side changes.
See docker-compose.yml:
docker compose up -dHealthTrack ships a Community Applications template: unraid/healthtrack.xml. See docs/UNRAID.md for install instructions (via CA or manual template URL).
Registration is invite-only by default — nobody can create an account on your instance without your say-so. The ways to bring people in:
- Invite links (new accounts) — as admin, go to Settings → Invites,
create a link (optionally note who it's for) and send it. Each link is
single-use and expires after 7 days. The recipient registers with email
- password; afterwards they can also use "Sign in with Google" on the same email (accounts link automatically). Revoke unused links any time.
- Dependents (no account needed) — track children or family members as profiles under your own account (Settings → Family & Dependents), with a transition flow to hand the record over when they grow up.
- Delegates — grant another account read-only or read-write access to manage your record (Settings → Delegate Access).
- Sharing — share selected sections of a record with another account, with expiry (Settings → Health Sharing).
Prefer open registration (e.g. a trusted LAN)? Set SIGNUPS_ENABLED=true.
Set SIGNUPS_ENABLED=false to close registration completely, invites included.
All configuration is via environment variables (see .env.example):
| Variable | Required | Default | Purpose |
|---|---|---|---|
APP_URL |
yes | http://localhost:3000 |
Absolute URL of the instance — must match what users browse to; used for auth callbacks |
AUTH_SECRET |
no | auto-generated to /data/keys |
Session signing secret |
ENCRYPTION_KEY |
no | auto-generated to /data/keys |
Encrypts stored OAuth tokens |
SIGNUPS_ENABLED |
no | invite-only | Unset: first account registers freely (becomes admin), everyone after needs a single-use invite link (Settings → Invites). true: open registration. false: fully closed |
ANTHROPIC_API_KEY |
no | — | Enables AI features (summaries, queries, PDF parsing, interaction checks) |
ANTHROPIC_MODEL |
no | current Sonnet | Claude model for reasoning features (summaries, queries, interaction checks) — e.g. claude-opus-4-8 |
ANTHROPIC_MODEL_EXTRACTION |
no | current Sonnet | Claude model for PDF extraction — a cheaper model like claude-haiku-4-5 works well |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
no | — | Enables "Sign in with Google" |
OURA_CLIENT_ID / OURA_CLIENT_SECRET |
no | — | Enables Oura Ring sync |
OURA_SYNC_ENABLED |
no | true when Oura is configured |
Startup + recurring native Oura sync toggle |
OURA_SYNC_INTERVAL_HOURS |
no | 4 |
Native scheduler interval; every run self-heals an inclusive 7-day window |
PUID / PGID |
no | 99 / 100 |
Ownership of files under /data (Unraid conventions) |
TZ |
no | Etc/UTC |
Valid IANA timezone for the container and Oura calendar-date sync windows |
HealthTrack serves plain HTTP on port 3000; put your reverse proxy of choice (Nginx Proxy Manager, Caddy, Traefik, Cloudflare Tunnel, …) in front of it for HTTPS. APP_URL must exactly match the URL users browse to (scheme, host, port) or login callbacks will fail.
Oura OAuth tokens are encrypted in each user's connected_sources row and are
the only credential store. Manual Sync Now and the native startup scheduler
share the same per-user refresh lock, so concurrent requests cannot rotate a
refresh token twice. The scheduler runs once on Node startup and at the
configured interval (4 hours by default), isolates failures per user, and
backfills an inclusive seven-day self-heal window. The OAuth callback keeps its
30-day initial backfill. HealthTrack writes directly to its canonical vitals
store with source=oura; no Notion or bridge writes are involved.
Oura date windows use the HealthTrack TZ setting so a sync near UTC midnight
still requests the owner's current local calendar day. TZ must be a valid IANA
timezone (for example America/New_York); the portable default is Etc/UTC.
Serve the app on one public hostname. If your proxy answers on both example.com and www.example.com (a common default), pick one, put it in APP_URL, and have the other redirect to it. Auth cookies are scoped to the exact host that set them, while OAuth redirect URIs are built from APP_URL — so a visitor who starts on the hostname you didn't configure gets sent back to the one you did, without their cookie, and Google sign-in fails with ?error=state_mismatch. HealthTrack now redirects browser page requests to APP_URL's hostname for you (API routes under /api/ are left alone, as are localhost and LAN addresses), but fixing it at the proxy or DNS layer is still cleaner.
Built-in email/password sign-in works anywhere, including a bare LAN IP — nothing extra to configure. The optional social logins have provider-specific requirements:
-
Sign in with Google requires a public HTTPS URL on a real hostname. Google's OAuth rejects
http://and raw IP-address redirect URIs (onlyhttp://localhostis exempt), so it will not work against something likehttp://192.168.1.50:3000— you must front the app with HTTPS (reverse proxy or tunnel) and setAPP_URLto that address. Then, in the Google Cloud Console OAuth client, add this Authorized redirect URI:APP_URL/api/auth/callback/google -
Oura Ring is more permissive (a LAN IP is fine). Register its redirect URI in the Oura developer portal — not the Google console; they are separate providers with separate credentials:
APP_URL/api/oura/callback
In every case APP_URL must exactly match the URL your browser uses (scheme, host and port), or the OAuth round-trip fails. Restart the container after changing APP_URL. The first user to sign in — by any method — becomes the admin.
Everything lives under /data: the SQLite database (healthtrack.db), uploaded PDFs (uploads/) and auto-generated secrets (keys/). To back up: stop the container, copy the /data directory, start it again. Restore is the reverse.
npm install
npm run dev # http://localhost:3000, state lands in ./data
npm test # vitest
npm run lint
npm run buildLocal development state (SQLite db, uploads, generated keys) lives in ./data (override with DATA_DIR). Database tests run in the node environment — see existing *.test.ts files for the environment pragma convention.
HealthTrack helps you organize health information. It is not a medical device and does not provide medical advice, diagnosis or treatment — always consult a qualified healthcare professional. AI-generated content can be wrong. You run this software self-hosted, on your own infrastructure, at your own responsibility; review SECURITY.md before exposing an instance to the internet.





