ROUTER_BASE_PATH=/ served a healthy API and a web UI that could never boot. The index.html in the pinned image is built with routerBasePath:/audiobookshelf, so every script request fell through to the SPA fallback and came back as text/html, which Chrome refuses to execute. healthcheck, the API and the phone app all stayed fine, which is exactly how this hid behind an API-only deploy check.
The deployer now reads the mounted base path out of the unit and hands it to the bootstrapper as ABS_URL, so the API door, the login route and the OIDC redirect subfolder cannot drift from the unit. healthcheck answers at the root under either base path, so the health gate is unchanged.
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.
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.