mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
A how-to for subscribing from Google, Apple and Outlook and what the feed shows; a section on the reproductive-health page saying that once it is on, the dates also sit with the calendar provider and turning it off does not reach those copies; a reverse-proxy note with Caddy and Nginx examples for keeping path credentials out of access logs; and API_TOKEN_HMAC_KEY rotation named as ending every feed link.
472 lines
25 KiB
Markdown
472 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 is built on the official Node 22 image (Debian
|
|
slim, glibc). 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, share link and cycle calendar-feed link, 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, share link and
|
|
calendar-feed 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).
|