Files
HealthLog/docs/self-hosting/getting-started.md
T
Marc-André Bombeck f5962497d6 v1.4.34.4 — audit-driven hotfix bundle (security, UX, code-quality, docs)
Consolidated hotfix landing the findings from a six-axis audit
covering mobile security, UX + accessibility, code quality + docs,
README + discoverability, web performance, and mobile-deep responsive
behaviour. None of the changes break the public API or the existing
data shape; every fix is additive and SAFE under the v1.4.34.x web
freeze. Full audit reports live in `.planning/round-audit-*.md`.

Security
  - Password rotation now wipes API tokens + refresh tokens in the
    same `$transaction` as session revocation. A stolen iOS Bearer no
    longer survives a user-initiated password change or an admin reset.
  - `/.well-known/` proxy bypass tightened to an explicit-allowlist
    `Set`. Stale `/api/auth/codex/callback` PUBLIC_PATHS entry dropped.
  - HSTS gains `preload`. Withings host removed from the global CSP
    and gated to the integration's own routes.
  - `getClientIp` warns once per process on `TRUST_PROXY_HOPS`/XFF
    mismatch + exposes a tagged helper for tighter universal buckets.
  - Service worker rejects off-origin notification-click URLs. Offline
    fallback body switched to a locale-neutral icon. Manifest gains
    `scope`, `id`, `display_override`.

UX + accessibility
  - i18n leaks closed on Dialog/Sheet close-X, ChartSkeleton's loading
    announcement, and the notifications-section breadcrumb.
  - Focus rings on bottom-nav, the More overflow button, and the
    mobile top-bar user menu.
  - Tap-target floor: Input / NativeSelect / Select / DateTimeInput
    switch to `h-11 sm:h-10`.
  - Admin chip-strip auto-scroll-into-view (matches v1.4.33 settings).
  - List-page subtitles visible on mobile.
  - Insights empty-state CTA vocabulary normalised so the link opens
    the right measurement form (was silently failing for
    BODY_TEMPERATURE / BLOOD_GLUCOSE / PULSE).

Mobile-deep
  - `viewport.themeColor` reacts to light/dark mode.
  - TopBar + Coach FAB reserve safe-area-inset-top / -bottom for
    notched iPhones.
  - Sonner toaster switches to `theme="system"`.

Code quality
  - `SESSION_SECRET` dropped — the env var was documented as required
    but never read by code. Three secrets, not four. Self-hosters
    upgrading can delete the existing line.
  - `toJson<T>()` helper consolidates seven Prisma JSON-cast sites.
  - `db-compat.ts` gains a header docstring naming it the
    schema-bootstrap path.
  - Passkey response types adopted from `@simplewebauthn/server`;
    `any` + eslint-disable pragmas drop out.
  - Zero-TODO CI gate added at `.github/workflows/no-todo-markers.yml`.

Documentation + discoverability
  - README rework: How-it-compares matrix, Apple Health + AI Coach
    promoted to standalone Key-Features bullets, status block, latest
    release + GHCR badges, OpenAPI pointer, three-secret Quick Start.
  - openGraph + twitter cards now carry `images` and a
    `summary_large_image` Twitter card.
  - GitHub repo metadata: description tightened from 290 to 156
    chars + keyword-front, `homepageUrl` flipped from a personal
    tenant to the demo site, six generic / PII-adjacent topics
    swapped for six high-intent ones.
  - Five new user-facing docs: `docs/self-hosting/getting-started.md`,
    `docs/self-hosting/reverse-proxy.md`,
    `docs/integrations/apple-health.md`,
    `docs/integrations/withings.md`,
    `docs/integrations/ai-providers.md`.
  - `docs/README.md` clarifies the docs tree as internal playbooks
    vs the user-facing `docs.healthlog.dev` site.
2026-05-17 00:39:58 +02:00

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

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