450 lines
22 KiB
Markdown
450 lines
22 KiB
Markdown
# Audiobookshelf (rootless Podman + Quadlet)
|
|
|
|
Runs [Audiobookshelf](https://www.audiobookshelf.org/) on `emily` as a
|
|
**rootless Podman** container managed by a **systemd user unit** (Quadlet),
|
|
behind the shared [rootless Caddy](../caddy/readme.md). Replaces the plain
|
|
`docker run` container on `monoco`:
|
|
|
|
```command
|
|
docker run -d \
|
|
--volume /var/lib/audiobookshelf/config:/config \
|
|
--volume /var/lib/audiobookshelf/metadata:/metadata \
|
|
--volume /var/lib/audiobookshelf/audiobooks:/audiobooks \
|
|
--volume /var/lib/audiobookshelf/podcasts:/podcasts \
|
|
--name=audiobookshelf \
|
|
--env=TZ=Asia/Kolkata \
|
|
--restart=unless-stopped \
|
|
ghcr.io/advplyr/audiobookshelf:latest
|
|
```
|
|
|
|
Unlike [bitwarden/](../bitwarden/readme.md) and [tandoor/](../tandoor/readme.md),
|
|
**there is no database to attach.** Audiobookshelf's state is one SQLite file
|
|
plus files under `~/podman-data/audiobookshelf`, so this play has no `postgres`
|
|
role and nothing to restore from the shared cluster.
|
|
|
|
Two facts about the published image shape everything below, and both were
|
|
verified against the real image rather than read off the docs:
|
|
|
|
| Finding | Consequence |
|
|
|---|---|
|
|
| The image declares **no `VOLUME`** | The four `Volume=` lines in the unit are load-bearing, not decorative. Without them the app runs perfectly — and loses everything on the next recreate |
|
|
| The image ships **no `HEALTHCHECK`** | This play defines one (`HealthCmd=`) so it can gate on the app actually answering |
|
|
|
|
## Ordering
|
|
|
|
Run [caddy/playbook.yml](../caddy/playbook.yml) first. It is the reverse proxy,
|
|
owns the Caddyfile this site appends to, supplies the `caddy.service` handlers,
|
|
and is the one play that applies the rootless-Podman prerequisites (installing
|
|
podman, `loginctl enable-linger`, the low-port sysctl, the Quadlet directory).
|
|
|
|
There is no second prerequisite to wait for — no cluster to bring across, since
|
|
this app has no database.
|
|
|
|
This playbook does **not** move the library. Copy the data first (see
|
|
["Migrating the data"](#migrating-the-data-from-monoco)) so the first start finds
|
|
the real `config/`, rather than creating a fresh empty database next to it.
|
|
|
|
## Layout
|
|
|
|
```
|
|
audiobookshelf/
|
|
├── playbook.yml
|
|
├── vars/default.yml
|
|
└── roles/
|
|
├── audiobookshelf/ # the four data dirs, the Quadlet unit, start + verify
|
|
└── caddy/ # appends this site's block to the shared Caddyfile
|
|
```
|
|
|
|
Quadlet units and state:
|
|
|
|
| On the host | What |
|
|
|---|---|
|
|
| `~/.config/containers/systemd/audiobookshelf.container` | the generated unit (Ansible-templated) |
|
|
| `~/.config/containers/systemd/audiobookshelf.network` | the app network, written by `caddy/roles/register` |
|
|
| `~/.config/containers/systemd/caddy.container.d/50-audiobookshelf.conf` | attaches Caddy to that network |
|
|
| `~/podman-data/audiobookshelf/config/` | `absdatabase.sqlite`, `migrations/` |
|
|
| `~/podman-data/audiobookshelf/metadata/` | `backups/`, `cache/`, `logs/`, `streams/`, cover art, item metadata |
|
|
| `~/podman-data/audiobookshelf/audiobooks/` | the book library (15 GB) |
|
|
| `~/podman-data/audiobookshelf/podcasts/` | downloaded podcast episodes |
|
|
|
|
## What is state, and what is not
|
|
|
|
Per [docs/backup-plan.md §2.1](../docs/backup-plan.md):
|
|
|
|
| Path | Keep? | Why |
|
|
|---|---|---|
|
|
| `config/` | **required** | `absdatabase.sqlite` — users, libraries, listening progress, bookmarks. 2.6 MB, irreplaceable |
|
|
| `metadata/` | **required** | cover art, item metadata, and `backups/`. 19 MB, irreplaceable |
|
|
| `audiobooks/` | media | 15 GB across 696 files. Re-downloadable, and [proposed for exclusion](../docs/backup-plan.md) from the backup ([§8](../docs/backup-plan.md)) |
|
|
| `podcasts/` | media | episodes the app downloads here. Unlike `audiobooks/`, this directory is **written to at runtime** |
|
|
|
|
So the real state is **22 MB, not 15 GB**. Treating the library as state is what
|
|
made a "full backup" of this service look impossible
|
|
([docs/backup-plan.md §2](../docs/backup-plan.md)).
|
|
|
|
`metadata/cache/`, `metadata/logs/` and `metadata/streams/` are regenerable —
|
|
transcode scratch space, the log, and the image cache. They live under
|
|
`metadata/` because that is where the app puts them, and they are small; the
|
|
migration copies them anyway rather than carving out exceptions.
|
|
|
|
**A live `absdatabase.sqlite` must not be `tar`-ed.** It is SQLite in WAL mode,
|
|
so the database file and its `-wal` sidecar are copied at different instants and
|
|
the result can be torn — a backup that will not open
|
|
([docs/backup-plan.md §3.1](../docs/backup-plan.md)). Use the app's own
|
|
**Settings → Backups** (which writes into `BACKUP_PATH`, defaulting to
|
|
`metadata/backups`), or stop the container and copy the directory.
|
|
|
|
## How it registers with Caddy
|
|
|
|
Caddy is the base unit and knows nothing about its upstreams. This playbook calls
|
|
the shared include in
|
|
[caddy/roles/register](../caddy/roles/register/tasks/main.yaml), which writes the
|
|
network unit **and** the drop-in that pulls it into Caddy:
|
|
|
|
```yaml
|
|
- name: Register Audiobookshelf's network with Caddy
|
|
ansible.builtin.include_tasks: "{{ playbook_dir }}/../caddy/roles/register/tasks/main.yaml"
|
|
```
|
|
|
|
It runs in `pre_tasks`, **before** the app unit is started, because
|
|
`audiobookshelf.container` references `audiobookshelf.network`: Quadlet fails
|
|
*unit generation* — so `systemctl --user start` reports "unit not found" — when a
|
|
referenced `.network` file does not exist yet.
|
|
|
|
`network_name` is `audiobookshelf`, so the *file* is `audiobookshelf.network` and
|
|
the *Podman network* is `audiobookshelf_net` (`NetworkName=` in the network unit
|
|
pins it, matching the name the migration plan already assumed).
|
|
|
|
[roles/caddy](roles/caddy/tasks/main.yaml) appends the site block to the shared
|
|
Caddyfile with `blockinfile`, exactly as the other services do. Caddy handles the
|
|
WebSocket upgrade the app's live-socket features need, and imposes no
|
|
request-body limit, so there is nothing like the old nginx `client_max_body_size`
|
|
to configure.
|
|
|
|
**Nothing is published on the host.** The container listens on 80 and Caddy
|
|
reaches it as `audiobookshelf:80` over `audiobookshelf_net`, so this play needs
|
|
none of the port-floor work Caddy itself needs.
|
|
|
|
## Settings
|
|
|
|
| Variable | Value / purpose |
|
|
|---|---|
|
|
| `podman_user` | **Derived** from the connection (`ansible_facts['user_id']`), not set by hand |
|
|
| `podman_data_dir` | `~/podman-data` |
|
|
| `quadlet_dir` | `~/.config/containers/systemd` |
|
|
| `host` | `audiobooks.tanshu.com` — the site block Caddy serves |
|
|
| `container_name` | `audiobookshelf`, and the Quadlet unit / container name |
|
|
| `image` | `ghcr.io/advplyr/audiobookshelf:latest` — already fully qualified |
|
|
| `network_name` | `audiobookshelf` → `audiobookshelf.network` / `audiobookshelf_net` |
|
|
| `config_dir` / `metadata_dir` | `~/podman-data/audiobookshelf/{config,metadata}` → `/config`, `/metadata` |
|
|
| `audiobooks_dir` / `podcasts_dir` | `~/podman-data/audiobookshelf/{audiobooks,podcasts}` → `/audiobooks`, `/podcasts` |
|
|
| `container_port` | `80` — the image's `EXPOSE 80` / `ENV PORT=80` |
|
|
| `timezone` | `Asia/Kolkata`, the only setting the Docker-era container passed |
|
|
| `health_cmd` | The `HealthCmd=` the unit defines, since the image has none |
|
|
| `caddy_name` / `caddyfile_path` | The shared Caddy unit and its Caddyfile |
|
|
|
|
The three `podman_*` / `quadlet_dir` vars are **duplicated** from
|
|
[caddy/vars/default.yml](../caddy/vars/default.yml), as in every service
|
|
playbook. That is a deliberate trade: a play cannot include the Caddy vars file
|
|
wholesale, because it also sets `host_directory` (to the *Caddy* data directory)
|
|
and every service defines that for itself. Keep them in sync. Caddy's fourth,
|
|
`podman_unprivileged_port_start`, belongs to the podman tasks this play does not
|
|
run.
|
|
|
|
Like Caddy, `podman_user` is derived from the connection and the play carries no
|
|
`become`. Because the podman tasks are not run here, there is **no guard against
|
|
connecting as `root`**; the prerequisites, including that check, live in
|
|
`caddy/playbook.yml`.
|
|
|
|
`UserNS=keep-id:uid=0,gid=0` — the image has no `USER` instruction, so it runs as
|
|
container-root throughout and writes `/config` and `/metadata` as root
|
|
([docs/podman-compat-audit.md §3.2](../docs/podman-compat-audit.md)). Mapping
|
|
container root onto `podman_user` keeps `~/podman-data/audiobookshelf` owned by
|
|
`tanshu` and leaves no subuid-owned files to trip over backups. Upstream
|
|
explicitly does not support `PUID`/`PGID`, so this is the mechanism for the
|
|
ownership rather than an env var.
|
|
|
|
**There is no environment file here**, unlike `bitwarden/` and `tandoor/`. This
|
|
deployment holds no secrets: Audiobookshelf generates its own JWT signing key
|
|
inside `config/`, and `TZ` is not sensitive — so it goes on the unit as a plain
|
|
`Environment=` line, and the unit stays world-readable without exposing anything.
|
|
|
|
## Running
|
|
|
|
```command
|
|
ansible-playbook -u tanshu audiobookshelf/playbook.yml
|
|
```
|
|
|
|
The play targets `emily` directly, so the inventory must contain it — the one in
|
|
`prepare-server/` does, and its `ansible.cfg` names it.
|
|
|
|
This play applies **only** this app's units and data. Its `pre_tasks` run Caddy's
|
|
`register` include and nothing else. The rootless-Podman prerequisites are the
|
|
shared task file at
|
|
[caddy/roles/podman/tasks/main.yaml](../caddy/roles/podman/tasks/main.yaml) and
|
|
are **not** run here. A host that has already run `caddy/playbook.yml` has them.
|
|
|
|
`systemctl --user enable audiobookshelf` is neither needed nor possible: the unit
|
|
is *generated*, and Quadlet applies `[Install] WantedBy=default.target` itself.
|
|
|
|
## Migrating the data from monoco
|
|
|
|
Everything lives in `/var/lib/audiobookshelf` today, owned by **uid 0** (the
|
|
image runs as root). It moves to `~/podman-data/audiobookshelf`, and the whole
|
|
tree is ~15 GB — budget real time for the bulk copy
|
|
([docs/podman-migration.md §4.3](../docs/podman-migration.md)).
|
|
|
|
**Run all of this from the control node that holds both SSH aliases** — the
|
|
machine where `ssh monoco` and `ssh emily` both work. The two hosts cannot reach
|
|
each other here: `emily` is a fresh instance that knows nothing about `monoco`,
|
|
and the `monoco` alias exists only in the control node's SSH config. `rsync`
|
|
also cannot copy remote-to-remote, so the 15 GB is staged on the control node in
|
|
between — check it has the room before starting.
|
|
|
|
### 1. Look before you copy
|
|
|
|
```bash
|
|
# Sizes, numeric owners (`ls -ln`), and whether the tree is readable without
|
|
# sudo. The old container ran as root, so the files are uid 0.
|
|
ssh monoco 'du -sh /var/lib/audiobookshelf/*'
|
|
ssh monoco 'ls -ln /var/lib/audiobookshelf'
|
|
ssh monoco 'find /var/lib/audiobookshelf -type f ! -readable' # empty = no sudo needed
|
|
|
|
# Space on the destination (it needs the full 15 GB) and on the control node
|
|
# (it holds a second copy until step 6 deletes it).
|
|
ssh emily 'df -h / | tail -1'
|
|
df -h /home | tail -1
|
|
```
|
|
|
|
If the `find` above prints anything, add `--rsync-path="sudo rsync"` to the
|
|
`monoco:` side of every `rsync` below so the transfer reads as root.
|
|
|
|
### 2. Create the destinations
|
|
|
|
```bash
|
|
ssh emily 'mkdir -p ~/podman-data/audiobookshelf/{config,metadata,audiobooks,podcasts}'
|
|
mkdir -p ~/audiobookshelf-migrate/{config,metadata,audiobooks,podcasts}
|
|
```
|
|
|
|
### 3. The 15 GB library, while the old container is still serving
|
|
|
|
Audiobookshelf only *reads* `/audiobooks`, so the slow part needs no downtime.
|
|
If a hop drops, run it again — `rsync -P` resumes at the file it was on.
|
|
|
|
```bash
|
|
# monoco -> control node
|
|
rsync -aP --no-owner --no-group \
|
|
monoco:/var/lib/audiobookshelf/audiobooks/ ~/audiobookshelf-migrate/audiobooks/
|
|
|
|
# control node -> emily
|
|
rsync -aP --no-owner --no-group \
|
|
~/audiobookshelf-migrate/audiobooks/ emily:~/podman-data/audiobookshelf/audiobooks/
|
|
```
|
|
|
|
A second run of either command that prints **no filenames** means that hop is
|
|
complete — cheaper and stronger than comparing counts, and it is why `rsync` is
|
|
used here rather than a single `tar` stream. Do **not** add `--delete`: if the
|
|
playbook has already run on emily, `--delete` would remove files the app created
|
|
there that are not on monoco.
|
|
|
|
### 4. The 22 MB of state, with the old container stopped
|
|
|
|
```bash
|
|
# Not optional for config/: absdatabase.sqlite is live SQLite in WAL mode, and a
|
|
# hot copy can be torn (docs/backup-plan.md §3.1). podcasts/ is written at
|
|
# runtime too, so it is not safe to copy while the app is up either.
|
|
ssh monoco 'docker stop audiobookshelf'
|
|
|
|
for d in config metadata podcasts; do
|
|
rsync -aP --no-owner --no-group "monoco:/var/lib/audiobookshelf/$d/" ~/audiobookshelf-migrate/"$d"/
|
|
rsync -aP --no-owner --no-group ~/audiobookshelf-migrate/"$d"/ "emily:~/podman-data/audiobookshelf/$d/"
|
|
done
|
|
|
|
# Final incremental pass over the library, in case a scan wrote metadata beside
|
|
# a book during step 3.
|
|
rsync -aP --no-owner --no-group monoco:/var/lib/audiobookshelf/audiobooks/ ~/audiobookshelf-migrate/audiobooks/
|
|
rsync -aP --no-owner --no-group ~/audiobookshelf-migrate/audiobooks/ emily:~/podman-data/audiobookshelf/audiobooks/
|
|
```
|
|
|
|
`--no-owner` / `--no-group` are explicit because `-a` implies the opposite, and
|
|
the source files are **root**-owned on monoco. The container runs as uid 0
|
|
in-container, which `keep-id` maps to `tanshu`, so files that arrive owned by
|
|
`tanshu` are already correct — and restoring root ownership would be wrong.
|
|
|
|
### 5. Verify, before the first start
|
|
|
|
Verify *before* deploying: once the playbook has run, emily's
|
|
`config/absdatabase.sqlite` has been migrated in place and no longer matches the
|
|
source.
|
|
|
|
```bash
|
|
# File count and total bytes per directory, both ends. audiobooks/ should be
|
|
# 696 files and 15.0 GB on monoco (docs/backup-plan.md §2).
|
|
for d in config metadata audiobooks podcasts; do
|
|
echo "== $d =="
|
|
ssh monoco "find /var/lib/audiobookshelf/$d -type f | wc -l; du -sb /var/lib/audiobookshelf/$d | cut -f1"
|
|
ssh emily "find ~/podman-data/audiobookshelf/$d -type f | wc -l; du -sb ~/podman-data/audiobookshelf/$d | cut -f1"
|
|
done
|
|
|
|
# Byte-level digests for the irreplaceable 22 MB. Hashing the 15 GB library costs
|
|
# more than copying it did, so it is checked by count and size above instead.
|
|
for d in config metadata; do
|
|
a=$(ssh monoco "cd /var/lib/audiobookshelf/$d && find . -type f -print0 | sort -z | xargs -0 -r sha256sum | sha256sum")
|
|
b=$(ssh emily "cd ~/podman-data/audiobookshelf/$d && find . -type f -print0 | sort -z | xargs -0 -r sha256sum | sha256sum")
|
|
[ -n "$a" ] && [ "$a" = "$b" ] && echo "$d OK ${a%% *}" || { echo "$d MISMATCH: $a vs $b"; false; }
|
|
done
|
|
|
|
# Nothing may be owned by root or by a subuid.
|
|
ssh emily 'find ~/podman-data/audiobookshelf ! -user tanshu' # empty = correct
|
|
```
|
|
|
|
### 6. Deploy, then clean up
|
|
|
|
```bash
|
|
# Run after the copy, so the first start finds the real database rather than
|
|
# creating an empty one beside it.
|
|
cd prepare-server && ansible-playbook -u tanshu ../audiobookshelf/playbook.yml
|
|
|
|
# Only after step 5 passed and the play reported a healthy container. The
|
|
# staging copy is ~15 GB; the library is on emily twice over until this runs.
|
|
rm -rf ~/audiobookshelf-migrate
|
|
```
|
|
|
|
Stop and leave the old container stopped. Its `config/` is now a stale snapshot,
|
|
and two live copies would diverge — rolling back means restoring monoco's
|
|
`config/` and starting it there, not starting it in place.
|
|
|
|
### Alternatives
|
|
|
|
Stream straight across with `tar` if the control node has no room for 15 GB. It
|
|
cannot resume: a drop at 90% starts over.
|
|
|
|
```bash
|
|
set -o pipefail
|
|
ssh monoco 'tar -C /var/lib/audiobookshelf/audiobooks -cf - .' \
|
|
| ssh emily 'tar --no-same-owner -C ~/podman-data/audiobookshelf/audiobooks -xf -'
|
|
```
|
|
|
|
`set -o pipefail` matters: without it the pipeline reports only the *receiving*
|
|
`tar`'s status, so a read error or a dropped stream on the source side exits 0
|
|
and leaves a silently short copy. The 22 MB of state is small enough that the
|
|
same pipe is fine for it.
|
|
|
|
Or stop the old container up front and copy all four directories in one pass.
|
|
Simpler, and provably consistent because nothing is writing; the cost is that the
|
|
service is down for the whole 15 GB instead of just the 22 MB.
|
|
|
|
```bash
|
|
ssh monoco 'docker stop audiobookshelf'
|
|
for d in config metadata audiobooks podcasts; do
|
|
rsync -aP --no-owner --no-group "monoco:/var/lib/audiobookshelf/$d/" ~/audiobookshelf-migrate/"$d"/
|
|
rsync -aP --no-owner --no-group ~/audiobookshelf-migrate/"$d"/ "emily:~/podman-data/audiobookshelf/$d/"
|
|
done
|
|
```
|
|
|
|
## Verification
|
|
|
|
```bash
|
|
systemctl --user status audiobookshelf
|
|
journalctl --user -u audiobookshelf -f
|
|
podman logs audiobookshelf
|
|
|
|
# What the playbook checks for you:
|
|
podman healthcheck run audiobookshelf # the HealthCmd= the unit defines
|
|
podman exec audiobookshelf ls -d /config /metadata /audiobooks /podcasts
|
|
test -e ~/podman-data/audiobookshelf/config/absdatabase.sqlite
|
|
|
|
# The health the unit reports on its own 30s timer, not just once:
|
|
podman inspect audiobookshelf --format '{{.State.Health.Status}}'
|
|
|
|
# The mounts are real bind mounts and not the container layer:
|
|
podman inspect audiobookshelf --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
|
|
|
|
# The site, end to end.
|
|
curl -sSI https://audiobooks.tanshu.com/healthcheck # 200 OK, body "OK"
|
|
```
|
|
|
|
Then open `https://audiobooks.tanshu.com` **in a browser** and confirm the
|
|
library is there and an existing book resumes at the right timestamp. A `curl`
|
|
cannot check that the migrated database is *readable*; only the app can.
|
|
|
|
On a *fresh* install the first visit asks you to create the admin account and add
|
|
a library — the playbook creates the directories, not the library entries.
|
|
|
|
Also exercise a **write** through the app — ask it to match/scan a book, or start
|
|
a podcast download — and confirm the files it creates under
|
|
`~/podman-data/audiobookshelf` are owned by `tanshu`. That is the check that
|
|
catches a wrong `keep-id`; a read-only smoke test will not.
|
|
|
|
Then reboot the host and confirm the unit returns unattended — without
|
|
`loginctl enable-linger tanshu` it will not, and the failure is silent.
|
|
|
|
## Notes and gotchas
|
|
|
|
- **The image declares no `VOLUME`.** This is the one silent failure worth
|
|
knowing: with a missing `Volume=` line the app starts, serves, and looks
|
|
healthy while writing into the container's writable layer — and loses it all on
|
|
the next recreate. The play guards against it by asserting that
|
|
`config/absdatabase.sqlite` exists on the *host* after the health check passes.
|
|
- **The image ships no `HEALTHCHECK`, so this unit defines one.** Upstream's docs
|
|
discourage health checks as "generally redundant to setting a restart policy"
|
|
and as log noise; that is fair, and the reason for the deviation is that
|
|
`is-active` cannot tell a serving server from a crash-looping one under
|
|
`Restart=always`. There is deliberately **no `HealthOnFailure=`** — that would
|
|
let a temporarily slow start kill and restart a healthy server. Add
|
|
`HealthOnFailure=kill` yourself if you would rather have self-healing.
|
|
- **`ROUTER_BASE_PATH` defaults to `/audiobookshelf`.** It is undocumented in the
|
|
env reference but real, and the server logs it on every start. Bare paths still
|
|
work — middleware rewrites non-prefixed URLs — which is why
|
|
`reverse_proxy audiobookshelf:80` at the site root is correct. It matters only
|
|
if you later serve the app under a subpath, where the prefix would appear twice.
|
|
- **`wget`, not `curl`.** `curl` exists only in the image's builder stage and is
|
|
absent at runtime; `wget` is present (BusyBox). That is what `health_cmd` uses.
|
|
- **`latest` floats.** `:latest` is the newest stable release; `:edge` tracks
|
|
every commit to master, and `vX.Y.Z` tags exist. Upstream makes no pinning
|
|
recommendation either way. The image measured for this playbook was **2.37.1**,
|
|
digest `sha256:581d68b2a6fc7ebf58d81c878a9f387cbbc0d88ac9d37b298b9cee10168af85b`
|
|
— pin that digest in `vars/default.yml` if you want reproducible deploys.
|
|
Upgrading is `podman pull` plus a restart; it may run SQLite migrations on
|
|
first start.
|
|
- **The 15 GB library is not backed up, and was never covered.** No backup script
|
|
exists for this service at all ([docs/backup-plan.md §13.6](../docs/backup-plan.md)),
|
|
and the media is a deliberate exclusion. Decide explicitly whether the
|
|
audiobooks exist anywhere else ([§8](../docs/backup-plan.md)) — this playbook
|
|
takes no position on it.
|
|
- **Registering restarts all of Caddy.** Writing the network unit and the drop-in
|
|
changes Caddy's generated unit, and Quadlet has no live equivalent of
|
|
`docker network connect`, so Caddy is recreated — a brief restart of *every*
|
|
hosted site, not just this one. See
|
|
[caddy/readme.md](../caddy/readme.md), "Trade-off worth knowing".
|
|
- **The scanner/watcher has no environment variable.** "Automatically watch
|
|
libraries for changes" is a UI setting (`scannerDisableWatcher`, needs a
|
|
restart), plus a per-library flag. It cannot be set from this playbook.
|
|
- **Client IPs.** Whether the original client address survives the rootless port
|
|
forward decides whether Audiobookshelf's own auth rate limiting
|
|
(`RATE_LIMIT_AUTH_*`) sees real addresses. It is unverified on emily — see
|
|
[caddy/readme.md](../caddy/readme.md), "Client IPs".
|
|
- **If the Caddyfile does not exist yet**, this playbook creates it containing
|
|
only the site block. That is a valid Caddyfile; the Caddy playbook's seeded
|
|
global-options block is then not added, because its seed only fires on a
|
|
missing or zero-length file. Deploy Caddy first on a fresh host.
|
|
- **DNS.** `audiobooks.tanshu.com` must resolve to `emily` before Caddy can
|
|
obtain a certificate for it.
|
|
- **Root's crontab cannot `podman exec`** into another user's rootless
|
|
containers, so any scheduled `Settings → Backups` orchestration or library
|
|
scan belongs in a **systemd user timer** next to the Quadlet units.
|
|
- **Do not restart the container on monoco.** Its `config/` is a stale snapshot
|
|
of a database emily has moved past.
|
|
|
|
## Not done here
|
|
|
|
- Pinning the image by digest or version tag, or wiring up
|
|
`AutoUpdate=registry` with `podman auto-update`.
|
|
- Backing up the 15 GB library, or deciding whether it belongs in scope at all.
|
|
- Creating a backup script or systemd user timer for the 22 MB of real state —
|
|
neither exists today, and the app's own **Settings → Backups** is the
|
|
preferred route rather than anything Ansible-side.
|
|
- Creating the admin account and library entries, which is a first-run UI step.
|
|
- `HealthOnFailure=kill`, and any external monitoring or alerting on the
|
|
`unhealthy` state.
|