From d196fe5b2ceb7955ef64eb7415f84c15f7dda90f Mon Sep 17 00:00:00 2001 From: Goyban Date: Thu, 20 Aug 2026 16:10:38 +0200 Subject: [PATCH] added audiobookshelf --- README.md | 1 + audiobookshelf/.env.example | 29 ++++++++++++ audiobookshelf/README.md | 90 +++++++++++++++++++++++++++++++++++++ audiobookshelf/compose.yaml | 23 ++++++++++ 4 files changed, 143 insertions(+) create mode 100644 audiobookshelf/.env.example create mode 100644 audiobookshelf/README.md create mode 100644 audiobookshelf/compose.yaml diff --git a/README.md b/README.md index 6910050..c0fbda8 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ handled separately — see [Exposing services](#exposing-services). | Service | Port | Notes | |---------|------|-------| +| [audiobookshelf](audiobookshelf/) | 13378 | Audiobooks and podcasts, with per-user progress sync | | [cloudflare-tunnel](cloudflare-tunnel/) | — | Outbound connector. Exposes other services without opening a port | | [jellyfin](jellyfin/) | 8096 | Media streaming. Also 7359/udp for auto-discovery | | [openwebui](openwebui/) | 3033 | LLM chat UI, talks to an external model backend | diff --git a/audiobookshelf/.env.example b/audiobookshelf/.env.example new file mode 100644 index 0000000..3fbd7aa --- /dev/null +++ b/audiobookshelf/.env.example @@ -0,0 +1,29 @@ +# Copy to .env and adjust. .env is git-ignored; this file is not. + +# ─── Identity ─────────────────────────────────────────────────────── +# Defaulted to 1000/1000 in compose.yaml. Unlike the LinuxServer images in +# this repo, these are NOT passed to the container as PUID/PGID -- that image +# ignores them. They feed Docker's `user:` directive, which actually changes +# the uid the process runs as. Must own CONFIG_PATH, METADATA_PATH and be able +# to read the media. +#PUID=1000 +#PGID=1000 + +#TZ=Europe/Paris + +# ─── Paths ────────────────────────────────────────────────────────── +# Audiobookshelf's database and settings. Small; defaults to ./config. +#CONFIG_PATH=./config + +# Cover art, cached metadata, downloads. Grows with your library. +#METADATA_PATH=./metadata + +# REQUIRED. The parent folder holding Audiobooks/ and Podcasts/. +# compose.yaml mounts ${MEDIA_PATH}/Audiobooks and ${MEDIA_PATH}/Podcasts. +MEDIA_PATH=/srv/media + +# ─── Ports ────────────────────────────────────────────────────────── +# Host port only. Do NOT add PORT to the container's environment -- the image +# reads PORT to decide what it listens on internally, which would break the +# 13378:80 mapping. +#PORT=13378 diff --git a/audiobookshelf/README.md b/audiobookshelf/README.md new file mode 100644 index 0000000..0e6f661 --- /dev/null +++ b/audiobookshelf/README.md @@ -0,0 +1,90 @@ +# Audiobookshelf + +Audiobook and podcast server. Tracks progress per user across devices, which is +the thing a plain file share can't do — pick up on your phone where the car +left off. + +## Quick start + +```bash +cp .env.example .env +$EDITOR .env # MEDIA_PATH is required +docker compose up -d +``` + +Open `http://:13378` and create the admin account. Point the Audiobooks +library at `/audiobooks` and Podcasts at `/podcasts` — those are the paths +*inside* the container. + +## Ports + +| Port | Proto | Purpose | +|------|-------|---------| +| 13378 | http | Web UI and API (container listens on 80) | + +## Why it looks like this + +**The official image, not LinuxServer.** Audiobookshelf ships its own and it's +the one upstream actually tests. That's a different call from jellyfin, where I +stay on LinuxServer out of long habit — here there's no reason to add a layer. + +**`user:` instead of `PUID`/`PGID`.** This is the part worth knowing, because +copying the pattern from the other services in this repo produces something +that *looks* right and silently isn't. The image's config: + +``` +User : '' <- empty, so root +Entrypoint: ['tini', '--'] +Cmd : ['node', 'index.js'] +``` + +There's no s6 layer and no init script — `tini` execs straight into Node. A +`PUID` environment variable is accepted and then read by nobody. Everything +gets written as root, and you find out when you try to move your library from +the host and can't. + +Docker's own `user:` directive is the mechanism that actually works here, so +`PUID`/`PGID` in `.env` feed that instead. Same names as the rest of the repo, +different plumbing underneath. + +**`/config` and `/metadata` are separate mounts** because they grow very +differently. `/config` is the SQLite database and settings — small, and the +thing to back up. `/metadata` is cover art, cached data and downloads, and +tracks the size of your library. Splitting them means a backup job can take the +first and skip the second. + +**Two media mounts, not one.** Audiobookshelf wants each library rooted +separately, so `${MEDIA_PATH}` points at the parent and the compose file mounts +`Audiobooks/` and `Podcasts/` beneath it. Only `MEDIA_PATH` goes in `.env`. + +## Gotchas + +- **`PORT` is a container variable here.** The image reads `PORT` to decide + what it listens on internally. It's used in `compose.yaml` for the *host* + side of `13378:80` only — if you ever add it to the `environment:` block, + the container moves off port 80 and the mapping breaks with nothing in the + logs to explain it. +- **Switching to `user:` on an existing install needs a chown.** If you ran + this as root first, the files it created are root-owned and the container + now can't write them. `sudo chown -R 1000:1000 config metadata` once. +- **Back up `/config`.** It holds the database — accounts, libraries, and + everyone's listening progress. `/metadata` regenerates; `/config` doesn't. +- **Media can be mounted `:ro`** unless you turn on storing metadata alongside + the media files, which is off by default. Worth doing if you want the library + protected from the server. + +## Exposing it + +Publishes a plain HTTP port; nothing here is proxy-aware. + +- [Reverse proxy with Caddy](../docs/reverse-proxy.md) +- [Cloudflare Tunnel](../cloudflare-tunnel/) — audio is far lighter than video, + but it's still media served through the proxy; read + [the limits](../docs/cloudflare-tunnel.md) before pointing a tunnel at it. +- [Single sign-on with Authentik](../docs/authentik-sso.md) — as with Jellyfin, + the mobile apps won't understand a forward-auth login page. + +## Links + +- Upstream docs: +- Image: diff --git a/audiobookshelf/compose.yaml b/audiobookshelf/compose.yaml new file mode 100644 index 0000000..49cdabd --- /dev/null +++ b/audiobookshelf/compose.yaml @@ -0,0 +1,23 @@ +services: + audiobookshelf: + image: ghcr.io/advplyr/audiobookshelf:latest + container_name: audiobookshelf + restart: unless-stopped + + # NOT a LinuxServer image: no s6, no init script -- tini execs straight + # into node, as root. PUID/PGID would be silently ignored, so ownership is + # set with Docker's own `user:` instead. See the README. + user: "${PUID:-1000}:${PGID:-1000}" + + environment: + - TZ=${TZ:-Etc/UTC} + + volumes: + - ${CONFIG_PATH:-./config}:/config + - ${METADATA_PATH:-./metadata}:/metadata + - "${MEDIA_PATH:?set MEDIA_PATH in .env to the parent of your Audiobooks/Podcasts folders}/Audiobooks:/audiobooks" + - ${MEDIA_PATH}/Podcasts:/podcasts + + ports: + # 13378 is the conventional host port; the container always listens on 80. + - "${PORT:-13378}:80"