HealthTrack

HealthTrack

Docker app from EzekielTheMad's Repository

Overview

Self-hosted personal health tracker. Track medications, conditions, allergies, labs, vitals, procedures, vaccines, appointments and notes for yourself and your dependents — all data stays in a single SQLite database and uploads folder under /data on your server. No cloud services required. The first registered user becomes the instance admin; after that, registration is invite-only by default (create single-use invite links under Settings, or set SIGNUPS_ENABLED to true to open registration). Optional integrations (each enabled only when its keys are set): Anthropic API key for AI health summaries, natural-language queries and lab/vaccine PDF parsing; Google OAuth for "Sign in with Google"; Oura for ring data sync. Back up /data to back up everything.

HealthTrack

License: MIT CI

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.

HealthTrack dashboard — AI health overview, quick stats with trends, active medications and lab flags

Features

  • 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/vitals and /api/v1/vitals/batch with 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.

Screenshots

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:

Vitals — all metrics view with trends and reference ranges

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:

Vitals — focus view with recovery, body composition and activity summaries

Medications — active/past history with categories, plus AI interaction checks that run automatically when your med list changes:

Medications with AI interaction status bar

Fitness — workout history with per-set detail, working-set summaries, trends with estimated 1RM and PR badges, weekly rollups and goals:

Fitness history with expanded strength session

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:

Import medical history review screen with dedupe badges

Quick start

docker run -d \
  --name healthtrack \
  -p 3000:3000 \
  -v ./data:/data \
  -e APP_URL=http://localhost:3000 \
  ghcr.io/ezekielthemad/healthtrack:latest

Open 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_URL at it, e.g. -p 3005:3000 with -e APP_URL=http://localhost:3005. The container always listens on 3000 internally; only the host side changes.

Docker Compose

See docker-compose.yml:

docker compose up -d

Unraid

HealthTrack ships a Community Applications template: unraid/healthtrack.xml. See docs/UNRAID.md for install instructions (via CA or manual template URL).

Adding users & family

Registration is invite-only by default — nobody can create an account on your instance without your say-so. The ways to bring people in:

  1. 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.
  2. 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.
  3. Delegates — grant another account read-only or read-write access to manage your record (Settings → Delegate Access).
  4. 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.

Configuration

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

Reverse proxy / HTTPS

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 architecture

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.

Authentication & OAuth

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 (only http://localhost is exempt), so it will not work against something like http://192.168.1.50:3000 — you must front the app with HTTPS (reverse proxy or tunnel) and set APP_URL to 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.

Backup

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.

Development

npm install
npm run dev     # http://localhost:3000, state lands in ./data
npm test        # vitest
npm run lint
npm run build

Local 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.

For local production-mode testing, explicitly pin DATA_DIR to an isolated scratch directory: the default switches to /data when NODE_ENV=production, so otherwise you may test against a different database. Never use a live health data directory as a test fixture.

Container implementation, image workflow, project release checks and historical release lessons are in docs/RELEASE.md. This is documentation, not deployment or publication authorization.

Medical-history extraction

Large medical-history PDFs are split with pdf-lib into 25-page chunks once past the threshold in chunk-pdf.ts. Extraction results are merged with duplicate collapsing and annotated against existing records before the user reviews and approves an import. A failed chunk becomes a page-range warning; if every chunk fails, extraction fails. Review warnings for missing information rather than treating a partial result as complete. ANTHROPIC_MODEL_EXTRACTION can differ from the reasoning model to manage cost. Long extraction requests must fit your proxy's timeout; the application route's duration setting does not override an upstream proxy limit.

Disclaimer

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.

License

MIT

Media gallery

1 / 3

Related apps

Details

Repository
ghcr.io/ezekielthemad/healthtrack:latest
Last Updated2026-10-05
First Seen2026-07-16

Runtime arguments

Web UI
http://[IP]:[PORT:3000]
Network
bridge
Shell
sh
Privileged
false

Template configuration

WebUI PortPorttcp

Host port for the HealthTrack web interface. If another container already uses 3000 (Mealie, Grafana, etc.), change this to a free port and update APP_URL to match — the container always listens on 3000 internally.

Target
3000
Default
3000
Value
3000
DataPathrw

All application state: SQLite database (healthtrack.db), uploaded lab PDFs (uploads/) and auto-generated secrets (keys/). Back up this folder to back up everything.

Target
/data
Default
/mnt/user/appdata/healthtrack
Value
/mnt/user/appdata/healthtrack
APP_URLVariable

The URL users browse to — required for login/auth callbacks to work. Use your server's address and the mapped port, e.g. http://YOUR_SERVER_IP:3000 (or your reverse-proxy HTTPS URL if you use one).

SIGNUPS_ENABLEDVariable

Registration policy. Leave EMPTY (recommended): the first account registers freely and becomes admin, then new accounts require a single-use invite link (Settings > Invites). Set to true to open registration to anyone, or false to close it completely.

ANTHROPIC_API_KEYVariable

Optional. Anthropic API key — enables AI features: health summaries, natural-language health queries, medication interaction checks and lab/vaccine PDF parsing. AI features are hidden when unset.

GOOGLE_CLIENT_IDVariable

Optional. Google OAuth client ID — enables the 'Sign in with Google' button. REQUIRES serving over HTTPS on a real hostname (reverse proxy or tunnel): Google rejects http:// and LAN-IP redirect URIs, so this will NOT work on a bare 192.168.x.x address. In the Google Cloud Console, add this authorized redirect URI: APP_URL/api/auth/callback/google (email/password login works without any of this).

GOOGLE_CLIENT_SECRETVariable

Optional. Google OAuth client secret (pairs with GOOGLE_CLIENT_ID).

OURA_CLIENT_IDVariable

Optional. Oura OAuth client ID — enables Oura Ring sync (sleep, heart rate, activity). In the Oura developer portal, set the redirect URI to: APP_URL/api/oura/callback

OURA_CLIENT_SECRETVariable

Optional. Oura OAuth client secret (pairs with OURA_CLIENT_ID).

OURA_SYNC_ENABLEDVariable

Advanced. Run Oura sync on HealthTrack startup and on its recurring interval. Set to false to disable native scheduled sync without disconnecting Oura.

Default
true
Value
true
OURA_SYNC_INTERVAL_HOURSVariable

Advanced. Hours between native Oura sync runs. Every run self-heals the latest seven calendar days.

Default
4
Value
4
AUTH_SECRETVariable

Advanced. Session signing secret. Leave empty — one is auto-generated into /data/keys on first start. Set only if you want to manage it yourself.

ENCRYPTION_KEYVariable

Advanced. Key used to encrypt stored OAuth tokens (Oura). Leave empty — one is auto-generated into /data/keys on first start. Set only if you want to manage it yourself.

PUIDVariable

User ID that owns files under /data (Unraid default: 99 / nobody).

Default
99
Value
99
PGIDVariable

Group ID that owns files under /data (Unraid default: 100 / users).

Default
100
Value
100
TZVariable

Valid IANA timezone for the container and Oura calendar-date sync windows, e.g. America/New_York.

Default
Etc/UTC
Value
Etc/UTC