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.
This commit is contained in:
Frank committed 2026-11-02 02:19:24 -08:00
1 parent 29aefc3b6f
commit 511d8dca93
6 files changed
+133 -1

No files matched your search

+3
View File
@@ -60,6 +60,7 @@ SERVICE_ORDER = (
"calibre-server",
"plex",
"tautulli",
"audiobookshelf",
"prowlarr",
"qbittorrent",
"radarr",
@@ -123,6 +124,7 @@ DANKSERVER_SERVICES = {
"calibre-server",
"plex",
"tautulli",
"audiobookshelf",
"prowlarr",
"qbittorrent",
"radarr",
@@ -420,6 +422,7 @@ def services_for_path(path: str, target: str) -> set[str]:
"copyparty": {"copyparty"},
"plex": {"plex"},
"tautulli": {"tautulli"},
"audiobookshelf": {"audiobookshelf"},
}
_add(services, *quadlet_services.get(parts[1], set()))
if len(parts) >= 2 and parts[1] == "host":
+21
View File
@@ -95,6 +95,7 @@ service_is_allowed() {
dankclaw:frank-tts-engine|dankclaw:frank-tts|\
dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\
dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:calibre-server|dankserver:plex|dankserver:tautulli|\
dankserver:audiobookshelf|\
dankserver:prowlarr|dankserver:qbittorrent|dankserver:radarr|\
dankserver:sonarr|dankserver:lidarr|dankserver:readarr|\
dankserver:frank-archon|dankserver:node-exporter|dankserver:cadvisor|dankserver:gpu-exporter|\
@@ -1300,6 +1301,25 @@ deploy_calibre_web() {
systemctl --user restart calibre-web.service
}
# Audiobookshelf keeps its whole state (SQLite library, accounts, progress,
# cached covers) on the NVMe under /home/dank/Containers/audiobookshelf; the
# audio is a read-only bind off the array, so the only thing the deployer has to
# create is the two state roots. mkdir names the owner — letting podman invent
# the directories leaves the SQLite file unwritable by the container user on a
# rootless first boot. No `systemctl --user enable`: a generated Quadlet unit
# cannot be enabled, and WantedBy=default.target is what carries it across a
# reboot.
deploy_audiobookshelf() {
cp "dankserver/audiobookshelf/quadlet/audiobookshelf.container" "$quadlet_unit_dir/"
systemctl --user daemon-reload
mkdir -p /home/dank/Containers/audiobookshelf/config \
/home/dank/Containers/audiobookshelf/metadata
systemctl --user restart audiobookshelf.service
# /healthcheck answers once the HTTP server is bound; the library scan that
# follows runs in the background and must not gate the deploy.
wait_for_health audiobookshelf http://127.0.0.1:8000/healthcheck 90
}
# The content server's single admin account and the desktop's basic-auth
# credential (same pair) are bootstrapped exactly once: the env file is the
# plaintext store (mode 600), the userdb's pw column is plaintext too, so
@@ -2176,6 +2196,7 @@ deploy_service() {
copyparty) deploy_copyparty ;;
plex) deploy_plex ;;
tautulli) deploy_tautulli ;;
audiobookshelf) deploy_audiobookshelf ;;
alloy) deploy_alloy ;;
prometheus|grafana|loki|node-exporter|cadvisor|alertmanager|blackbox-exporter) deploy_observability "$service" ;;
gpu-exporter) deploy_gpu_exporter ;;
+1
View File
@@ -148,6 +148,7 @@ valid_service() {
dankclaw:frank-dragoon|dankclaw:frank-zealot|\
dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\
dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:calibre-server|dankserver:plex|dankserver:tautulli|\
dankserver:audiobookshelf|\
dankserver:qbittorrent|dankserver:radarr|dankserver:sonarr|\
dankserver:lidarr|dankserver:readarr|dankserver:node-exporter|dankserver:cadvisor|dankserver:gpu-exporter|\
dankserver:prowlarr|dankserver:frank-archon|\
+1 -1
View File
@@ -334,7 +334,7 @@ for path in "${CHANGED[@]}"; do
# Cutover-era dankserver quadlet units (romm trio, jellyfin, calibre-web,
# copyparty, plex, tautulli): the seam checks boot-persistence +
# v1-generator contract; deploy-flow seams guard the deployer routing.
dankserver/romm/*|dankserver/jellyfin/*|dankserver/calibre-web/*|dankserver/copyparty/*|dankserver/plex/*|dankserver/tautulli/*)
dankserver/romm/*|dankserver/jellyfin/*|dankserver/calibre-web/*|dankserver/copyparty/*|dankserver/plex/*|dankserver/tautulli/*|dankserver/audiobookshelf/*)
add_test tests/dankserver_quadlet.bats
add_test tests/deployment_flow.bats
add_test tests/deployment_scoping.bats