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
cp .env.example .env
$EDITOR .env # MEDIA_PATH is required
docker compose up -d
Open http://<host>: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
PORTis a container variable here. The image readsPORTto decide what it listens on internally. It's used incompose.yamlfor the host side of13378:80only — if you ever add it to theenvironment: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 metadataonce. - Back up
/config. It holds the database — accounts, libraries, and everyone's listening progress./metadataregenerates;/configdoesn't. - Media can be mounted
:rounless 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
- Cloudflare Tunnel — audio is far lighter than video, but it's still media served through the proxy; read the limits before pointing a tunnel at it.
- Single sign-on with Authentik — as with Jellyfin, the mobile apps won't understand a forward-auth login page.