
A three-VM audiobook + podcast library: Audiobookshelf + nginx + observability, from one prompt
June 2, 2026
Audiobookshelf is the self-hosted audiobook + podcast server that does what Audible and Spotify Podcasts do, without the algorithm and without the lock-in. It rounds out the top 10 most-deployed apps in the 2026 r/selfhosted survey.
This post walks through the Audiobookshelf stack on OpenFactory: three buildable VMs — Audiobookshelf, an nginx reverse proxy with WebSocket-ready upgrade headers for streaming, and a Prometheus observability node — from one prompt, shipped as bootable ISOs.
audiobookshelf (10.79.0.10:13378) — the library server with /var/lib/audiobookshelf/{config,metadata,audiobooks,podcasts} pre-wired.nginx-proxy (10.79.0.20:80) — reverse proxy with Connection: upgrade headers so client streaming and live progress sync work through it.observability (10.79.0.30:9090/3000) — Prometheus + Grafana scrape targets ready for the live stack.Three Debian Trixie VMs on 10.79.0.0/24. nginx fronts the library; observability scrapes both.
Paste this verbatim into the chat builder at console.openfactory.tech. Nothing above or below it — the builder expects the prompt body to start at the “Build a compact multi-node lab…” line.
Build a compact multi-node lab named `audiobookshelf-library`.
Output discipline: keep the plan small. Use one startup script per node, about 25 shell lines or less. Do not install the real Audiobookshelf Node.js binary, FFmpeg, or external `apt` repos at build time. Write deployment-time config examples and tiny Python stdlib or shell compatibility stubs only. The goal is a buildable preparation lab, not a production deployment.
## Topology
Create 3 buildable `debian-trixie` nodes, all `x86_64`, SSH enabled, DHCP/default route intact with lab aliases, firewall disabled, DNS `1.1.1.1` and `8.8.8.8`, user `ops` password `abs-ops` in `sudo`. Every recipe must set top-level `test_config` to `{ "enabled": false, "tests": [] }`.
- `audiobookshelf`: role `audio-library`, 2 GB RAM, 24 GB disk, alias `10.79.0.10/24`, x `230`, y `60`
- `nginx-proxy`: role `reverse-proxy`, 1 GB RAM, 8 GB disk, alias `10.79.0.20/24`, x `110`, y `220`
- `observability`: role `observability`, 2 GB RAM, 16 GB disk, alias `10.79.0.30/24`, x `350`, y `220`
Connections: `nginx-proxy` to `audiobookshelf:13378`; `observability` to all nodes.
## Common Recipe Requirements
All nodes: features `headless`, `ssh`; packages `openssh-server`, `python3`, `curl`, `jq`, `iproute2`, `netcat-openbsd`, `ca-certificates`. Each startup script adds the alias with `IFACE=$(ip route show default | awk '{print $5; exit}')`, `ip link set "$IFACE" up || true`, and `ip addr add <alias> dev "$IFACE" || true`. If `os.startup_scripts[].after` is present, it must be the string `"network-online.target"`, not an array.
## Node Requirements
`audiobookshelf`: features `headless`, `ssh`. Write `/etc/audiobookshelf/abs.env` with `PORT=13378`, `CONFIG_PATH=/var/lib/audiobookshelf/config`, `METADATA_PATH=/var/lib/audiobookshelf/metadata`, `AUDIOBOOKS_PATH=/var/lib/audiobookshelf/audiobooks`, `PODCASTS_PATH=/var/lib/audiobookshelf/podcasts`. Create `/var/lib/audiobookshelf/{config,metadata,audiobooks,podcasts}` mode `0750 ops:ops`. Add a Python stdlib HTTP service on `0.0.0.0:13378` exposing:
- `GET /healthcheck` -> `200 {"status":"ok","node":"audiobookshelf"}`
- `GET /status` -> `200 {"app":"audiobookshelf","serverVersion":"compat-1.0","isInit":true,"language":"en-us"}`
- `GET /ping` -> `200 {"success":true}`
- `GET /metrics` -> `audiobookshelf_compat_up 1`
Register `audiobookshelf-compat.service`.
`nginx-proxy`: features `headless`, `ssh`, `nginx`; packages `nginx`. Write `/etc/nginx/sites-enabled/abs.conf` proxying `:80` to `http://10.79.0.10:13378` with WebSocket upgrade headers (`proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";`) for streaming playback. Restart nginx.
`observability`: features `headless`, `ssh`, `prometheus`, `monitoring`; packages `python3`. Write `/etc/prometheus/prometheus.yml` scraping `10.79.0.10:13378` and `10.79.0.20:80`. Add tiny Python listeners for `:9090/-/healthy` and `:3000/api/health`.
## Scenario
Emit exactly one group scenario named `audiobookshelf-library-validation`. Put `custom_tests[].assertions[]` inside the scenario entry; leave `scenarios[].tests` empty. Every assertion needs `on_vm`. Use only `port_listening`, `command_output`, and `http_responds`; do not emit `vm_boots`, `network_reachable`, or `service_running`.
- `Stack ports listen`: `port_listening` for `audiobookshelf:13378`, `nginx-proxy:80`, `observability:9090`, `observability:3000`.
- `Audiobookshelf healthcheck`: on `audiobookshelf`, `curl -fsS http://localhost:13378/healthcheck | jq -e '.status == "ok"' >/dev/null && echo abs-ok`.
- `Audiobookshelf status`: on `audiobookshelf`, `curl -fsS http://localhost:13378/status | jq -e '.app == "audiobookshelf" and .isInit == true' >/dev/null && echo status-ok`.
- `Proxy reaches abs`: on `nginx-proxy`, `nc -z -w 5 10.79.0.10 13378 && echo upstream-reachable`.
- `Observability`: on `observability`, grep both scrape targets from `/etc/prometheus/prometheus.yml` and `http_responds` for `http://localhost:9090/-/healthy` and `http://localhost:3000/api/health`.
Preserve warnings that real Audiobookshelf Node.js binary install, audio transcoding for streaming, mobile-app push notifications, library scan scheduling, Audible/Libby/Plex integration, off-host backups of `/var/lib/audiobookshelf`, real TLS termination, and `10.79.0.0/24` aliasing are deployment-time concerns.Driving OpenFactory from an AI agent instead of the browser? The same flow is exposed through the OpenFactory MCP server — submit the prompt programmatically, get the build-plan preview back, and call create_build / start_vm on the resulting recipes. Single-image builds go straight through the openfactory CLI.
The prompt produces a buildable preparation lab — the right topology, the right ports listening, deployment-time config templates dropped in the right places, and tiny compatibility services that prove the wiring works. A few things still sit outside the recipe and need operator attention before this carries real load:
/var/lib/audiobookshelf/config holds users, listening history, and library metadata.Audiobooks plus the rest of the media stack: see the Jellyfin media stack post for video. For the *arr suite that keeps libraries filled automatically, see the *arr media automation post. And the Enterprise & GxP page covers fleet rollouts.
OpenFactory's free flow is for browsing. Persistent VMs, SSH access, snapshots, your own ISO, and fleet deployment live on a paid plan.