diff --git a/README.md b/README.md index bd0c053a6..da2570519 100644 --- a/README.md +++ b/README.md @@ -42,18 +42,36 @@ Try the [live demo](https://demo.healthlog.dev) to see a working install — sig The full feature tour, integration guides, and API reference live at [docs.healthlog.dev](https://docs.healthlog.dev). -## Quick start +## Quick start (Docker Compose) ```bash git clone https://github.com/MBombeck/HealthLog.git && cd HealthLog cp .env.example .env + +# Generate the three secrets (each a 64-char hex string = 32 bytes): 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 + +# LAN / plain-HTTP host (NAS, homelab, Tailscale) — required, or login fails: +echo "SESSION_COOKIE_SECURE=false" >> .env + docker compose up -d ``` -Open **http://localhost:3000** — the first registered user becomes admin. The compose file pulls a pre-built multi-arch image (`amd64` + `arm64`) from [GHCR](https://github.com/MBombeck/HealthLog/pkgs/container/healthlog); no build step required. Behind a reverse proxy, set `NEXT_PUBLIC_APP_URL` and `APP_URL` first — see the [self-hosting guide](https://docs.healthlog.dev/self-hosting/). +Open **http://localhost:3000** (or `http://:3000`) — the first registered user becomes admin. The compose file pulls a pre-built multi-arch image (`amd64` + `arm64`) from [GHCR](https://github.com/MBombeck/HealthLog/pkgs/container/healthlog); no build step required. + +Verify the running version: + +```bash +curl -s http://localhost:3000/api/version # returns version + buildSha + builtAt +``` + +`SESSION_COOKIE_SECURE=false` is the one setting plain-HTTP self-hosts must set: without it the session cookie carries the `Secure` flag, the browser drops it over HTTP, and login silently fails. Leave it unset (or `true`) only when a TLS reverse proxy serves HTTPS — and then set `NEXT_PUBLIC_APP_URL` and `APP_URL` to the public URL first (see the [self-hosting guide](https://docs.healthlog.dev/self-hosting/)). + +Persistent state lives in exactly one named volume (`pgdata`, the Postgres data dir); the app container is stateless, so backing up the database backs up everything. + +NAS and homelab users have ready-made paths: [Unraid Community Applications](docs/self-hosting/unraid.md) and [Portainer app templates](docs/self-hosting/portainer/README.md). ## Self-hosting diff --git a/docker-compose.yml b/docker-compose.yml index 8a22bb5b8..f28293eee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,22 @@ +# HealthLog — two-container stack (app + Postgres 16). +# +# First run (clean host): +# cp .env.example .env +# 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 +# docker compose up -d +# # then open http://:3000 — the first registered user becomes admin. +# +# LAN / plain-HTTP self-hosts (NAS, homelab, Tailscale): set +# SESSION_COOKIE_SECURE=false +# in .env, otherwise the session cookie carries the `Secure` flag over HTTP +# and login silently fails. This is the single most common self-host snag. +# Leave it unset (or true) only when a TLS reverse proxy serves HTTPS. +# +# Persistent state lives in exactly one named volume (`pgdata`, Postgres' +# data dir). The app container is stateless — GeoLite2 MMDBs are baked into +# the image — so a backup of the database is a backup of everything. services: # Single-container default (HEALTHLOG_PROCESS_TYPE=all). Existing # self-host deployments stay working with `docker compose up`. @@ -39,18 +58,27 @@ services: # a free connection before erroring. DATABASE_URL: "postgresql://healthlog:${POSTGRES_PASSWORD}@db:5432/healthlog?schema=public&connection_limit=${DB_CONNECTION_LIMIT:-20}&pool_timeout=${DB_POOL_TIMEOUT:-20}" NODE_ENV: production + # Encryption at rest (AES-256-GCM). Generate each secret with + # `openssl rand -hex 32` (a 64-char hex string = 32 bytes). ENCRYPTION_KEY + # is the single-key path and is all most self-hosts need. The map form + # (ENCRYPTION_KEYS={"v1":"","v2":""} + ENCRYPTION_ACTIVE_KEY_ID=v2) + # is for key rotation — see docs/ops/encryption-key-rotation.md. Keep these + # values stable and backed up: lose them and the encrypted columns are + # unrecoverable. ENCRYPTION_KEY: "${ENCRYPTION_KEY}" ENCRYPTION_KEYS: "${ENCRYPTION_KEYS:-}" ENCRYPTION_ACTIVE_KEY_ID: "${ENCRYPTION_ACTIVE_KEY_ID:-}" API_TOKEN_HMAC_KEY: "${API_TOKEN_HMAC_KEY}" NEXT_PUBLIC_APP_URL: "${NEXT_PUBLIC_APP_URL:-http://localhost:3000}" APP_URL: "${APP_URL:-http://localhost:3000}" - # v1.5.1 session-cookie Secure-flag opt-out. Forwarded explicitly so - # an operator setting SESSION_COOKIE_SECURE=false in .env actually - # reaches the Node process (the compose `environment:` block is a - # whitelist — vars not listed here don't propagate from .env into - # the container, even though docker-compose reads .env for - # `${VAR}` substitution at compose-up time). + # Session-cookie Secure-flag opt-out. SET THIS TO false FOR LAN / plain-HTTP + # self-hosts (http://:3000 with no TLS reverse proxy) — otherwise the + # session cookie carries the `Secure` flag, the browser drops it over HTTP, + # and login silently fails. Unset → the flag tracks NODE_ENV=production + # (i.e. on); set to true to force it. Forwarded explicitly so an operator + # value in .env reaches the Node process (the `environment:` block is a + # whitelist — vars not listed here don't propagate even though + # docker-compose reads .env for `${VAR}` substitution at compose-up time). SESSION_COOKIE_SECURE: "${SESSION_COOKIE_SECURE:-}" # v1.18.11 (W3) — client-IP resolution + geo lookup. Behind Cloudflare # the real visitor IP is in `cf-connecting-ip`; set diff --git a/docs/self-hosting/portainer/README.md b/docs/self-hosting/portainer/README.md new file mode 100644 index 000000000..b9da05db8 --- /dev/null +++ b/docs/self-hosting/portainer/README.md @@ -0,0 +1,49 @@ +# HealthLog on Portainer + +HealthLog ships a Portainer **app template** that deploys the app and its +PostgreSQL 16 database as one stack from the project `docker-compose.yml`. + +## Add the template + +1. In Portainer, go to **Settings → App Templates**. +2. Set the URL to the raw template file: + + ``` + https://raw.githubusercontent.com/MBombeck/HealthLog/main/docs/self-hosting/portainer/templates.json + ``` + +3. Save. **HealthLog** now appears under **App Templates**. + +## Deploy + +1. Open **App Templates → HealthLog**. +2. Fill in the variables. Generate each secret with `openssl rand -hex 32` (a + 64-character hex string): + - `POSTGRES_PASSWORD` + - `ENCRYPTION_KEY` — back this up; losing it makes encrypted data + unrecoverable + - `API_TOKEN_HMAC_KEY` + - `SESSION_COOKIE_SECURE` — **set to `false` for plain-HTTP / LAN access**, or + login silently fails. Set to `true` only behind an HTTPS reverse proxy. + - `NEXT_PUBLIC_APP_URL` and `APP_URL` — how you reach the app, e.g. + `http://nas:3000` or `https://healthlog.example.com`. +3. **Deploy the stack**. The app waits for PostgreSQL, runs database migrations + on first boot, then starts serving. +4. Open the app URL. The first registered user becomes the admin. + +Verify the running version: + +```bash +curl -s http://:3000/api/version +``` + +## Notes + +- Persistent state lives in one named volume (`pgdata`, the Postgres data dir); + the app container is stateless. Back up the database to back up everything. +- Pin a release tag (`HEALTHLOG_IMAGE_REF=:vX.Y.Z`) for deliberate upgrades, or + track `:latest`. Skim the + [CHANGELOG](https://github.com/MBombeck/HealthLog/blob/main/CHANGELOG.md) and + back up before upgrading. Migrations are forward-only. +- For passive discovery you can also submit this template to the community + aggregator at https://github.com/Lissy93/portainer-templates. diff --git a/docs/self-hosting/portainer/templates.json b/docs/self-hosting/portainer/templates.json new file mode 100644 index 000000000..985e01c44 --- /dev/null +++ b/docs/self-hosting/portainer/templates.json @@ -0,0 +1,54 @@ +{ + "version": "3", + "templates": [ + { + "type": 3, + "title": "HealthLog", + "name": "healthlog", + "description": "Self-hosted personal health tracker (PWA + native iOS client): weight, blood pressure, pulse, blood glucose, body composition, sleep, mood, menstrual cycle, and medications on one encrypted timeline. AES-256-GCM at rest, no vendor cloud, no telemetry. Ships app + PostgreSQL 16 as one stack.", + "note": "Generate each secret with: openssl rand -hex 32 (a 64-char hex string). For plain-HTTP / LAN access set SESSION_COOKIE_SECURE to false, or login fails. Set NEXT_PUBLIC_APP_URL and APP_URL to how you reach the app. Full guide: https://github.com/MBombeck/HealthLog/blob/main/docs/self-hosting/getting-started.md", + "categories": ["Tools", "Other"], + "platform": "linux", + "logo": "https://raw.githubusercontent.com/MBombeck/HealthLog/main/public/logo-512.png", + "repository": { + "url": "https://github.com/MBombeck/HealthLog", + "stackfile": "docker-compose.yml" + }, + "env": [ + { + "name": "POSTGRES_PASSWORD", + "label": "PostgreSQL password", + "description": "Database password. Generate with: openssl rand -hex 32" + }, + { + "name": "ENCRYPTION_KEY", + "label": "Encryption key (data at rest)", + "description": "AES-256-GCM key. Generate with: openssl rand -hex 32. Back this up — losing it makes encrypted data unrecoverable." + }, + { + "name": "API_TOKEN_HMAC_KEY", + "label": "API token HMAC key", + "description": "Generate with: openssl rand -hex 32" + }, + { + "name": "SESSION_COOKIE_SECURE", + "label": "Session cookie Secure flag", + "description": "Set to false for plain-HTTP / LAN access, or login fails. Set to true only behind an HTTPS reverse proxy.", + "default": "false" + }, + { + "name": "NEXT_PUBLIC_APP_URL", + "label": "Public app URL", + "description": "How users reach HealthLog, e.g. http://nas:3000 or https://healthlog.example.com", + "default": "http://localhost:3000" + }, + { + "name": "APP_URL", + "label": "Internal app URL", + "description": "Normally identical to the public app URL.", + "default": "http://localhost:3000" + } + ] + } + ] +} diff --git a/docs/self-hosting/unraid.md b/docs/self-hosting/unraid.md new file mode 100644 index 000000000..062562321 --- /dev/null +++ b/docs/self-hosting/unraid.md @@ -0,0 +1,137 @@ +# HealthLog on Unraid + +HealthLog runs on Unraid in two ways. The **Compose Manager** path is the clean, +self-contained install (app + database as one unit) and is recommended. The +**Community Applications (CA)** template gives you the familiar one-app-at-a-time +Unraid UI, but because CA templates describe a single container you install +PostgreSQL separately and wire the two together. + +HealthLog stores all persistent state in PostgreSQL — the app container itself is +stateless (the offline geo-IP databases are baked into the image). A backup of +the database is a backup of everything. + +## What you need + +- Unraid 6.12 or newer. +- The image: `ghcr.io/mbombeck/healthlog:latest` (multi-arch, amd64 + arm64). +- Three secrets, each generated with `openssl rand -hex 32` (a 64-character hex + string). On Unraid open the terminal (the `>_` icon, top-right) and run the + command three times: + - `POSTGRES_PASSWORD` — the database password + - `ENCRYPTION_KEY` — AES-256-GCM key for data at rest + - `API_TOKEN_HMAC_KEY` — API token hashing key + +Keep `ENCRYPTION_KEY` safe and backed up. If you lose it, the encrypted columns +become unrecoverable. + +--- + +## Recommended: Compose Manager + +This installs the app and its database together, exactly as the project's +`docker-compose.yml` describes. + +1. Install **Compose Manager** from Community Applications (search "Compose + Manager", by dcflachs). +2. **Settings → Docker → Compose Manager → Add New Stack**, name it `healthlog`. +3. Paste the project + [`docker-compose.yml`](https://github.com/MBombeck/HealthLog/blob/main/docker-compose.yml) + into the stack's compose file. +4. Edit the stack's `.env` (the "Edit Stack → env" field) and set: + + ```env + POSTGRES_PASSWORD= + ENCRYPTION_KEY= + API_TOKEN_HMAC_KEY= + SESSION_COOKIE_SECURE=false + NEXT_PUBLIC_APP_URL=http://tower:3000 + APP_URL=http://tower:3000 + ``` + + Replace `tower` with your server's hostname or IP. `SESSION_COOKIE_SECURE=false` + is required for plain-HTTP LAN access — see the note below. + +5. **Compose Up**. The app waits for PostgreSQL, runs database migrations on + first boot, then starts serving. +6. Open `http://tower:3000`. The first account you register becomes the admin. + +Verify the running version: + +```bash +curl -s http://tower:3000/api/version +``` + +--- + +## Alternative: Community Applications template + +Use this if you prefer the per-app Unraid UI. You install PostgreSQL yourself, +then add HealthLog pointed at it. + +### Step 1 — install PostgreSQL 16 + +1. In **Apps** (Community Applications), search for a **PostgreSQL** container + (for example jj9987's `postgresql`). Install version 16. +2. Set: + - `POSTGRES_USER` = `healthlog` + - `POSTGRES_PASSWORD` = your generated password + - `POSTGRES_DB` = `healthlog` +3. Map the Postgres data path to a share on the array (e.g. + `/mnt/user/appdata/healthlog-db`) so the database survives container updates. +4. Note the container's IP (or container name if both run on the same custom + Docker network). + +### Step 2 — add the HealthLog template + +The template lives at +[`docs/self-hosting/unraid/healthlog.xml`](unraid/healthlog.xml). Add it via +**Docker → Add Container → Template** and paste the raw URL, or copy it into +`/boot/config/plugins/dockerMan/templates-user/` and pick it from the template +dropdown. + +Fill in the fields: + +| Field | Value | +| ----------------------- | -------------------------------------------------------------------------------- | +| WebUI Port | `3000` | +| `DATABASE_URL` | `postgresql://healthlog:@:5432/healthlog?schema=public` | +| `ENCRYPTION_KEY` | your generated key | +| `API_TOKEN_HMAC_KEY` | your generated key | +| `SESSION_COOKIE_SECURE` | `false` for plain-HTTP LAN access | +| `NEXT_PUBLIC_APP_URL` | `http://tower:3000` (or your domain) | +| `APP_URL` | same as `NEXT_PUBLIC_APP_URL` | + +Replace `` and `` with the values from Step 1. Apply, +then open `http://tower:3000` — the first registered user becomes the admin. + +The HealthLog container has no persistent volume of its own; all state is in the +PostgreSQL container you installed in Step 1. Back that one up. + +--- + +## The one setting that trips people up + +`SESSION_COOKIE_SECURE=false` is required for plain-HTTP LAN installs +(`http://tower:3000`). Left at its default, the session cookie carries the +`Secure` flag, the browser refuses to send it over HTTP, and login appears to +"do nothing" — the page reloads to the login screen with no error. + +Set `SESSION_COOKIE_SECURE=true` (or leave it unset) only when a TLS reverse +proxy serves HealthLog over HTTPS. In that case also point `NEXT_PUBLIC_APP_URL` +and `APP_URL` at the public `https://` URL. + +## Updates + +Pin a release tag (`ghcr.io/mbombeck/healthlog:vX.Y.Z`) and bump deliberately, +or track `:latest` and let Unraid's update check re-pull. Skim the +[CHANGELOG](https://github.com/MBombeck/HealthLog/blob/main/CHANGELOG.md) before +upgrading, and back up the database first. Migrations are forward-only and run +automatically on container start. + +## Health check + +Both `/api/health` and `/api/version` respond without authentication. +`/api/health` returns `200` when the database and background worker are up +(`503` otherwise) and is the container health probe baked into the image. +`/api/version` returns the version, build SHA, and build timestamp for deploy +verification. diff --git a/docs/self-hosting/unraid/healthlog.xml b/docs/self-hosting/unraid/healthlog.xml new file mode 100644 index 000000000..3ad71c0f5 --- /dev/null +++ b/docs/self-hosting/unraid/healthlog.xml @@ -0,0 +1,64 @@ + + + + HealthLog + ghcr.io/mbombeck/healthlog:latest + https://github.com/MBombeck/HealthLog/pkgs/container/healthlog + bridge + + sh + false + https://github.com/MBombeck/HealthLog/issues + https://github.com/MBombeck/HealthLog + + Self-hosted personal health tracker (PWA + native iOS client): weight, blood + pressure, pulse, blood glucose, body composition, sleep, mood, menstrual + cycle, and medications on one encrypted timeline. AES-256-GCM at rest, no + vendor cloud, no telemetry. + + REQUIRES a separate PostgreSQL 16 container — install one first (CA has + community PostgreSQL templates), create a database and user, then set + DATABASE_URL below. Step-by-step guide: + https://github.com/MBombeck/HealthLog/blob/main/docs/self-hosting/unraid.md + + Generate each required secret with: openssl rand -hex 32 + + LAN / plain-HTTP installs MUST keep SESSION_COOKIE_SECURE set to false, + otherwise the session cookie carries the Secure flag, the browser drops it + over HTTP, and login silently fails. + + Tools: Productivity: Other: + http://[IP]:[PORT:3000]/ + https://raw.githubusercontent.com/MBombeck/HealthLog/main/public/logo-512.png + --security-opt no-new-privileges:true + + + + 3000 + + + + + + + + false + + + + + + + + +