mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
The dependency and build stages stay on node:22-trixie-slim, where pnpm picks the glibc native packages. The runner moves to wolfi-base, pinned by its index digest: glibc, about fifty packages, and every one of them known to the image scan, Node and the PostgreSQL client included. nodejs-22, tzdata and postgresql-18-client come from apk, so the pgclient stage and its pin guard go. The builder writes its Node module ABI and glibc version out and the runner refuses to build on another ABI or an older glibc. Wolfi's npm is major 12 and runs no install scripts unless package.json allows them, so the Prisma CLI install names the three packages that have one, and the build checks that the schema engine file is there before asking the CLI about it. npm is installed for that step and removed in the same layer; the check that nothing is left asks a fresh shell, whose command cache cannot remember the deleted binary.
471 lines
25 KiB
Markdown
471 lines
25 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
|
|
and linux/arm64), so a Raspberry Pi 4 or 5 or an x86 VPS all work
|
|
unchanged. The image runs on glibc (a Wolfi base). Document thumbnails and
|
|
scanned-PDF rendering use a native renderer that runs on every arm64
|
|
board, the Pi 4 included, and on x86-64 CPUs with AVX2; on an older x86
|
|
CPU (Celeron/Atom-class NAS chips) it switches itself off and those two
|
|
features fall back to text only. `NATIVE_CANVAS=off` or `on` in `.env`
|
|
overrides that detection (`on` crashes PDF processing on a CPU that truly
|
|
lacks the instructions).
|
|
- **3 GB RAM (4 GB recommended), 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. The app
|
|
container is capped at `APP_MEMORY_LIMIT`, `1536m` by default (Node
|
|
then sizes its heap at about 792 MB), and Postgres and the system need
|
|
the rest. 1536 MB is the supported floor for the app container: do not
|
|
lower it to fit a smaller host. Below it, a long-used account or a large
|
|
import can fill the heap and restart the app; the app still starts, but
|
|
the boot log, the support report and Admin > System status say it runs
|
|
below the supported floor. On a busy instance, raise it.
|
|
- **Outbound HTTPS** to `ghcr.io` (image pulls) plus whichever
|
|
integration endpoints you plan to enable (Withings, OpenAI, etc.).
|
|
The environmental-context module additionally reaches
|
|
`archive-api.open-meteo.com`, `air-quality-api.open-meteo.com` and
|
|
`geocoding-api.open-meteo.com` for weather, air quality and the city
|
|
search. It is on by default, but nothing leaves the host until someone
|
|
sets a home location, and every outbound call goes through the
|
|
SSRF-guarded egress wrapper.
|
|
Three requests happen by default without anything connected: the
|
|
HaveIBeenPwned range query when a password is set (only the first five
|
|
characters of the password's SHA-1 are sent), a daily check of the
|
|
GitHub releases API for the update badge, and the online IP
|
|
geolocation lookup for login events. `PASSWORD_BREACH_CHECK_DISABLED`,
|
|
`UPDATE_CHECK_DISABLED` and `IP_GEO_LOOKUP_DISABLED` turn each off; see
|
|
`.env.production.example`.
|
|
- **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 a short starter with the variables most self-hosts
|
|
touch. The complete reference, with every variable the app reads and what
|
|
it does, is `.env.production.example`; CI checks that file against
|
|
`scripts/env-manifest.json`. Anything you do not set falls back to a sane
|
|
default or leaves the optional subsystem off.
|
|
|
|
## 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 and web session cookies before storage | Invalidates every issued `hlk_*` token and signs out every browser session |
|
|
|
|
You do **not** edit `DATABASE_URL` on the bundled `docker-compose.yml`
|
|
stack: compose constructs the connection string inside the `app`
|
|
service from `${POSTGRES_PASSWORD}` (its `environment:` block overrides
|
|
any `DATABASE_URL` in `.env`), so the `openssl` line above is the only
|
|
password step. `DATABASE_URL` only matters if you run a **bare Node**
|
|
process (`pnpm dev` against a host Postgres) or a **custom compose** that
|
|
does not derive it — in that case uncomment and set `DATABASE_URL`
|
|
explicitly in `.env` (see the commented example there).
|
|
|
|
### Using an existing or shared Postgres
|
|
|
|
HealthLog can live in a database on a Postgres instance you already run,
|
|
version 16 or newer; the compose file ships 18, and every release is tested
|
|
on both.
|
|
It stores every time as UTC and pins each connection it opens to UTC
|
|
(`TimeZone=UTC`, sent with the libpq `options` startup parameter), so the
|
|
server's or the database's own `timezone` setting does not matter: an
|
|
instance set to `Europe/Berlin` works the same as one set to UTC. The app
|
|
checks the setting at startup and logs a warning naming the zone if a
|
|
connection still reports something else.
|
|
|
|
Behind PgBouncer with `DATABASE_SESSION_OPTIONS_DISABLED=1` (see
|
|
[Scaling](./scaling.md#postgres-connection-pool-sizing)) the app pins the
|
|
zone with `SET TIME ZONE 'UTC'` on each new connection instead; PgBouncer
|
|
keeps that setting per client in transaction pooling too. If the startup
|
|
warning still appears, set the database default as well:
|
|
|
|
```sql
|
|
ALTER DATABASE healthlog SET timezone TO 'UTC';
|
|
```
|
|
|
|
## 3. Bring the stack up
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
Compose starts two services and one short-lived step:
|
|
|
|
- `db` — PostgreSQL 18 with a named volume for persistence.
|
|
- `db-upgrade` — moves a Postgres 16 database from v1.45 or earlier into
|
|
the new one, once, and exits; on a new install it has nothing to do. See
|
|
[Moving to a newer Postgres major](../ops/postgres-major-upgrade.md).
|
|
- `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.
|
|
|
|
> **Close public registration if you expose the instance to the
|
|
> internet.** Registration stays **open** after the first admin so you
|
|
> can invite the rest of your household — but on a publicly reachable
|
|
> host that means strangers can still sign up. Once everyone who needs
|
|
> an account has one, turn it off in **`/admin` → registration toggle**
|
|
> (or keep it open and hand out invite links instead). The admin
|
|
> settings show a reminder underneath the toggle while registration is
|
|
> open.
|
|
|
|
> **No email is configured out of the box** — a deliberate first-boot
|
|
> simplicity win, but it also means password reset has no self-service
|
|
> path until you set SMTP (see `docs/self-hosting/notifications.md`). If
|
|
> you are the sole admin, **save your password in a manager now**: a
|
|
> locked-out solo admin with no SMTP recovers only via the CLI script in
|
|
> `docs/ops/password-reset.md`.
|
|
|
|
## 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` |
|
|
| Tailnet-only HTTPS via `tailscale serve` (no reverse proxy) | `docs/self-hosting/tailscale.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
|
|
|
|
- **Where to look.** For a bug report, copy the support report (Admin,
|
|
System status) and the support info (Settings, About), and note the error
|
|
id the failure showed. Admin, App logs finds the event behind an error id.
|
|
What each contains, and what it never contains, is in
|
|
[`support.md`](./support.md).
|
|
- **`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/health` every 30 seconds
|
|
(it returns 503 while the database or worker is down) and will mark
|
|
the container unhealthy if Postgres is still
|
|
starting. Tail `docker compose logs db` to confirm Postgres came up
|
|
cleanly.
|
|
- **The login page says "Signing in cannot complete on this page".** The
|
|
server sets its session cookie with the `Secure` flag (the production
|
|
default) and the page was opened over plain `http://`, where the browser
|
|
would drop it. Set `SESSION_COOKIE_SECURE=false` for a server reached only
|
|
over HTTP on a private network, or open the `https://` address (behind a TLS
|
|
proxy, make sure it forwards `X-Forwarded-Proto: https`). The sign-in is
|
|
refused before any password, code or ticket is used.
|
|
- **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`).
|
|
|
|
## Save the recovery kit
|
|
|
|
No key HealthLog encrypts with is stored in the database. Without them the
|
|
encrypted parts of the database, a database dump and every backup copy cannot
|
|
be read. The recovery kit under Admin, Encryption lists every key a restore of
|
|
this server needs:
|
|
|
|
- `ENCRYPTION_KEY` (or each id in the `ENCRYPTION_KEYS` map), including a
|
|
retired id for as long as a stored or off-host backup copy still needs it;
|
|
- `BACKUP_ENCRYPTION_KEY` and any `BACKUP_ENCRYPTION_PREVIOUS_KEYS`, when
|
|
off-host copies are set up;
|
|
- `API_TOKEN_HMAC_KEY`, without which every session, token and share link ends
|
|
(no data is lost).
|
|
|
|
Each key is shown by id and a fingerprint (the first 12 hex characters of
|
|
SHA-256 over the key), never the key itself, and **Check my copy** compares a
|
|
pasted copy without storing it. Keep a copy of each outside the server, then
|
|
confirm the kit. Admins see a dashboard reminder until it is confirmed, and the
|
|
kit is due again whenever a key it needs changes: a rotation, a new backup
|
|
key, a re-keyed install. The kit also links the
|
|
[disaster recovery guide](../ops/disaster-recovery.md).
|
|
|
|
The **Recovery readiness** card on the admin overview and the Backups page
|
|
judges every backup signal in one place: the database dump, the weekly account
|
|
copies, the off-host copies, the last restore drill, whether off-host copies
|
|
are set up, whether the bucket ever removes old database dumps, and the
|
|
recovery kit. An hourly check sends each admin a
|
|
**Backup health** notification when a signal turns to "needs attention" or "at
|
|
risk", once a day while it stays at risk, and once when it recovers. Route or
|
|
switch it off per channel under Notifications.
|
|
|
|
| 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 kit on your platform's instructions.
|
|
|
|
To compute the fingerprint of your copy of a hex key, and of the token key:
|
|
|
|
```bash
|
|
echo -n "<your key>" | xxd -r -p | sha256sum | cut -c1-12
|
|
printf %s "<API_TOKEN_HMAC_KEY>" | sha256sum | cut -c1-12
|
|
```
|
|
|
|
**HealthLog refuses to serve: encryption key does not match this database.**
|
|
At start the app checks that its key opens the stored data. If it does not,
|
|
every API request answers `503` with `meta.errorCode`
|
|
`encryption.key_mismatch`, `/api/health` answers `503` with
|
|
`reason: "encryption_key_mismatch"`, and background jobs are not started.
|
|
Restore the original key, point `DATABASE_URL` at the matching database, or
|
|
start with an empty database; then restart.
|
|
|
|
The check remembers the key it first saw for each key id in the table
|
|
`encryption_key_canaries`. Before it records a key, it reads the oldest
|
|
encrypted values in the database and records the key only if they open, or
|
|
if the database holds no encrypted value under that key id at all. When the
|
|
stored data proves neither (a single unreadable value, the oldest values
|
|
failing while newer ones open, or a probe that ran out of time), the app
|
|
keeps serving, records nothing, logs a warning that starts with
|
|
`[boot] Encryption key check inconclusive`, and checks again at the next
|
|
start.
|
|
|
|
If you are certain the configured key is the one your data was written
|
|
with, and the check still refuses because it recorded a different key
|
|
earlier, reset the check for that key id and restart. The refusal in the log
|
|
prints the exact statement; for the default key id it is:
|
|
|
|
```bash
|
|
docker compose exec -T db psql -U healthlog healthlog \
|
|
-c "DELETE FROM encryption_key_canaries WHERE key_id = 'v1';"
|
|
docker compose restart app
|
|
```
|
|
|
|
The next start probes the stored data again and records the key only if
|
|
that data opens with it. Do this only when you are sure: if the key is in
|
|
fact wrong, the app serves errors on every encrypted value instead of the
|
|
one clear refusal. After a key rotation the rotation script removes the
|
|
record of a retired key id itself, once it reports that no values remain
|
|
under that id.
|
|
|
|
At a first start there is no record yet, so there is nothing to reset. If
|
|
the check refuses there and you are certain the key is right, the last
|
|
resort is `ENCRYPTION_KEY_CHECK=warn` (default `enforce`): the check still
|
|
runs and logs its full result, `/api/health` names it as
|
|
`warning: "encryption_key_mismatch"` without failing, requests are served,
|
|
and no key is recorded while the check does not pass. This disables a
|
|
safety check. With a wrong key every encrypted value fails and new rows are
|
|
written under that key, so remove the setting once the key is confirmed.
|
|
|
|
## Backup and restore
|
|
|
|
Your database is the only stateful piece — back it up, and back up the
|
|
key that decrypts it.
|
|
|
|
**Database dumps the app takes itself.** Every night, and before an update
|
|
applies migrations, the app writes a `pg_dump` of the whole database with a
|
|
manifest into the `backups` volume and keeps the newest few. With the
|
|
off-host bucket configured, each nightly dump is also uploaded, encrypted.
|
|
`docker compose run --rm db-restore latest` restores one into an empty
|
|
database. What is kept where, the keys a restore needs, and restoring onto a
|
|
new server: [`docs/ops/disaster-recovery.md`](../ops/disaster-recovery.md).
|
|
Keep a copy of those dumps off this host.
|
|
|
|
**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).
|
|
|
|
## Updates
|
|
|
|
A new image can bring database migrations, and `docker-entrypoint.sh`
|
|
applies them on the first start of that image. Most are additive, but some
|
|
rewrite data, and going back to the previous image does not undo them. The
|
|
way back from a migration that went wrong is a dump taken before it. The
|
|
entrypoint takes one itself (`healthlog-<time>-pre-migration.dump` in the
|
|
`backups` volume, see
|
|
[`docs/ops/disaster-recovery.md`](../ops/disaster-recovery.md)); a dump of
|
|
your own before every pull, kept off the host, is the second copy:
|
|
|
|
```bash
|
|
# 1. Dump the database (same command as under "Backup and restore").
|
|
docker compose exec -T db pg_dump -U healthlog healthlog \
|
|
| gzip > healthlog-before-update-$(date +%F).sql.gz
|
|
|
|
# 2. Pull the new image and recreate the app container.
|
|
docker compose pull app
|
|
docker compose up -d app
|
|
|
|
# 3. Confirm the running build.
|
|
curl -s http://localhost:3000/api/version
|
|
```
|
|
|
|
Keep the dump until the new version has run for a few days, and keep your
|
|
encryption key with it: a dump without the key cannot be read.
|
|
|
|
The database image is a separate matter: a new Postgres minor is a change of
|
|
the `image:` line. The move from 16 to 18 runs by itself on the first start
|
|
of the compose file from v1.46; a database you run yourself moves by dump
|
|
and restore. Both are in
|
|
[`docs/ops/postgres-major-upgrade.md`](../ops/postgres-major-upgrade.md).
|
|
|
|
The start log says what is about to happen before the migrations run:
|
|
|
|
```text
|
|
HealthLog: 2 database migrations pending. Unless DB_BACKUP_BEFORE_MIGRATE=false, a pg_dump of the database is written to DB_BACKUP_DIR before they run (docs/ops/disaster-recovery.md); a migration is not undone by going back to the previous image.
|
|
HealthLog: taking a database dump before 2 pending migration(s) into /var/lib/healthlog/backups...
|
|
HealthLog: pre-migration dump written: healthlog-20261025T081502Z-pre-migration.dump (212.4 MB, 231 tables, 38 s).
|
|
HealthLog: Running database migrations...
|
|
```
|
|
|
|
or `HealthLog: Database schema is up to date.` when the image brings none.
|
|
The migrations run right after the dump. A dump that fails on a writable
|
|
directory stops the start before anything is migrated, and the log names the
|
|
switch that skips it once (`DB_BACKUP_BEFORE_MIGRATE=false`). Your own dump
|
|
belongs before step 2, not after you see these lines; by then the database is
|
|
already being migrated.
|
|
|
|
If an update to v1.43 stops at migration `0409_drop_plaintext_location`,
|
|
nothing was changed; the recovery is in
|
|
[`docs/ops/migration-0409-plaintext-location.md`](../ops/migration-0409-plaintext-location.md).
|
|
|
|
For the ordering on a host where you want the migration result before the
|
|
new container takes traffic, see [`docs/ops/deploy.md`](../ops/deploy.md).
|