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:
1 parent
29aefc3b6f
commit
511d8dca93
6 files changed
+133
-1
No files matched your search
@@ -0,0 +1,59 @@
|
||||
# Quadlet unit: Audiobookshelf audiobook server (rootless, boot-persistent).
|
||||
#
|
||||
# Install path on dankserver: ~/.config/containers/systemd/audiobookshelf.container
|
||||
# Unit name: audiobookshelf.service (podman 5.x drops the container- prefix)
|
||||
#
|
||||
# Everything this server writes is its own state — the SQLite library, accounts,
|
||||
# playback progress, cached covers — and it lives under
|
||||
# /home/dank/Containers/audiobookshelf on the NVMe, so no metadata byte ever
|
||||
# lands on the spindle. The audio itself is bound READ-ONLY off the array: the
|
||||
# only sanctioned write path into /mnt/entropy is the overlay view
|
||||
# (scripts/media-view.sh), Audiobookshelf has no need of it, and a read-only bind
|
||||
# makes a stray sidecar write structurally impossible rather than discouraged.
|
||||
#
|
||||
# The digest is the linux/amd64 manifest of the tag, which is what podman
|
||||
# verifies and what RepoDigests records on this single-arch host. Pinning the
|
||||
# manifest-list digest instead would let a re-pull resolve to a different image.
|
||||
# The upstream version tags stop at 2.19.0 (built 2025-02-02) while `latest` is
|
||||
# newer by a year and a half, so `latest@digest` is the only pin that names the
|
||||
# build actually running — same reasoning as the jellyfin and romm units.
|
||||
|
||||
[Unit]
|
||||
Description=Audiobookshelf audiobook server container
|
||||
After=network.target
|
||||
|
||||
[Container]
|
||||
ContainerName=audiobookshelf
|
||||
Image=ghcr.io/advplyr/audiobookshelf:latest@sha256:37c2057e150694970442579815c646fe6bdc01c95b9d1547c4c9068be8844ef3
|
||||
Network=pasta
|
||||
Environment=ABS_CONFIG_DIR=/config
|
||||
Environment=ABS_METADATA_DIR=/metadata
|
||||
Environment=ABS_PGID=1000
|
||||
Environment=ABS_PORT=8000
|
||||
Environment=ABS_PUID=1000
|
||||
Environment=TZ=America/Vancouver
|
||||
PublishPort=8000:8000
|
||||
Volume=/home/dank/Containers/audiobookshelf/config:/config
|
||||
Volume=/home/dank/Containers/audiobookshelf/metadata:/metadata
|
||||
Volume=/mnt/entropy/Audiobooks:/audiobooks:ro
|
||||
# The image is Alpine: it ships wget and no curl, so the health check must name
|
||||
# the binary that exists or the container is unhealthy while serving perfectly.
|
||||
HealthCmd=wget -q -O /dev/null http://127.0.0.1:8000/healthcheck
|
||||
HealthInterval=30s
|
||||
HealthTimeout=10s
|
||||
HealthRetries=3
|
||||
|
||||
[Service]
|
||||
# Refuse to start until the array is a real mount — with the bind source missing,
|
||||
# podman invents an empty directory and Audiobookshelf records an empty library as
|
||||
# truth. See the header of scripts/wait-for-mount.sh for the blind-boot incident.
|
||||
ExecStartPre=/home/dank/serverAppConfigs/scripts/wait-for-mount.sh /mnt/entropy
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
# The first boot scans the whole audiobook tree. Weighted LOW (default 100) so a
|
||||
# scan loses to playback when the array is contended. Proportional share, not a
|
||||
# cap: inert while the disk is idle.
|
||||
IOWeight=10
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
Reference in new issue
Block a user