mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
Cover the full channel set self-hosters can run without an Apple account: Web Push, Telegram, ntfy, the generic webhook, and email over SMTP, plus the dispatcher cascade and graceful degradation when APNs is absent (ntfy max priority, Web Push high urgency, webhook urgency flag). Document the PWA path as the recommended default: install to the Home Screen, enable Web Push, and get reminders, the taken-clears-the-reminder behaviour, and the outstanding-dose app badge. Note the inherent limit that the lock-screen Live Activity needs a native build. Explain why native iOS push binds to the publisher's Apple team and walk through running your own signed build under your own bundle ID, team, and .p8 key, with the APNS_* env vars including APNS_CRITICAL_ENTITLEMENT.
226 lines
9.7 KiB
Markdown
226 lines
9.7 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```bash
|
|
echo "POSTGRES_PASSWORD=$(openssl rand -hex 32)" >> .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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```env
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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` |
|
|
| Push notifications — Web Push, Telegram, ntfy, webhook, email, APNs | `docs/self-hosting/notifications.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` |
|
|
| Google Health sync — Fitbit & Pixel (per-user BYO-keys) | `docs/integrations/google-health.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.
|
|
- **An env var in `.env` seems ignored / a feature stays "off".** The
|
|
compose `environment:` block is a **whitelist** — a variable set in
|
|
`.env` only reaches the container if `docker-compose.yml` lists it
|
|
under `environment:`. Vars not listed never propagate, even with
|
|
`${VAR}` substitution at compose-up. If a setting looks configured but
|
|
is silently dead, check the var is in that block (add it if you run a
|
|
custom compose); the supported alternative for most secrets (VAPID,
|
|
GitHub token, GlitchTip DSN) is the **admin panel**, which stores them
|
|
in the database and sidesteps the whitelist entirely.
|
|
- **A new image doesn't pick up / `:latest` looks stale.** `docker-compose.yml`
|
|
sets `pull_policy: always`, which is load-bearing: without it Docker
|
|
re-uses the cached `:latest` digest and silently skips the registry
|
|
round-trip on `compose up`. If you pin a custom compose, keep
|
|
`pull_policy: always` on the `app` service, and verify the running
|
|
build with `GET /api/version` (it returns `version` + `buildSha`).
|
|
|
|
## Backup and restore
|
|
|
|
Your database is the only stateful piece — back it up, and back up the
|
|
key that decrypts it.
|
|
|
|
**Local snapshot (the common single-box case).** Dump the `db` service
|
|
with `pg_dump`:
|
|
|
|
```bash
|
|
# The bundled db service uses user `healthlog` and database `healthlog`.
|
|
docker compose exec -T db pg_dump -U healthlog healthlog \
|
|
| gzip > healthlog-$(date +%F).sql.gz
|
|
```
|
|
|
|
Restore into a fresh database the same way:
|
|
|
|
```bash
|
|
gunzip -c healthlog-2026-01-01.sql.gz \
|
|
| docker compose exec -T db psql -U healthlog healthlog
|
|
```
|
|
|
|
> **Back up your `ENCRYPTION_KEYS` / `ENCRYPTION_KEY` alongside the dump.**
|
|
> HealthLog encrypts sensitive columns at rest (AES-256-GCM). A database
|
|
> dump **without the encryption key is unrecoverable** — the encrypted
|
|
> data cannot be read back. Store the key (and any retired keys still in
|
|
> the rotation map) somewhere separate from the dump but equally durable.
|
|
> Losing the key means losing the encrypted data, full stop.
|
|
|
|
For off-host encrypted backups to S3/R2/B2 see
|
|
[`docs/ops/backup-restore.md`](../ops/backup-restore.md); for rotating the
|
|
encryption key safely (keep the old key until zero rows remain on it) see
|
|
[`docs/ops/encryption-key-rotation.md`](../ops/encryption-key-rotation.md).
|