Files
serverAppConfigs/dankserver/audiobookshelf/quadlet/audiobookshelf.container
Frank cfdd3435bc fix(audiobookshelf): mount the app at the base path its SPA was built for
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.
2026-11-02 19:50:44 -08:00

76 lines
3.9 KiB
INI

# 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 MUST be /audiobookshelf: the SPA in /app/client/dist/index.html is
# built with `routerBasePath:"/audiobookshelf"` baked in, so it asks for its assets under
# that prefix no matter what the server is mounted at. With `/` the server answers the
# page but every script request falls through to the SPA fallback and comes back as
# text/html — Chrome refuses to execute it and the UI never leaves "Loading…"
# (measured 2026-11-03; the API and /healthcheck were fine, which is why an API-only
# verification missed it).
Environment=PORT=8000
Environment=ROUTER_BASE_PATH=/audiobookshelf
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.
# /healthcheck answers `OK` at the root AND under the base path (measured on both
# base paths), so the probe below is correct whatever ROUTER_BASE_PATH says.
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