# 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 # The names are the image's own: the server reads PORT, CONFIG_PATH, METADATA_PATH # and ROUTER_BASE_PATH (measured against the digest below — `podman inspect` shows # PORT=80 CONFIG_PATH=/config METADATA_PATH=/metadata in the image ENV, and the # server's source greps for exactly these). The ABS_* family printed by the server # README is the npm package's naming and is INERT here: with ABS_PORT=8000 the # server listened on :80 under base path /audiobookshelf, so the unit sat # `active (running)` while every probe to :8000 was refused (measured 2026-11-02, # first deploy). ROUTER_BASE_PATH=/ is what makes the URL the plain host:port. Environment=PORT=8000 Environment=ROUTER_BASE_PATH=/ Environment=CONFIG_PATH=/config Environment=METADATA_PATH=/metadata 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