podman wraps a bare HealthCmd as CMD-SHELL (measured on Dankclaw's podman 5.7:
--health-cmd "/surreal is-ready" stores {"Test":["CMD-SHELL","/surreal is-ready"]}),
and the pinned SurrealDB v2 image is distroless — no /bin/sh, not even `ls`.
So the probe could never run: every 30 s interval exited 1 in ~30 ms with empty
output while the database was listening and answering, and open-notebook's first
deploy died at the gate ("did not become healthy in 90s"). The hand-check
`podman exec … /surreal is-ready` printed OK, which is why the old comment
believed the probe worked.
The JSON array survives the Quadlet generator (--health-cmd "[\"/surreal\",\"is-ready\"]")
and stores ["CMD","/surreal","is-ready"], which execs directly: a throwaway
instance of the pinned digest went healthy on that form. The seam is re-pinned
to the shell-free contract; the old assertion forbade quotes in any HealthCmd,
which is what pinned the broken shape.
94 lines
5.0 KiB
INI
94 lines
5.0 KiB
INI
# Quadlet unit: open-notebook's SurrealDB (rootless, boot-persistent).
|
|
#
|
|
# Install path on dankclaw: ~/.config/containers/systemd/open-notebook-surrealdb.container
|
|
# Unit name: open-notebook-surrealdb.service (container name: open-notebook-surrealdb)
|
|
#
|
|
# WHAT IT IS. open-notebook's only datastore: notebooks, notes, sources,
|
|
# provider credentials and podcast episodes all live here. The app talks to it
|
|
# over the WebSocket RPC on the private bridge (SURREAL_URL in
|
|
# scripts/open-notebook.env), which is why the container NAME is part of the
|
|
# contract — renaming it breaks the app's database URL, not just this file.
|
|
#
|
|
# IMAGE. `surrealdb/surrealdb:v2` pinned by manifest-list digest, resolved from
|
|
# the registry on 2026-10-18 (that digest is SurrealDB 2.7.0, image built
|
|
# 2026-09-23; `v2` itself is a MOVING tag, so the digest — not the tag — is what
|
|
# makes this reproducible). Upstream open-notebook's own compose file pins the
|
|
# same `v2` tag, and its Dockerfile copies /surreal out of the same image for
|
|
# the retired single-container variant, so v2 is the version this app is built
|
|
# against. Pull=never + the documented pull step (docs: [[Open Notebook]]):
|
|
#
|
|
# podman pull docker.io/surrealdb/surrealdb:v2@sha256:e5d7387e3727bbe2f7cd6d869e48cd34777272258572ead37c9656b25f745723
|
|
#
|
|
# NO PUBLISHED PORT. 8000 is deliberately not published: the app reaches it by
|
|
# name on systemd-open-notebook, and a published 8000 would put the RPC with
|
|
# the root credentials on every interface (upstream publishes 127.0.0.1:8000
|
|
# "purely for local debugging" — a `podman exec` probe is enough here). The
|
|
# binary binds 0.0.0.0:8000 INSIDE its own namespace via the image's
|
|
# SURREAL_BIND env; without that image default it would bind loopback only and
|
|
# the app could never connect, so no --bind flag belongs here.
|
|
#
|
|
# CREDENTIALS, AND WHY THEY ARE NOT IN THE COMMAND LINE. The `surreal start`
|
|
# CLI reads its options from the environment (`--username` is [env:
|
|
# SURREAL_USER=], `--password` is [env: SURREAL_PASS=] — verified against the
|
|
# pinned image's own `start --help`), so the credentials arrive through the two
|
|
# EnvironmentFile lines and the argv stays clean. Passing `--pass` instead —
|
|
# what upstream's compose does — would put the database password in
|
|
# /proc/<pid>/cmdline, readable by any `ps` on the host. Note the two
|
|
# spellings: the DATABASE reads SURREAL_PASS, the APP reads SURREAL_PASSWORD,
|
|
# and scripts/open-notebook.env declares both because they must hold the same
|
|
# value; the deployer writes that pair when it seeds the host secret file.
|
|
#
|
|
# NO USER= OVERRIDE. The image runs as uid 65532 (Chainguard glibc-dynamic
|
|
# base). With a NAMED volume that works unmodified: podman creates the volume's
|
|
# backing directory owned by the container's user (measured with this digest —
|
|
# the host directory lands on the mapped subuid and the store starts clean),
|
|
# which is exactly why upstream's `user: root` line is not repeated here: that
|
|
# line is commented in upstream as being "required for bind mounts", and this
|
|
# unit has none. The consequence is that the DB files are NOT readable by dank
|
|
# without `podman unshare` — accepted for a datastore nobody edits by hand;
|
|
# episode AUDIO is a host directory for the opposite reason (see the app unit).
|
|
#
|
|
# HEALTH. The base image is distroless — measured against this digest, not even
|
|
# `ls` exists (`exec: "ls": executable file not found in $PATH`), so there is no
|
|
# `/bin/sh`, no curl and no wget. SurrealDB ships its own probe instead:
|
|
# `surreal is-ready` connects to ws://localhost:8000 by default, needs no
|
|
# credentials, prints `OK` and exits 0 while the server is up.
|
|
#
|
|
# It must be handed over as a QUADLET-SIDE JSON ARRAY, not a bare command.
|
|
# podman wraps a bare `HealthCmd` string as `CMD-SHELL` (measured on this host's
|
|
# podman 5.7: `--health-cmd "/surreal is-ready"` stores
|
|
# `{"Test":["CMD-SHELL","/surreal is-ready"]}`), and with no `/bin/sh` in the
|
|
# image that probe fails in ~30 ms with empty output on every interval while the
|
|
# database answers perfectly — which is exactly how this service's first deploy
|
|
# died on 2026-10-19 (`did not become healthy in 90s`). The quoted array survives
|
|
# the generator (`--health-cmd "[\"/surreal\",\"is-ready\"]"`) and stores
|
|
# `["CMD","/surreal","is-ready"]`, which execs directly and goes healthy. Do not
|
|
# "simplify" it back to a bare command.
|
|
|
|
[Unit]
|
|
Description=SurrealDB (open-notebook datastore)
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Container]
|
|
ContainerName=open-notebook-surrealdb
|
|
Image=docker.io/surrealdb/surrealdb:v2@sha256:e5d7387e3727bbe2f7cd6d869e48cd34777272258572ead37c9656b25f745723
|
|
Pull=never
|
|
Network=open-notebook.network
|
|
EnvironmentFile=/home/dank/serverAppConfigs/scripts/open-notebook.env
|
|
EnvironmentFile=/home/dank/Containers/open-notebook/env
|
|
Exec=start --log info rocksdb:/mydata/mydatabase.db
|
|
Volume=open-notebook-db:/mydata
|
|
HealthCmd=["/surreal","is-ready"]
|
|
HealthInterval=30s
|
|
HealthTimeout=10s
|
|
HealthRetries=3
|
|
HealthStartPeriod=20s
|
|
|
|
[Service]
|
|
Restart=always
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=default.target
|