mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
One-shot prettier pass so the new format:check gate starts from a clean baseline. Formatting only — no behaviour change. Generator-owned and vendored artifacts stay untouched via .prettierignore.
226 lines
11 KiB
Markdown
226 lines
11 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).
|