Files
HealthLog/docs/self-hosting/getting-started.md
T
Marc-André BombeckandClaude Opus 4.8 65c7012590 docs: document the v1.11.0 features
Add WHOOP as a synced provider (per-user BYO-keys OAuth2 + HMAC-signed
webhooks), the longitudinal coach (narrative + per-metric trajectory),
and the read-only FHIR R4 REST API plus the scoped, time-limited
clinician share-link record across README, the self-hosting and ops
runbooks, and a new WHOOP integration page. Note the two optional
WHOOP instance env vars, that migrations 0110-0114 are additive, and
the two known limitations (dual-source standard-vital dedup deferred;
coach durable-memory deferred).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 04:25:22 +02:00

7.2 KiB

Getting started — self-host HealthLog

This walkthrough takes a fresh host from git clone to a running instance with a working first user. About fifteen minutes if Docker is already installed, longer if a TLS-fronted public hostname is in scope (see the reverse-proxy guide for that).

Prerequisites

  • Docker 24+ with Compose v2. The bundled docker-compose.yml pulls a multi-arch image from GitHub Container Registry (linux/amd64
    • linux/arm64), so a Raspberry Pi 5 or an x86 VPS both work unchanged.
  • 2 GB RAM, 10 GB free disk. The Postgres data volume grows with measurement history; the Apple Health import worker can briefly hold a 1.5 GB upload in /tmp while it parses.
  • Outbound HTTPS to ghcr.io (image pulls) plus whichever integration endpoints you plan to enable (Withings, OpenAI, etc.).
  • Optional but recommended: a reverse proxy that terminates TLS and forwards to http://localhost:3000 (Caddy, Traefik, Nginx Proxy Manager, Coolify, or bare Nginx — see reverse-proxy.md).

1. Clone the repository

git clone https://github.com/MBombeck/HealthLog.git
cd HealthLog
cp .env.example .env

.env.example is the canonical reference for every env var the app reads. Anything you do not uncomment falls back to a sane default or disables the optional subsystem.

2. Generate the three required secrets

HealthLog needs three secrets before it will boot. Generate them with openssl and append to .env:

echo "POSTGRES_PASSWORD=$(openssl rand -base64 24)" >> .env
echo "ENCRYPTION_KEY=$(openssl rand -hex 32)"       >> .env
echo "API_TOKEN_HMAC_KEY=$(openssl rand -hex 32)"   >> .env
Variable Purpose Rotation cost
POSTGRES_PASSWORD Bundled Postgres password — used by both the db service and DATABASE_URL Database restart + connection-string update
ENCRYPTION_KEY AES-256-GCM key for at-rest secrets (Withings tokens, AI provider keys, VAPID secrets) Coordinated; see docs/ops/encryption-key-rotation.md
API_TOKEN_HMAC_KEY HMAC-SHA256 key for hashing Bearer API tokens before storage Invalidates every issued hlk_* token

Then open .env and confirm DATABASE_URL includes the password you just generated. The default template (postgresql://healthlog:CHANGE-ME@db:5432/healthlog) references the literal string CHANGE-ME; replace it with the value you put in POSTGRES_PASSWORD, or rewrite the URL to substitute explicitly.

3. Bring the stack up

docker compose up -d

Compose starts two services:

  • db — PostgreSQL 16 with a named volume for persistence.
  • app — HealthLog itself, listening on localhost:3000. The image runs prisma migrate deploy on boot so the schema lands automatically; subsequent restarts no-op when the database is already current.

The first boot pulls the image (a few hundred megabytes) and runs migrations. Once docker compose logs -f app shows the Next.js banner, the instance is live.

4. Create the first user

Open http://localhost:3000 in a browser. The registration flow asks for a username, email, and password. The first user that registers is promoted to admin automatically. Every subsequent registration creates a regular user.

After the first user lands, you can:

  • Open /admin to review the worker status, audit log, and any optional channels you want to configure (Telegram bot, ntfy, Web Push VAPID keys, AI fallback key).
  • Open /measurements and add a weight or blood pressure to confirm the data path is wired end-to-end.

5. Point the instance at a public hostname

Local-only is fine for a single-user trial. For anything more — and to unlock OAuth callbacks, webhook deliveries, and Web Push — set the public URL pair before restarting:

NEXT_PUBLIC_APP_URL="https://your-instance.example.com"
APP_URL="https://your-instance.example.com"

Both vars must match. NEXT_PUBLIC_APP_URL lands in the client bundle at build time and drives every absolute URL the UI emits (OAuth redirects, share links, manifest icons). APP_URL is the server-side mirror used by background jobs that have no request context.

Restart the app container after editing:

docker compose up -d

The pre-built image reads both values at startup; no rebuild needed.

6. Pick a reverse-proxy

The bundled stack exposes plain HTTP on localhost:3000. Production deployments terminate TLS in front of the container. The docs/self-hosting/reverse-proxy.md guide carries copy-paste configurations for Caddy, Traefik, Nginx Proxy Manager, Coolify, and bare Nginx — including the X-Forwarded-For chain shape that the rate-limiter trusts and the TRUST_PROXY_HOPS env var that pins how many hops to read back.

Build-time feature toggles

A small number of toggles are NEXT_PUBLIC_* variables: the framework inlines them into the client bundle at build time, so the pre-built GHCR image cannot read them at runtime. Setting them in .env or the compose environment: block has no effect — they only take hold when you build your own image. They are intentionally absent from the compose runtime whitelist for that reason.

Variable Default Effect when set
NEXT_PUBLIC_DASHBOARD_SNAPSHOT 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. Build with NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false to fall back to the legacy four-cell path.

To flip one, rebuild the image with the variable exported:

NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false pnpm build
# or pass it as a Dockerfile build ARG / Docker build --build-arg

7. Where to go next

Next step File
TLS + reverse-proxy configuration docs/self-hosting/reverse-proxy.md
Web/worker process split for horizontal scale docs/self-hosting/scaling.md
Off-host encrypted backups to S3/R2/B2 docs/ops/backup-restore.md
Encryption-key rotation procedure docs/ops/encryption-key-rotation.md
Withings device sync docs/integrations/withings.md
WHOOP device sync (per-user BYO-keys) docs/integrations/whoop.md
Apple Health export.zip import docs/integrations/apple-health.md
AI provider setup (OpenAI / Anthropic / local / ChatGPT OAuth) docs/integrations/ai-providers.md

Troubleshooting

  • app container exits with a Prisma migration error. The Postgres healthcheck races the migration on slow disks. Run docker compose restart app once the database has stabilised — migrations are idempotent.
  • First-user registration shows "registration disabled". The bundled image ships with registration enabled; the toggle lives in app_settings.singleton.registrationEnabled. The admin panel can flip it back if a previous owner closed it.
  • Service unreachable on localhost:3000. Check docker compose ps — the app healthcheck polls /api/version every 30 seconds and will mark the container unhealthy if Postgres is still starting. Tail docker compose logs db to confirm Postgres came up cleanly.
  • OAuth callbacks loop back to localhost. Confirm APP_URL and NEXT_PUBLIC_APP_URL both point at the public hostname, not at localhost, and restart app after the change.