diff --git a/README.md b/README.md
index 299fa3065..214f00045 100644
--- a/README.md
+++ b/README.md
@@ -89,7 +89,7 @@ curl -s http://localhost:3000/api/version # returns version + buildSha + built
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).
+NAS and homelab users have ready-made paths: [Unraid Community Applications](docs/self-hosting/unraid.md), [Portainer app templates](docs/self-hosting/portainer/README.md) and [TrueNAS](docs/self-hosting/truenas.md) (the Community train app is pending catalog review).
## Self-hosting
diff --git a/docs/ops/deploy.md b/docs/ops/deploy.md
index 14a790232..046be2905 100644
--- a/docs/ops/deploy.md
+++ b/docs/ops/deploy.md
@@ -199,11 +199,26 @@ dev --create-only --name vX_Y_Z_rollback_
`), edit the SQL,
## Catalog templates
-A new required environment variable, or a change to the compose shape, needs a
-matching pull request to the TrueNAS apps catalog in the same release cycle
-(and an update to `docs/self-hosting/unraid/healthlog.xml` and
-`docs/self-hosting/portainer/templates.json`). See
-`docs/self-hosting/truenas.md` for what the catalog template carries.
+The TrueNAS catalog bot follows new image tags on its own, so a routine
+release needs nothing there. These changes do need a matching pull request to
+`truenas/apps` (`ix-dev/community/healthlog/`) in the same release cycle, and
+an update to `docs/self-hosting/unraid/healthlog.xml` and
+`docs/self-hosting/portainer/templates.json`:
+
+- a new required environment variable, or a new format or minimum for an
+ existing one (the form validates the secrets);
+- a changed listening port, health check path or health semantics, or start
+ time beyond the template's five-minute start period;
+- a required PostgreSQL major version, or an extension the stock `postgres`
+ image does not ship;
+- any other change to the compose shape: a new container, a volume the app
+ must write to, a different user.
+
+The catalog app sets `HEALTHLOG_PLATFORM=truenas`, `PORT`, `HOSTNAME`,
+`DATABASE_URL`, `ENCRYPTION_KEY`, `API_TOKEN_HMAC_KEY`, `APP_URL`,
+`NEXT_PUBLIC_APP_URL` and `SESSION_COOKIE_SECURE`; everything else reaches it
+through the form's additional environment variables. See
+`docs/self-hosting/truenas.md` for the operator side.
## Verify the served version
diff --git a/docs/self-hosting/getting-started.md b/docs/self-hosting/getting-started.md
index 0541c2333..071f83a1a 100644
--- a/docs/self-hosting/getting-started.md
+++ b/docs/self-hosting/getting-started.md
@@ -274,13 +274,13 @@ Encryption: the step shows the key id and a fingerprint (the first 12 hex
characters of SHA-256 over the key) and can check a pasted copy without
storing it. Admins see a dashboard reminder until the active key is confirmed.
-| Platform | Where the key is set |
-| --------- | --------------------------------------------------------------- |
-| Compose | `.env` next to `docker-compose.yml` |
-| TrueNAS | Apps, HealthLog, Edit, Environment |
-| Coolify | the application's Environment Variables |
-| Unraid | the container template (Docker tab, Edit) or the Compose `.env` |
-| Portainer | the stack's Environment variables |
+| Platform | Where the key is set |
+| --------- | -------------------------------------------------------------------------------------------------- |
+| Compose | `.env` next to `docker-compose.yml` |
+| TrueNAS | Apps, HealthLog, Edit: the Encryption Key field (`ENCRYPTION_KEY` in a YAML install's environment) |
+| Coolify | the application's Environment Variables |
+| Unraid | the container template (Docker tab, Edit) or the Compose `.env` |
+| Portainer | the stack's Environment variables |
Set `HEALTHLOG_PLATFORM` (`compose`, `truenas`, `coolify`, `unraid`,
`portainer`) to open the step on your platform's instructions.
diff --git a/docs/self-hosting/truenas.md b/docs/self-hosting/truenas.md
index 3f7f6abcb..460867eb4 100644
--- a/docs/self-hosting/truenas.md
+++ b/docs/self-hosting/truenas.md
@@ -1,82 +1,152 @@
-# HealthLog on TrueNAS SCALE
+# HealthLog on TrueNAS
-HealthLog runs on TrueNAS SCALE as a Docker app: one `app` container from
-`ghcr.io/mbombeck/healthlog` and one PostgreSQL 16 database. The app container
-is stateless; everything you care about lives in the database dataset and in
-one value you must keep yourself: the encryption key.
+HealthLog runs on TrueNAS Community Edition (formerly SCALE) as a Docker app:
+one container from `ghcr.io/mbombeck/healthlog` and one PostgreSQL database.
+The app container is stateless. Everything you care about lives in the
+database dataset and in one value you must keep yourself: the encryption key.
-## What the app needs
+> **Status: pending catalog review.** A HealthLog app for the TrueNAS
+> Community train has been prepared but is not listed yet. Until it is, use
+> [Install via YAML](#before-the-listing-install-via-yaml) at the end of this
+> page. This note goes away once the app appears under Apps, Discover Apps.
-| Setting | Value |
-| ------------ | ---------------------------------------------------------------------------------------- |
-| Image | `ghcr.io/mbombeck/healthlog`, tag `X.Y.Z` (amd64 and arm64) |
-| Port | `3000` (HTTP) |
-| User | runs as uid `1001`; no extra capabilities, `no-new-privileges` is fine |
-| Health check | `GET /api/health`, `200` when ready |
-| Start period | at least 60 s, with more retries than the image's 5 × 30 s: migrations run at every boot |
-| Database | PostgreSQL 16 (17 and later are not a drop-in), no extensions |
-| Volumes | none for the app; the database gets its own dataset |
+## Install from the Community train
-## Environment
+1. Generate two secrets. Open the TrueNAS shell (System, Shell) and run the
+ command twice, once for each field:
-| Variable | Required | Notes |
-| ----------------------- | -------- | ------------------------------------------------------------------------------------------- |
-| `DATABASE_URL` | yes | `postgresql://:@:5432/` |
-| `ENCRYPTION_KEY` | yes | 64 hex characters: `openssl rand -hex 32`. Keep a copy outside the NAS (see below). |
-| `API_TOKEN_HMAC_KEY` | yes | 64 hex characters, generated the same way. Changing it signs everyone out. |
-| `NEXT_PUBLIC_APP_URL` | yes | the address people open, for example `http://truenas.local:30180` |
-| `APP_URL` | yes | the same address |
-| `SESSION_COOKIE_SECURE` | no | `false` for a LAN install served over plain `http://`; leave unset behind an HTTPS proxy |
-| `HEALTHLOG_PLATFORM` | no | `truenas`: the admin's key backup step opens on the TrueNAS instructions |
-| `GEOLITE2_DIR` | no | a mounted directory of GeoLite2 `.mmdb` files, so sign-in locations are resolved on the NAS |
+ ```bash
+ openssl rand -hex 32
+ ```
-`HEALTHLOG_PROCESS_TYPE` stays unset (`all`): one container serves the web app
-and runs the background jobs.
+ Each run prints 64 hex characters. Put both in your password manager
+ before you continue; see [Back up your encryption key](#back-up-your-encryption-key).
+
+2. Open Apps, Discover Apps, search for **HealthLog** and choose Install.
+3. Fill in the form:
+
+ | Field | What to enter |
+ | --------------------- | -------------------------------------------------------------------------------------------------------------- |
+ | Timezone | your timezone |
+ | Database Password | any strong password; only the two containers use it |
+ | Encryption Key | the first `openssl rand -hex 32` output |
+ | Token Signing Key | the second output. Changing it later signs everyone out |
+ | App URL | the address you open HealthLog at, with scheme and port, for example `http://192.168.1.10:30536` |
+ | Secure Cookies | leave off when you open HealthLog over `http://` on the LAN; turn on behind an HTTPS reverse proxy (see below) |
+ | WebUI Port | `30536` unless that port is taken |
+ | Postgres Data Storage | the default ixVolume, or a host path on a dataset you snapshot |
+ | Resources | keep memory at 1536 MB or more; the default of 4096 MB is fine |
+
+4. Install. The first start applies the database migrations before the web
+ server listens, so give it a minute. The app shows as running once
+ `/api/health` answers.
+5. Open the App URL. The first account you register becomes the admin.
+
+The App URL has to match the address in the browser: passkeys, sign-in
+redirects and the links in notifications are built from it. If you later put
+HealthLog behind a reverse proxy with its own domain, change App URL to that
+`https://` address.
+
+Settings the form does not show go under Additional Environment Variables, for
+example `UPDATE_CHECK_DISABLED=1`. The full list is in
+[environment.md](environment.md). To use your own GeoLite2 databases, add the
+directory under Additional Storage and point `GEOLITE2_DIR` at its mount path
+([geolite2.md](geolite2.md)).
## Plain HTTP on the LAN
-Most NAS installs are reached over `http://`. Set `SESSION_COOKIE_SECURE=false`
-for that. If it is left at its default, the browser drops the sign-in cookie
-and the login page says so before you type a password: "This server only sets
-its sign-in cookie over https://".
+Most NAS installs are reached over `http://`. Leave **Secure Cookies** off for
+that. With it on, the browser drops the sign-in cookie on a plain-HTTP page,
+and the login page says so before you type a password: "Signing in cannot
+complete on this page". Turn it on only when HealthLog is served over
+`https://`, and set App URL to that `https://` address.
## Back up your encryption key
-HealthLog encrypts tokens, notes, documents and in-database backups with
-`ENCRYPTION_KEY`. The key is not stored in the database. If the app is deleted
-and reinstalled with a new key over the old database dataset, the data is
+HealthLog encrypts tokens, notes, documents and in-database backups with the
+Encryption Key. The key is not stored in the database. If the app is deleted
+and installed again with a new key over the old database dataset, the data is
still there but cannot be read.
-1. Copy `ENCRYPTION_KEY` from the app's settings (Apps, HealthLog, Edit,
- Environment) into your password manager.
-2. Sign in as the admin and open Admin, Encryption. The step shows the key id
- and a fingerprint, the first 12 hex characters of SHA-256 over the key.
- Compare it with your copy:
+TrueNAS keeps the values you typed into the form with the app's configuration
+on the apps pool. Treat that as a convenience, not as your copy: deleting the
+app removes it, and a reinstall starts with empty fields. Keep the key in a
+password manager or another place outside the NAS.
+
+1. Open Apps, select HealthLog and choose Edit. The key is the Encryption Key
+ field under HealthLog Configuration.
+2. Sign in as the admin and open Admin, Encryption. The **Back up your
+ encryption key** step shows the active key id and a fingerprint, the first
+ 12 hex characters of SHA-256 over the key. Compare it with your copy:
```bash
echo -n "" | xxd -r -p | sha256sum | cut -c1-12
```
- or paste the copy into "Check my copy" (it is compared and dropped, never
- stored or logged).
+ or paste the copy into **Check my copy**. It is compared and dropped, never
+ stored or logged.
-3. Confirm the backup. Until you do, admins see a reminder on the dashboard.
- After a key rotation the step comes back for the new key.
+3. Press **I have backed up this key**. Until you do, admins see a reminder on
+ the dashboard. After a key rotation the step comes back for the new key.
+
+The catalog app sets `HEALTHLOG_PLATFORM=truenas`, so the step opens on these
+instructions.
+
+## Backups and restore
+
+A restore needs two things: a backup of the database **and** the Encryption
+Key that was active when it was taken. One without the other gets you nothing
+back.
+
+- **Snapshots.** Snapshot the Postgres Data Storage dataset (the ixVolume
+ lives under the apps pool, a host path wherever you put it). Stop the app
+ first, or the snapshot holds a database that was mid-write.
+- **A dump.** Open a shell on the `postgres` container (Apps, HealthLog,
+ the container's Shell) and run:
+
+ ```bash
+ pg_dump -U healthlog healthlog > /var/lib/postgresql/healthlog-$(date +%F).sql
+ ```
+
+ The file lands in the Postgres Data Storage dataset, next to the database
+ files, so copy it off the NAS as well.
+
+- **Off-host.** HealthLog can push encrypted nightly backups to any
+ S3-compatible bucket; see [backup-restore.md](../ops/backup-restore.md).
+
+To restore a dump, run the app with the Encryption Key the dump was taken
+under, put the dump file in the Postgres Data Storage dataset, and from the
+`postgres` container shell replace the database with it:
+
+```bash
+psql -U healthlog -d postgres \
+ -c "DROP DATABASE healthlog WITH (FORCE);" \
+ -c "CREATE DATABASE healthlog OWNER healthlog;"
+psql -v ON_ERROR_STOP=1 -U healthlog healthlog < /var/lib/postgresql/healthlog-YYYY-MM-DD.sql
+```
+
+Then restart the app. Restoring over the existing database without dropping
+it first does not work: the tables the background queue creates on its own
+refuse a plain overwrite.
## When the key does not match the database
At every start HealthLog checks that its key opens the data already in the
-database. If it does not, it refuses to serve instead of failing on every
-encrypted value: pages show "HealthLog cannot open its data", API requests
-answer `503` with `meta.errorCode` `encryption.key_mismatch`, `/api/health`
-answers `503` with `reason: "encryption_key_mismatch"`, and no background jobs
-run. TrueNAS shows the app as unhealthy rather than crashed, so the reason
-stays visible.
+database. A missing or malformed key stops the container at once with
+`HealthLog: ERROR — encryption configuration is invalid.` in its log. A
+well-formed key that does not open the stored data is different: HealthLog
+refuses to serve instead of failing on every encrypted value. Pages show
+"HealthLog cannot open its data", API requests answer `503` with
+`meta.errorCode` `encryption.key_mismatch`, `/api/health` answers `503` with
+`reason: "encryption_key_mismatch"`, and no background jobs run. TrueNAS
+shows the app as unhealthy rather than crashed, so the reason stays visible in
+the app's logs.
Fix it one of three ways, then restart the app:
-1. Put the original `ENCRYPTION_KEY` back.
-2. Point `DATABASE_URL` at the database that belongs to this key.
+1. Put the original Encryption Key back (Apps, HealthLog, Edit).
+2. Point the app at the database that belongs to this key, for example by
+ restoring that dataset.
3. Start fresh with an empty database. The old data and its backups need the
old key.
@@ -91,9 +161,8 @@ checks again at the next start.
If you are certain the configured key is the one the data was written with,
and the app still refuses because the check recorded a different key earlier,
-remove that record and restart. Open a shell on the database container (Apps,
-HealthLog, the database container's Shell) and run, with the key id the log
-names (`v1` unless you use `ENCRYPTION_KEYS`):
+remove that record and restart. Open a shell on the `postgres` container and
+run, with the key id the log names (`v1` unless you use `ENCRYPTION_KEYS`):
```bash
psql -U healthlog healthlog \
@@ -108,7 +177,7 @@ id: the rotation script removes its record once no values remain under it.
On a first start there is no record to remove. If the check refuses there and
you are certain the key is right, add `ENCRYPTION_KEY_CHECK` = `warn` under
-Apps, HealthLog, Edit, Environment as a last resort. The check still runs and
+Additional Environment Variables as a last resort. The check still runs and
logs, `/api/health` reports `warning: "encryption_key_mismatch"` without
failing, the app serves, and no key is recorded until the check passes. This
switches off a safety check: with a wrong key every encrypted value fails and
@@ -117,6 +186,49 @@ confirmed.
## Updates
-Pick the new tag, take a snapshot of the database dataset first, and update.
-Migrations run on start; the first start of a new install applies all of them,
-which is why the start period above is generous.
+The catalog follows HealthLog releases on its own: when a new image is
+published, the TrueNAS catalog picks it up and the app shows an update. Take a
+snapshot or a dump first, then update.
+
+Database migrations run at every start, before the web server listens, and
+only go forward. The start log says how many are pending. Going back to an
+older version with Roll Back does not undo them, and an older version may not
+start against a database a newer one has migrated. The way back is the
+database backup you took before the update, restored together with the older
+version.
+
+The Postgres Image field offers the Postgres major versions the catalog
+supports. Changing it runs a one-way upgrade of the database files, so back up
+before you touch it.
+
+## Before the listing: install via YAML
+
+Until the catalog app is listed, install HealthLog with Apps, Discover Apps,
+the menu next to Custom App, Install via YAML, using these values:
+
+| Setting | Value |
+| ------------ | ---------------------------------------------------------------------------------------- |
+| Image | `ghcr.io/mbombeck/healthlog`, tag `X.Y.Z` (amd64 and arm64) |
+| Port | `3000` (HTTP) |
+| User | runs as uid `1001` by default; no extra capabilities, `no-new-privileges` is fine |
+| Health check | `GET /api/health`, `200` when ready |
+| Start period | at least 60 s, with more retries than the image's 5 × 30 s: migrations run at every boot |
+| Database | PostgreSQL 16 or 18, no extensions, in its own container with its own dataset |
+| Volumes | none for the app; the database gets its own dataset |
+
+| Variable | Required | Notes |
+| ----------------------- | -------- | ---------------------------------------------------------------------------------------- |
+| `DATABASE_URL` | yes | `postgresql://:@:5432/` |
+| `ENCRYPTION_KEY` | yes | 64 hex characters: `openssl rand -hex 32`. Keep a copy outside the NAS. |
+| `API_TOKEN_HMAC_KEY` | yes | 64 hex characters, generated the same way. Changing it signs everyone out. |
+| `NEXT_PUBLIC_APP_URL` | yes | the address people open, for example `http://truenas.local:30180` |
+| `APP_URL` | yes | the same address |
+| `SESSION_COOKIE_SECURE` | no | `false` for a LAN install served over plain `http://`; leave unset behind an HTTPS proxy |
+| `HEALTHLOG_PLATFORM` | no | `truenas`: the admin's key backup step opens on the TrueNAS instructions |
+
+`HEALTHLOG_PROCESS_TYPE` stays unset (`all`): one container serves the web app
+and runs the background jobs. In a YAML install the key sits in the
+environment block, so the place to copy it from is `ENCRYPTION_KEY` there.
+To move from a YAML install to the catalog app later, take a dump, install the
+catalog app with the same key, and restore the dump as described under
+[Backups and restore](#backups-and-restore).