Three hotfix-ready findings from the v1.4.34.3 mobile-deep audit
plus the first two docs A8 audit identified as missing.
Mobile-deep:
- `viewport.themeColor` was pinned to `#282a36` (dark). The
Android URL bar + iOS PWA status bar stayed dark even when the
user was on the light theme. Replaced with the dual-media-query
form: `#ffffff` under `(prefers-color-scheme: light)`,
`#282a36` under `(prefers-color-scheme: dark)`. Browser chrome
now matches the active palette.
- Coach FAB was pinned at `bottom-20` (5 rem). On notched iPhones
the home-indicator safe area is ~34 px so the FAB collided with
the bottom-nav. Moved to
`bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)]` so it
floats above the inset-aware nav.
- Sonner toaster shipped `theme="dark"` hardcoded. On a light-mode
page the toast contrasted badly against the surface. Switched to
`theme="system"` so Sonner reads the resolved theme from the
root element (set pre-paint by the inline theme script in
layout.tsx) instead of pinning dark.
Self-hosting docs:
- `docs/self-hosting/getting-started.md` — step-by-step from
`git clone` to first measurement, covering the 3-secret env
setup (post-SESSION_SECRET drop), `docker compose up -d`, the
first-user-becomes-admin contract, and the next-step pointers.
- `docs/self-hosting/reverse-proxy.md` — Caddy, Traefik, Nginx
Proxy Manager, Coolify, and bare-Nginx config examples. Closes
the README link at line 107 that previously punted to the docs
site.
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.ymlpulls 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
/tmpwhile 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 — seereverse-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 onlocalhost:3000. The image runsprisma migrate deployon 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
/adminto 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
/measurementsand 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
appcontainer exits with a Prisma migration error. The Postgres healthcheck races the migration on slow disks. Rundocker compose restart apponce 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. Checkdocker compose ps— theapphealthcheck polls/api/versionevery 30 seconds and will mark the container unhealthy if Postgres is still starting. Taildocker compose logs dbto confirm Postgres came up cleanly. - OAuth callbacks loop back to localhost. Confirm
APP_URLandNEXT_PUBLIC_APP_URLboth point at the public hostname, not atlocalhost, and restartappafter the change.