mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
docs(self-hosting): add Unraid CA and Portainer templates, harden compose quick-start
Surface the load-bearing first-run vars in docker-compose.yml and the README quick-start: secret generation, the SESSION_COOKIE_SECURE=false LAN note, the single persistent volume, and a /api/version verify step. Add an Unraid Community Applications template plus a NAS walkthrough covering the external Postgres setup, and a Portainer app template referencing the compose stack.
This commit is contained in:
1 parent
3712b80d0f
commit
c903c7f1b1
6 files changed
+358
-8
No files matched your search
@@ -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://<host>: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.
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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=<your openssl rand -hex 32>
|
||||
ENCRYPTION_KEY=<your openssl rand -hex 32>
|
||||
API_TOKEN_HMAC_KEY=<your openssl rand -hex 32>
|
||||
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:<password>@<postgres-host>: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 `<password>` and `<postgres-host>` 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.
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0"?>
|
||||
<!--
|
||||
HealthLog — Unraid Community Applications template.
|
||||
|
||||
CA templates describe ONE container. HealthLog needs PostgreSQL 16, so
|
||||
install a PostgreSQL container FIRST (CA has community PostgreSQL templates),
|
||||
create a `healthlog` database + user, then point DATABASE_URL below at it.
|
||||
Full walkthrough: docs/self-hosting/unraid.md.
|
||||
|
||||
LAN installs serve plain HTTP (http://tower:3000). Keep SESSION_COOKIE_SECURE
|
||||
set to false (the default in this template) or login will silently fail.
|
||||
-->
|
||||
<Container version="2">
|
||||
<Name>HealthLog</Name>
|
||||
<Repository>ghcr.io/mbombeck/healthlog:latest</Repository>
|
||||
<Registry>https://github.com/MBombeck/HealthLog/pkgs/container/healthlog</Registry>
|
||||
<Network>bridge</Network>
|
||||
<MyIP/>
|
||||
<Shell>sh</Shell>
|
||||
<Privileged>false</Privileged>
|
||||
<Support>https://github.com/MBombeck/HealthLog/issues</Support>
|
||||
<Project>https://github.com/MBombeck/HealthLog</Project>
|
||||
<Overview>
|
||||
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.
|
||||
</Overview>
|
||||
<Category>Tools: Productivity: Other:</Category>
|
||||
<WebUI>http://[IP]:[PORT:3000]/</WebUI>
|
||||
<Icon>https://raw.githubusercontent.com/MBombeck/HealthLog/main/public/logo-512.png</Icon>
|
||||
<ExtraParams>--security-opt no-new-privileges:true</ExtraParams>
|
||||
<DonateText/>
|
||||
<DonateLink/>
|
||||
|
||||
<Config Name="WebUI Port" Target="3000" Default="3000" Mode="tcp" Description="HTTP port HealthLog listens on." Type="Port" Display="always" Required="true" Mask="false">3000</Config>
|
||||
|
||||
<Config Name="DATABASE_URL" Target="DATABASE_URL" Default="postgresql://healthlog:CHANGE-ME@POSTGRES-HOST:5432/healthlog?schema=public" Mode="" Description="Connection string for your separately-installed PostgreSQL 16. Replace CHANGE-ME with the DB password and POSTGRES-HOST with the Postgres container IP or name." Type="Variable" Display="always" Required="true" Mask="true"/>
|
||||
|
||||
<Config Name="ENCRYPTION_KEY" Target="ENCRYPTION_KEY" Default="" Mode="" Description="AES-256-GCM key for data at rest. Generate with: openssl rand -hex 32 (64-char hex). Back this up — losing it makes encrypted data unrecoverable." Type="Variable" Display="always" Required="true" Mask="true"/>
|
||||
|
||||
<Config Name="API_TOKEN_HMAC_KEY" Target="API_TOKEN_HMAC_KEY" Default="" Mode="" Description="HMAC key for API token hashing. Generate with: openssl rand -hex 32 (64-char hex)." Type="Variable" Display="always" Required="true" Mask="true"/>
|
||||
|
||||
<Config Name="SESSION_COOKIE_SECURE" Target="SESSION_COOKIE_SECURE" Default="false" Mode="" Description="Set to false for LAN / plain-HTTP installs (http://tower:3000) or login fails. Set to true only behind an HTTPS reverse proxy." Type="Variable" Display="always" Required="true" Mask="false">false</Config>
|
||||
|
||||
<Config Name="NEXT_PUBLIC_APP_URL" Target="NEXT_PUBLIC_APP_URL" Default="http://tower:3000" Mode="" Description="Public URL users reach HealthLog at. Use the LAN URL (e.g. http://tower:3000) or your HTTPS domain behind a reverse proxy." Type="Variable" Display="always" Required="true" Mask="false"/>
|
||||
|
||||
<Config Name="APP_URL" Target="APP_URL" Default="http://tower:3000" Mode="" Description="Internal app base URL. Normally identical to NEXT_PUBLIC_APP_URL." Type="Variable" Display="always" Required="true" Mask="false"/>
|
||||
|
||||
<Config Name="ENCRYPTION_KEYS" Target="ENCRYPTION_KEYS" Default="" Mode="" Description="Optional. Key-rotation map, e.g. {"v1":"<hex>","v2":"<hex>"}. Leave empty unless rotating; use ENCRYPTION_KEY instead." Type="Variable" Display="advanced" Required="false" Mask="true"/>
|
||||
|
||||
<Config Name="ENCRYPTION_ACTIVE_KEY_ID" Target="ENCRYPTION_ACTIVE_KEY_ID" Default="" Mode="" Description="Optional. Active key id for the ENCRYPTION_KEYS map (e.g. v2). Leave empty when using ENCRYPTION_KEY." Type="Variable" Display="advanced" Required="false" Mask="false"/>
|
||||
</Container>
|
||||
Reference in new issue
Block a user