Commit Graph
2 Commits
Author SHA1 Message Date
Frank 3c035ccef3 fix(audiobookshelf): set the port with the env names the image actually reads
The first deploy of the unit failed its own health gate in the worst possible shape:
systemd said active (running), the engine logged a clean database init, and the
server then listened on :80 under base path /audiobookshelf - so every probe of
:8000 was refused and wait_for_health timed out at 180s beside a container that
looked healthy.

Cause: ABS_PORT / ABS_CONFIG_DIR / ABS_METADATA_DIR / ABS_PUID are the npm
package's names, printed by the server README; this image never reads them. Its own
contract, read off the pinned digest with podman inspect, is PORT=80
CONFIG_PATH=/config METADATA_PATH=/metadata, and the server greps for exactly PORT,
CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH. The two path variables appeared to
work only because the image already exports the same values - the coincidence that
made the inert lines look correct.

The unit now speaks the image's names, adds ROUTER_BASE_PATH=/ so the service is
the plain host:port the docs and the homepage card promise, and the dead ABS_*
lines are deleted instead of kept as decoration. Proven against the same digest in
a throwaway container (temp state dirs, loopback-only mapping, removed after):
base path "/", listening on :8000, GET /healthcheck -> 200, and the old prefixed
path -> 404.

Guards: the quadlet seam now refuses an Environment=ABS_* line and requires
PORT/ROUTER_BASE_PATH; the vault page gains the law with its failure signature and
the two-second image-inspect check. Suite: ALL TESTS PASSED, 812 ok.
2026-11-02 02:28:37 -08:00
Frank 511d8dca93 feat(audiobookshelf): the audiobook server as a boot-persistent quadlet
Audiobookshelf serves /mnt/entropy/Audiobooks to browsers and phones with its own
library and per-listener positions. The design point of the unit: every byte this
server writes is its own state - SQLite library, accounts, progress, cached covers
- and all of it lands on the NVMe under /home/dank/Containers/audiobookshelf. The
audio bind is :ro, so a stray sidecar write onto the spindle is structurally
impossible rather than merely discouraged, and the array keeps exactly one
sanctioned write path (scripts/media-view.sh).

Three failure modes the unit is shaped around:

- Bind-source ordering. In a rootless unit RequiresMountsFor= is inert and
  ConditionPathIsMountPoint= is fail-dead, so a missing array does not stop the
  start: podman invents an empty directory and the first scan records an empty
  library as truth. ExecStartPre=wait-for-mount.sh is the gate that works here,
  the same one copyparty uses.
- The health binary. The image is Alpine: wget, no curl. A curl HealthCmd reads a
  healthy server as unhealthy and Restart=always turns that into a loop.
- The image pin. podman records the linux/amd64 manifest for what it pulled, not
  the manifest-list digest `latest` points at, so the amd64 digest is the pin that
  names the build this host runs. Upstream version tags stop at 2.19.0 while
  `latest` is newer, so latest@digest is the only form that is both current and
  exact - the jellyfin/romm precedent.

The first-boot scan reads the whole tree once, so IOWeight=10 keeps it behind
playback; that weight is only honoured because the user.slice accounting drop-in
and the user@1000 delegation are installed by the deployer.

Deployer registration is the standard four touch-points (client gate, remote
allowlist + handler + dispatcher, planner order/set/path map, test routing). The
handler creates both state roots before the first start - letting podman invent
them leaves the SQLite file unwritable by the container user under a rootless
first boot - and gates on /healthcheck.
2026-11-02 02:19:24 -08:00