Pin images by digest; add update, smoke-test and status scripts

Goal: at every session start, run exactly the application the repo says — and be able
to move to a new upstream release deliberately, with a way back.

The old setup (v1-latest + pull_policy: always) was unreliable in both directions.
`docker compose up -d` silently pulled a new application, including an irreversible
SurrealDB schema migration, while a reboot (restart: always) or `docker compose start`
kept running the old image without pulling anything. The running version was effectively
unpredictable.

Note v1-latest tracks releases, not main: the current image (v1.10.0, 2026-06-18) IS the
latest release — the repo being "ahead" is unreleased code, which is explicitly not
wanted here. So this is about guaranteeing and detecting, not catching up.

- docker-compose.yml: both images pinned by digest, pull_policy: missing.
- scripts/check_updates.sh: reports running version/digest vs the latest release and
  registry digest. Changes nothing; meant for the session-start ritual.
- scripts/update_stack.sh: resolve new digest -> stop -> back up surreal_data,
  notebook_data and docker-compose.yml -> repin -> start -> smoke test -> roll back data
  AND compose file if the smoke test fails. The data backup is the point: the app migrates
  the DB on startup and an older app cannot read a migrated DB, so a bad update would
  otherwise be a one-way door. tar runs inside a container because the data dirs are
  root-owned and the host user cannot restore over them.
- scripts/smoke_test.sh: checks what actually breaks here, not just "does it start".
  Every local adaptation leans on upstream internals and can break silently: the
  prompts/podcast directory mount masks the image's directory (a new template upstream
  would be invisible), config/content_core.yaml freezes content-core's defaults (because
  CCORE_CONFIG_PATH replaces rather than merges), and the env-var workarounds depend on
  current content-core/esperanto/podcast_creator behaviour. So it verifies those
  assumptions explicitly, plus API, TTS audio, STT and a real YouTube transcript.

Both scripts read the digest from the lfnovo/open_notebook line specifically. A naive
"first sha256 in the file" grep matches surrealdb (listed first) — caught while testing:
check_updates.sh falsely reported an update, and update_stack.sh would have rewritten the
database image instead of the application.

Verified: smoke test green against the current stack, and red (exit 1) when failures are
injected (dead TTS port, removed template variable). Backup/restore mechanics exercised
separately: 44 MB in 3.3s, restore yields 13 DB files and 91 notebook files. The update
path itself cannot be exercised end to end until a newer image exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-07-11 14:12:31 +02:00
commit 4805fe75ed
7 changed files with 434 additions and 7 deletions

View file

@ -9,13 +9,62 @@ This is a local Docker Compose deployment of [Open Notebook](https://github.com/
## Commands
```bash
docker compose up -d # start (pulls images per pull_policy: always)
docker compose up -d # start (images are digest-pinned; no implicit upgrade)
docker compose down # stop and remove containers (data volumes persist)
docker compose logs -f open_notebook # follow app logs
docker compose ps # container status
./scripts/check_updates.sh # is there a newer release? (reports only, changes nothing)
./scripts/update_stack.sh # backup -> update -> smoke test -> rollback on failure
./scripts/smoke_test.sh # verify the stack still does what we rely on
```
No build/lint/test suite exists in this repo — nothing here to run.
`smoke_test.sh` is the closest thing to a test suite here — run it after touching
`docker-compose.yml`, the bind-mounted overlays, or the host TTS/STT services.
## Image versioning
Images are **pinned by digest**, and updating is an explicit act — never a side effect of
starting the stack.
The previous setup (`v1-latest` + `pull_policy: always`) was unreliable in *both* directions:
`docker compose up -d` silently pulled a new application — including an irreversible SurrealDB
schema migration — while a reboot (`restart: always`) or `docker compose start` kept running the
**old** image without any pull. So the running version was essentially unpredictable.
Note `v1-latest` tracks *releases*, not `main`: the tag lags the repo by weeks. Chasing `main`
would mean building from source and running unreleased code — explicitly not wanted here.
Updating (`scripts/update_stack.sh`): resolve the new digest → stop → back up `surreal_data` +
`notebook_data` + `docker-compose.yml` into `backups/<ts>/` → rewrite the digest → start → run
`smoke_test.sh`**roll back data and compose file if the smoke test fails**. The data backup is
the point: the app migrates the database on startup, and an older application generally cannot
read a migrated database, so a bad update would otherwise be a one-way door. Backup/restore runs
`tar` **inside a container** because the data directories are root-owned (the container writes as
root) and the host user cannot restore over them.
Both scripts read the digest from the `lfnovo/open_notebook` line specifically — a naive "first
sha256 in the file" grep picks up **surrealdb** (it is listed first), which would make the update
script rewrite the database image instead of the application.
### Why a smoke test, and not just "does it start"
Every local adaptation in this repo leans on upstream internals, and an update can break them
*silently* — the stack comes up fine and misbehaves later:
- the `prompts/podcast/` **directory** mount masks the image's directory: a third template added
upstream would simply be invisible;
- `config/content_core.yaml` is a full dump of content-core's defaults (because `CCORE_CONFIG_PATH`
replaces rather than merges), so new upstream defaults would be frozen out;
- the env-var workarounds (`TTS_BATCH_SIZE`, `OPENAI_COMPATIBLE_BASE_URL_STT`,
`ESPERANTO_TTS_TIMEOUT`) depend on how content-core / esperanto / podcast_creator behave today.
`smoke_test.sh` therefore checks those assumptions explicitly (template file list vs. the image,
`{{ language }}` and `/no_think` still present, `language` still plumbed by podcast_creator,
`TTS_BATCH_SIZE` still read, `preferred_languages` still used, esperanto still honouring
`OPENAI_COMPATIBLE_BASE_URL_STT`, our config still covering all default keys) on top of the obvious
end-to-end ones (API, TTS audio, STT, YouTube transcript). `SKIP_NET=1` skips the internet-dependent
check.
## Architecture