--- type: reference description: "Audiobookshelf on Dankserver: quadlet shape, the read-only array bind, state on the SSD, first-run account, laws." tags: - dankserver - audiobooks - quadlet timestamp: 2026-11-02 --- # Audiobookshelf Audiobookshelf (ABS) serves the [[Entropy Volume]] audiobook library to browsers and phones: it scans `/mnt/entropy/Audiobooks`, keeps its own SQLite library with per-listener playback positions, and streams the audio. It is **not** a media server — it shares nothing with [[Plex]] or [[Jellyfin]] and owns no files on the array. **Host:** Dankserver — **Unit:** `audiobookshelf.service` (rootless Quadlet, user manager) — **Internal-only:** LAN address, no vhost, no DNS name. ## Shape | Item | Value | |------|-------| | Unit file | `dankserver/audiobookshelf/quadlet/audiobookshelf.container` (the authority for every number here) | | Image | `ghcr.io/advplyr/audiobookshelf` pinned by **amd64 manifest digest** — see Laws | | Port | `PublishPort=8000:8000` — web UI and API on the same port | | Network | `pasta` (standalone; publishes nothing to other containers) | | Audio bind | `/mnt/entropy/Audiobooks:/audiobooks:ro` — read-only, by design | | State | `/home/dank/Containers/audiobookshelf/config` → `/config`, `…/metadata` → `/metadata` (both NVMe) | | Mount gate | `ExecStartPre=…/wait-for-mount.sh /mnt/entropy` | | Health | `wget` against `/healthcheck` inside the container, 30 s / 3 retries | | I/O share | `IOWeight=10` — a scan loses the array to every other tenant | | Self-healing | `Restart=always`, `RestartSec=5`, `WantedBy=default.target` | | Deploy | `./scripts/deploy.sh dankserver --service audiobookshelf` | | Handler | `deploy_audiobookshelf` in `scripts/deploy-remote.sh` | ## First run 1. Open `http://192.168.0.235:8000` **from the LAN**. ABS accepts the initial account creation only from a local-network address; a remote address gets a refusal, not a form. 2. Create the owner account, then add one **Audi Book** library with folder `/audiobooks`. The path is the in-container name of the read-only bind. 3. The first scan reads the whole tree once. It runs in the background — the deploy gate is already green by then, and `IOWeight=10` keeps it off the critical path. Nothing in this repo holds an ABS credential: the account is created in the browser and lives in the `config` bind on the SSD. ## Laws - **The audio bind stays `:ro`.** ABS writes nothing that belongs beside the audio: its library, covers, progress and sidecar metadata all land in `/metadata` on the SSD. A writable bind would put stray writes on the spindle and hand the array to a service with no reason to hold it. If per-book `.opf`/`.nfo` sidecars on disk are ever wanted, the rollback is one line — point the audio bind at the [[Media Metadata View]] the media servers already use — but that view's upper layer exists for those two servers' sidecars, so a third writer needs a reason first ([[Media Metadata View]]). - **Do not bind the raw array without the mount gate.** In a rootless unit `ConditionPathIsMountPoint=` is fail-*dead* and `RequiresMountsFor=` is inert, so a missing array does not stop the start: podman invents an empty directory, ABS indexes it, and an empty library becomes recorded truth. `wait-for-mount.sh` is the only gate that works here — same law the media servers obey ([[Quadlet Lifecycle]]). - **The `HealthCmd` must use `wget`.** The image is Alpine and ships `wget`, not `curl`; a `curl` health command reads a perfectly healthy server as unhealthy and `Restart=always` turns that into a loop. - **The image pin is the amd64 manifest digest, not the index digest.** `podman inspect` reports the platform manifest (`RepoDigests[0]`) for what was pulled, which differs from the manifest-list digest `latest` points at. Pin the one this host runs. `latest` is newer than the newest version tag: upstream stopped publishing version tags after `2.19.0` while `latest` kept moving, so "highest version tag" is stale here — read the index, don't guess from tags. - **`IOWeight=10` is inert without the host drop-ins.** `dankserver/host/systemd/` carries `user.slice.d/io-accounting.conf` and the `user@1000` delegate, and the deployer installs both; without them systemd silently drops the weight ([[Quadlet Lifecycle]]). - **No `systemctl --user enable`.** Generated Quadlet units cannot be enabled on this host; `WantedBy=default.target` in the unit file is what carries the service across a reboot ([[Quadlet Lifecycle]]). - **LAN-only until Frank says otherwise.** `0.0.0.0:8000` with no reverse proxy in front. Any public vhost requires the Anubis + Authentik chain first, and the outpost proxy provider has to exist before the vhost flips (external exposure policy in `AGENTS.md`). ## Verification `./tests/run.sh` covers this unit through `tests/dankserver_quadlet.bats` (boot persistence, the SSD state roots, the `:ro` array bind, the mount gate, the `wget` health check) and `tests/media_overlay.bats` (only [[Plex]] and [[Jellyfin]] may reference the metadata view).