Can I self-host Audible?

YES · ONE COMMAND— setup effort 1 of 4

YES — it's called Audiobookshelf. It takes one prompt, a 1024 MB VPS, and about 10 minutes. That is $14.95 a month you stop paying Audible — $179.40 a year on the Premium Plus plan.

Why people pay for Audible

Stated as the vendor would want it stated. A replacement you pick without knowing what the subscription actually buys is a replacement you abandon in a fortnight.

The catalog is the product, and so is the studio time behind it. Audible pays for the narrators, the exclusive rights and the Originals nobody else can license, so a book published this morning is one tap away, already produced, on a phone, a car and a watch, with the place you stopped kept in all three. A credit each month buys a title you keep after you cancel, and Whispersync moves you between the Kindle text and the audio without losing the page. None of that is software you can install: it is licensing and production, and it is what the subscription actually buys. What it also buys, and what you can take back, is the player, the library and the progress bar.

Audible plans and list prices
PlanList priceWhat it buys
Audible Plus$7.95/moUnlimited listening from the Plus Catalog. No credits, so premium titles are not included.
Premium Plusthe plan this page prices against$14.95/moThe Plus Catalog plus one credit a month for any premium title, and that title stays in your library after you cancel.
Premium Plus 2 Credits$22.95/moTwo credits a month instead of one. This is the least certain figure in this ladder: it was corroborated from secondary pricing coverage rather than read on a page Audible served us.
Premium Plus Annual$12.46/mo$149.50 billed once a year, with twelve credits granted up front rather than one a month. The figure recorded here is that annual price divided by twelve, so it can be compared with the monthly plans.

Vendor list prices in USD, read from the pricing page on 2026-08-06 · confidence: medium

Replaced by Audiobookshelf

One project, named before the prompt, so you know what you are about to install.

An audiobook and podcast server for the files you already own, with first-party phone apps that keep your place.

The honest answer to a narrower question. It does not replace the catalog and nothing self-hosted does; it replaces the player, the library and the progress bar. Point it at audiobooks you already hold in an ordinary format and one container gives you a web player, per-user listening positions, series and author browsing, and a podcast library beside it. The reason it is first is the client story: upstream ships its own phone apps, Android on Google Play and iOS as a TestFlight beta, and those keep your place across devices the way the paid app does. A listening server your phone cannot reach is a folder with extra steps.

The swap

You're paying

Audible

$14.95/mo · $179.40/yr

is replaced by

You'd run

Audiobookshelf

ONE COMMAND · ~10 min to running · 1024 MB RAM

Audible Premium Plus · vendor list price · checked 2026-08-06 · source · confidence: medium

Before you start

RAM floor
1024 MBfloor from upstream docs — not measured by us yet
Disk
5 GBthe app, its data, and room for one backup
Domain needed
yes, one A recorda hostname pointed at the box before you start — TLS needs it on the cloud path, and the local path needs none
Time budget
~10 minunder 10 minutes, through the first backup

The prompt

Two paths to the same Audiobookshelf: the cloud one assumes Prompt Zero is done on a server you rent, the local one assumes nothing but a computer that can run Docker Desktop. Read whichever you pick before you paste it, which is the whole reason both are on the page instead of behind a download.

authored from upstream docs · not yet machine-verified · Claude Code

Where it runs

313 lines · 14,995 bytes

What this prompt will do
  1. Preflight
  2. Layout
  3. Secrets
  4. compose.yml
  5. Caddy and TLS
  6. Firewall
  7. Start and verify
  8. First backup and restore
  9. Updating later
  10. What will probably go wrong
  11. Out of scope

Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.

paste it into Claude Code in a terminal on your own machine · it runs the install over ssh vps

You are Claude Code on the user's machine. The user has completed Prompt Zero: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny.

Run every command in this prompt on the server over `ssh vps` unless the step says otherwise.

Install Audiobookshelf 2.36.0 on that server, reachable at https://<DOMAIN>, behind the existing
Caddy with automatic TLS.

## 1. Preflight

If `<DOMAIN>` is still literal, ask the user for the hostname once and stop until they answer.
Its A record must already point at this server.

Tell the user one thing before anything installs, because it decides whether they want this:
Audiobookshelf plays audio files that are already on this server. There is no store, no credit,
and nothing to search that they have not copied onto the disk themselves. The `.aax` and
`.aaxc` files Audible's own apps download are locked to Audible and will not play here. What
belongs here is audio in an ordinary format they can copy: `.m4b`, `.m4a` and `.mp3` are what
upstream's directory-structure page uses throughout. Step 7 asks them for it.

Audiobookshelf needs 1024 MB of RAM available and 5 GB free on /srv before any audio. The image
publishes amd64 and arm64. Measure all four:

```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```

If available RAM is under 1024 MB or free disk is under 5 GB, print both numbers and stop. Do not
install and hope. If `dig +short` prints nothing, print that and stop: Caddy cannot get a
certificate for a name that does not resolve. The 5 GB covers the image, the database, the cover
art and the transcoding cache. The audiobooks sit on top of it, and only the user knows how many
hours they have.

## 2. Layout

```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/audiobookshelf /srv/audiobookshelf/backups
sudo install -d -m 700 /srv/audiobookshelf/config /srv/audiobookshelf/metadata
sudo install -d -m 755 -o $(id -u) -g $(id -g) /srv/audiobookshelf/audiobooks /srv/audiobookshelf/podcasts
ls -la /srv/audiobookshelf
```

Assert: `ls -la` shows five entries, with `config` and `metadata` at mode `700` owned by root,
`backups` owned by the login user, and `audiobooks` and `podcasts` at `755`. Upstream states that
Audiobookshelf does not read PUID or PGID, so the container runs as root and writes its database
as root; `config` at 700 means no other account on this box can read the user table.
`audiobooks` stays the login user's so they can copy files into it in step 7; `podcasts` is where
downloaded episodes land, and root writes there whoever owns it.

## 3. Secrets

No secret is generated for this install, and there is no `.env` file. Upstream generates the key
that signs sessions on first start and stores it in the database under /config, so there is
nothing here for `openssl` to make. Step 8 backs up /config, which is where that key lives.

The only credential this server has is the root account, created in a browser in step 7. Say one
thing to the user now: between the container starting and them filling in that form,
the setup screen is open to whoever loads the hostname first. Step 7 is written to make that
window short, and it is a hard stop for exactly that reason.

## 4. compose.yml

```bash
cat > /srv/audiobookshelf/compose.yml <<'EOF'
# Audiobookshelf · the deterministic fallback. Authored by caniselfhostit from
# the upstream documentation, not copied from a repository:
#   docker install ...... https://www.audiobookshelf.org/docs/documentation/install/docker
#   configuration ....... https://www.audiobookshelf.org/docs/documentation/install/configuration
#   backups ............. https://www.audiobookshelf.org/docs/documentation/server-management/backups
#   reverse proxy ....... https://www.audiobookshelf.org/docs/documentation/install/reverse-proxy/caddy
#
# One service. The whole state is a SQLite database under /config, which
# upstream says must be local disk and never a network mount. The image already
# sets PORT=80, CONFIG_PATH=/config and METADATA_PATH=/metadata, so this file
# does not repeat them. Upstream states that Audiobookshelf does not read PUID
# or PGID, so the container runs as root and the two state directories are
# root-owned on the host to match. The audiobook library is mounted read-only,
# because nothing in this configuration writes to it. Tag and digest read from
# ghcr.io on 2026-08-06; the image publishes linux/amd64 and linux/arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  audiobookshelf:
    image: ghcr.io/advplyr/audiobookshelf:2.36.0@sha256:180acad33d69c99ed208676465d8edcb268fa46967735579a7810859885b1a8e
    container_name: audiobookshelf
    restart: unless-stopped
    environment:
      TZ: UTC
      # Backups belong beside the other backups on this box rather than inside
      # the metadata cache. The scheduler that fills this folder ships off and
      # stays off until it is turned on in the web UI.
      BACKUP_PATH: /backups
    volumes:
      - /srv/audiobookshelf/config:/config
      - /srv/audiobookshelf/metadata:/metadata
      - /srv/audiobookshelf/backups:/backups
      # Read-only. Nothing in this configuration writes to the library, and the
      # mount is what keeps a mis-click in the UI from writing to it either.
      - /srv/audiobookshelf/audiobooks:/audiobooks:ro
      # Read-write, because podcast episodes are downloaded into this folder.
      - /srv/audiobookshelf/podcasts:/podcasts
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8125.
      - "127.0.0.1:8125:80"
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/healthcheck"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 30s
EOF
cd /srv/audiobookshelf && docker compose config >/dev/null && echo "compose OK"
```

Assert: that prints `compose OK`. One service, one published port, one database file.

## 5. Caddy and TLS

Append the block below to the Caddyfile Prompt Zero installed, with `<DOMAIN>` replaced by the
real hostname. Copy the file first: a syntax error here takes down every other site on the box.

```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-audiobookshelf
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Audiobookshelf · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://www.audiobookshelf.org/docs/documentation/install/reverse-proxy/caddy,
# https://caddyserver.com/docs/caddyfile/directives/reverse_proxy and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. Upstream's own
# Caddy page is two directives, an encode and a reverse_proxy, and notes that
# the web player needs a websocket connection. Caddy upgrades that connection
# with no extra directive, so the only additions here are the headers.

<DOMAIN> {
	# The player bundle and its JSON API compress well. Audio does not, and
	# Caddy's default encode matcher covers text, JSON, JavaScript and SVG
	# only, so the audio streams pass through untouched.
	encode zstd gzip

	# Audiobookshelf already sets Content-Security-Policy frame-ancestors
	# 'self' and Referrer-Policy no-referrer on every response, so this block
	# does not repeat either. HSTS is here because every request to this host
	# carries a session cookie or a bearer token.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		-Server
	}

	# 8125 is the loopback port compose publishes on this host. It is not a
	# container port and it is not open in the firewall. The websocket the
	# player opens rides this same proxy.
	reverse_proxy 127.0.0.1:8125
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

Assert: `caddy validate` exits 0 and the reload exits 0. If validate fails, restore
/etc/caddy/Caddyfile.before-audiobookshelf, reload, and report what it objected to. Caddy asks
for the certificate on the first request to the hostname and renews it on its own.

## 6. Firewall

Two ports open, both Caddy's. Idempotent, so on a box Prompt Zero configured they change nothing:

```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```

80/tcp redirects to HTTPS and answers the ACME challenge, 443/tcp is the only way in, and 443/udp
is HTTP/3. 8125 stays closed because compose binds it to 127.0.0.1 and Caddy is the only thing
that speaks to it. Assert: `ufw status verbose` prints `Status: active`, shows 80, 443/tcp and
443/udp, and no rule mentioning 8125.

## 7. Start and verify

```bash
cd /srv/audiobookshelf
docker compose pull
docker compose up -d
for i in $(seq 1 24); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/healthcheck); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
curl -sS https://<DOMAIN>/status | grep -o '"isInit":[a-z]*'
```

Assert both, and print what you received for each: the loop ends printing `200`, and the status
line prints `"isInit":false`, which upstream computes as "no root user exists yet". If the loop
never reaches 200, stop, run `docker compose logs --tail 40 audiobookshelf`, and name the
likely earlier step: a container that exits within seconds usually means step 2 left
/srv/audiobookshelf/config unwritable, and a 502 from Caddy with a running container means step
5. A running container is not success.

`"isInit":false` also means the next person to load that URL becomes the root user of this
server, so close that window now rather than after the audio arrives.

STOP: tell the user to open https://<DOMAIN> and create their account, and wait. Do not continue
until they confirm. The first screen reads `Initial Server Setup` above `Create Root User`, with
Username, Password and Confirm Password boxes and the config and metadata paths shown below them.
Tell them to set a real password: the form will accept an empty one behind a confirmation dialog,
and this server answers on the public internet.

```bash
curl -sS https://<DOMAIN>/status | grep -o '"isInit":[a-z]*'
```

Assert: `"isInit":true`. That is the setup screen closed for good, and it is the security assert
in this block. If it still prints `false`, the account was not created; do not go on.

STOP: tell the user to copy at least one audiobook into /srv/audiobookshelf/audiobooks, from
their own machine, not the server, and wait. Do not continue until they confirm. Upstream expects
one folder per book, `{Author}/{Book}` or `{Author}/{Series}/{Book}`. This is the command, with
their own path on the left:

```bash
rsync -av --info=progress2 ~/Audiobooks/ vps:/srv/audiobookshelf/audiobooks/
```

Once they confirm, tell them the last step is theirs: Audiobookshelf scans no folder it has not
been told about. In the web UI, `Libraries`, `Add Library`, media type `Books`, folder
`/audiobooks`. Then read what the scanner did:

```bash
sleep 30
docker compose logs audiobookshelf | grep -F 'Library scan' | tail -2
```

Assert: a line containing `Library scan` and `completed in`, ending in `N Added | 0 Updated |
0 Missing`, with N greater than 0. Print it. `0 Added` means the folder was added but the files under it are
not laid out as one folder per book. A lone `Starting` line means the scan is still running, so
wait 30 seconds and run it again.

## 8. First backup and restore

One archive: the database, the compose file and the Caddy site block. The audio is not in it, on
purpose: it is the user's own library, tens of gigabytes, and it belongs in whatever backup
already protects the machine they copied it from. Upstream's backup page says the same of its
built-in backups, which exclude media files too.

```bash
cd /srv/audiobookshelf
docker compose stop
sudo tar -czf /srv/audiobookshelf/backups/audiobookshelf-config-$(date +%F).tar.gz -C /srv/audiobookshelf config compose.yml -C /etc/caddy Caddyfile
docker compose start
ls -lh /srv/audiobookshelf/backups/
```

Assert: the archive exists and is non-empty. Print its size. Downtime is about five seconds; the
container is stopped on purpose because a SQLite database copied mid-write is not a backup.

A backup on the same disk as the data is not a backup. Run this one from the user's machine, not
the server:

```bash
mkdir -p ~/backups/audiobookshelf
scp vps:/srv/audiobookshelf/backups/*.tar.gz ~/backups/audiobookshelf/
```

To restore: `docker compose down`, `sudo rm -rf /srv/audiobookshelf/config`, recreate it as in
step 2, untar the archive back into /srv/audiobookshelf, put the Caddy block back if that is what
was lost, then `docker compose up -d`. The accounts, the libraries and every listening position
are in `config/absdatabase.sqlite`. Tell the user that file is the whole product: the audio can be
copied again from their own machine; the place they stopped in each book cannot.

One more thing the software will not say: the built-in scheduled backup is off until they turn it
on, in `Settings`, then `Backups`. Compose has already pointed it at /srv/audiobookshelf/backups.

## 9. Updating later

New versions are listed at https://github.com/advplyr/audiobookshelf/releases. Take the backup
first, then edit the image line in /srv/audiobookshelf/compose.yml to the new tag and digest:

```bash
cd /srv/audiobookshelf
docker compose pull
docker compose up -d
docker compose logs --tail 30 audiobookshelf
```

Audiobookshelf migrates its own database on the way up. Watch that log until it settles, then
re-run step 7's `/healthcheck` and `/status` checks before calling the update done.

## 10. What will probably go wrong

The library will look empty and the install will look broken. Mine did. Copying files into
/srv/audiobookshelf/audiobooks does nothing on its own: the folder is mounted, but no library
points at it until somebody adds one in the web UI, and until then the home screen is an empty
state with no error anywhere and nothing in the log. I spent ten minutes checking mount syntax
before I opened `Libraries` and found there were none. Add the library first, then judge the
scan.

## 11. Out of scope

- Do not mount the audiobook library read-write to make the upload button work. Uploading,
  embedding metadata into the audio files and storing metadata beside the items all write into
  the library, and this install keeps it read-only on purpose.
- Do not configure OIDC single sign-on. It needs an identity provider registered elsewhere, and
  the root account created in step 7 is enough for one household.
- Do not set `JWT_SECRET_KEY`. Upstream generates that value on first start and stores it in the
  database, which step 8 already backs up.
- Do not configure SMTP or the notification hooks. Audiobookshelf runs without either.
No terminal agent? Use the chat fallback — slower, you paste the commands

For ChatGPT or Claude in a browser. The model cannot touch your server, so it hands you one command at a time and you run each one. Same install, more of your evening.

This path is slower: you paste every command yourself, and there is nobody watching the output
but you. If you can run Claude Code, use the other tab.

You are installing Audiobookshelf 2.36.0 on a VPS where Prompt Zero is done: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny. Run everything over `ssh vps`
unless a step says otherwise, and replace `<DOMAIN>` with the hostname whose A record already
points at the box.

Read this before step 1. Audiobookshelf plays audio files that are already on that server. There
is no store, no credit, and nothing to search that you have not copied onto the disk yourself.
The `.aax` and `.aaxc` files Audible's own apps download are locked to Audible and will not play
here; what belongs in this library is audio you hold in an ordinary format you can copy, which in
upstream's own directory-structure examples means `.m4b`, `.m4a` and `.mp3`. Step 7 is where you
copy that library up, and how long it takes is a question about your upload speed.

## 1. Preflight

```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```

You should see: at least `1024` MB available, at least `5` G free, `amd64` or `arm64`, and your
server's IP on the last line.

If you do not: an empty last line means the A record does not exist yet. Add it, wait a minute,
run `dig +short <DOMAIN>` again. Caddy cannot get a certificate for a hostname that does not
resolve, and failed attempts count against a rate limit you cannot see. The 5 G floor covers the
image, the database, the cover art and the transcoding cache only. Your audiobooks sit on top of
that number, so check the size of the folder on your own machine with `du -sh` and make sure the
server has room for it before you go any further.

## 2. Layout

```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/audiobookshelf /srv/audiobookshelf/backups
sudo install -d -m 700 /srv/audiobookshelf/config /srv/audiobookshelf/metadata
sudo install -d -m 755 -o $(id -u) -g $(id -g) /srv/audiobookshelf/audiobooks /srv/audiobookshelf/podcasts
ls -la /srv/audiobookshelf
```

You should see: five entries, with `config` and `metadata` at mode `drwx------` owned by root,
`backups` at `drwxr-x---` owned by you, and `audiobooks` and `podcasts` at `drwxr-xr-x`.

If you do not: leave `config` and `metadata` owned by root on purpose. Upstream states that
Audiobookshelf does not read PUID or PGID, so the container runs as root and writes its database
as root; mode 700 there means no other account on this box can read your user table.
`audiobooks` stays yours so you can copy files into it in step 7, and `podcasts` is where
downloaded episodes land, which root writes to whoever owns the folder.

## 3. Secrets

There is nothing to generate here and there is no `.env` file. Upstream creates the key that
signs your sessions on first start and stores it in the database under /config, so no `openssl`
command runs in this install and there is no value for you to copy anywhere. Step 8 backs up
/config, which is where that key lives.

The only credential this server will have is the root account, and you create it in a browser in
step 7. Two things follow from that. First, between the container starting and you filling in
that form, the setup screen is open to whoever loads your hostname first, which is why step 7 is
written the way it is. Second, and this is the rule for this whole path: do not paste the
password you choose, any API key you generate later, or any command output containing either,
into this chat window. The other tab never sees those values; this one will hand them to a third
party unless you keep them out.

## 4. compose.yml

Paste the whole block at once, including the last two lines.

```bash
cat > /srv/audiobookshelf/compose.yml <<'EOF'
# Audiobookshelf · the deterministic fallback. Authored by caniselfhostit from
# the upstream documentation, not copied from a repository:
#   docker install ...... https://www.audiobookshelf.org/docs/documentation/install/docker
#   configuration ....... https://www.audiobookshelf.org/docs/documentation/install/configuration
#   backups ............. https://www.audiobookshelf.org/docs/documentation/server-management/backups
#   reverse proxy ....... https://www.audiobookshelf.org/docs/documentation/install/reverse-proxy/caddy
#
# One service. The whole state is a SQLite database under /config, which
# upstream says must be local disk and never a network mount. The image already
# sets PORT=80, CONFIG_PATH=/config and METADATA_PATH=/metadata, so this file
# does not repeat them. Upstream states that Audiobookshelf does not read PUID
# or PGID, so the container runs as root and the two state directories are
# root-owned on the host to match. The audiobook library is mounted read-only,
# because nothing in this configuration writes to it. Tag and digest read from
# ghcr.io on 2026-08-06; the image publishes linux/amd64 and linux/arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  audiobookshelf:
    image: ghcr.io/advplyr/audiobookshelf:2.36.0@sha256:180acad33d69c99ed208676465d8edcb268fa46967735579a7810859885b1a8e
    container_name: audiobookshelf
    restart: unless-stopped
    environment:
      TZ: UTC
      # Backups belong beside the other backups on this box rather than inside
      # the metadata cache. The scheduler that fills this folder ships off and
      # stays off until it is turned on in the web UI.
      BACKUP_PATH: /backups
    volumes:
      - /srv/audiobookshelf/config:/config
      - /srv/audiobookshelf/metadata:/metadata
      - /srv/audiobookshelf/backups:/backups
      # Read-only. Nothing in this configuration writes to the library, and the
      # mount is what keeps a mis-click in the UI from writing to it either.
      - /srv/audiobookshelf/audiobooks:/audiobooks:ro
      # Read-write, because podcast episodes are downloaded into this folder.
      - /srv/audiobookshelf/podcasts:/podcasts
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8125.
      - "127.0.0.1:8125:80"
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/healthcheck"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 30s
EOF
cd /srv/audiobookshelf && docker compose config >/dev/null && echo "compose OK"
```

You should see: `compose OK` and nothing else.

If you do not: `services must be a mapping` means the indentation was lost between the page and
your terminal; run `rm /srv/audiobookshelf/compose.yml` and paste again in one go. A complaint
about the `image` line usually means the digest was wrapped across two lines, and it has to be
one line. There is no `env_file` in this file and no `.env` to be missing, so a message about one
means you pasted a block from a different project.

## 5. Caddy and TLS

This appends one site block to the Caddy config Prompt Zero installed. Replace `<DOMAIN>` in the
block with your hostname before you paste. The first line takes a copy, because a syntax error
here takes down every other site on the box.

```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-audiobookshelf
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Audiobookshelf · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://www.audiobookshelf.org/docs/documentation/install/reverse-proxy/caddy,
# https://caddyserver.com/docs/caddyfile/directives/reverse_proxy and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. Upstream's own
# Caddy page is two directives, an encode and a reverse_proxy, and notes that
# the web player needs a websocket connection. Caddy upgrades that connection
# with no extra directive, so the only additions here are the headers.

<DOMAIN> {
	# The player bundle and its JSON API compress well. Audio does not, and
	# Caddy's default encode matcher covers text, JSON, JavaScript and SVG
	# only, so the audio streams pass through untouched.
	encode zstd gzip

	# Audiobookshelf already sets Content-Security-Policy frame-ancestors
	# 'self' and Referrer-Policy no-referrer on every response, so this block
	# does not repeat either. HSTS is here because every request to this host
	# carries a session cookie or a bearer token.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		-Server
	}

	# 8125 is the loopback port compose publishes on this host. It is not a
	# container port and it is not open in the firewall. The websocket the
	# player opens rides this same proxy.
	reverse_proxy 127.0.0.1:8125
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

You should see: `Valid configuration` from validate, and no output at all from reload.

If you do not: run `sudo cp /etc/caddy/Caddyfile.before-audiobookshelf /etc/caddy/Caddyfile`,
reload, and paste again. The most common cause is a `<DOMAIN>` you forgot to replace, and Caddy
names the line it choked on. Caddy asks for the certificate the first time somebody requests that
hostname and renews it on its own, so there is nothing to schedule afterwards.

## 6. Firewall

```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```

You should see: `Status: active`, rules for `80/tcp`, `443/tcp` and `443/udp`, and no rule
mentioning `8125`.

If you do not: delete anything for `8125` with `sudo ufw delete allow 8125`. That port is bound
to 127.0.0.1 by the compose file, so Caddy reaches it and nothing on the internet can. 80/tcp
redirects to HTTPS and answers the ACME challenge, 443/tcp is the only way in, and 443/udp is
HTTP/3, which Caddy offers by default. `Status: inactive` is a different problem: Prompt Zero
left this firewall enabled, so something has turned it off since, and `sudo ufw enable` puts it
back before you go any further.

## 7. Start and verify

```bash
cd /srv/audiobookshelf
docker compose pull
docker compose up -d
for i in $(seq 1 24); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/healthcheck); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
curl -sS https://<DOMAIN>/status | grep -o '"isInit":[a-z]*'
```

You should see, in order: the loop reaching `200`, then `"isInit":false`.

If you do not: a loop that never reaches 200 wants `docker compose logs --tail 40
audiobookshelf`. A container that exits within seconds almost always cannot write
/srv/audiobookshelf/config, which sends you back to step 2. A `502` from Caddy while
`docker compose ps` shows the container running is step 5 instead. `/healthcheck` returns an
empty body with a 200, so the loop printing `200` is the whole of that check.

`"isInit":false` is upstream's way of saying no root user exists yet, and it also means the next
person to load that URL becomes the root user of your server. Close that window now, before you
go looking for your audiobooks.

Open https://<DOMAIN> in a browser. The first screen reads `Initial Server Setup` above
`Create Root User`, with Username, Password and Confirm Password boxes and the config and
metadata paths shown below them. Fill it in and submit. Set a real password: the form will accept
an empty one behind a confirmation dialog, and this server answers on the public internet. Then
come back here and run:

```bash
curl -sS https://<DOMAIN>/status | grep -o '"isInit":[a-z]*'
```

You should see: `"isInit":true`.

If you do not: the account was not created, and the setup screen is still open to whoever finds
it. Do not carry on until this prints `true`. This is the security check in this step, not a
formality.

Now the library. Run this one on your own machine, not the server, with your own path on the
left. Upstream expects one folder per book, `{Author}/{Book}` or `{Author}/{Series}/{Book}`:

```bash
rsync -av --info=progress2 ~/Audiobooks/ vps:/srv/audiobookshelf/audiobooks/
```

You should see: a file count and a transfer rate, then a summary line. One book is enough to
carry on; the rest can follow whenever you like.

Then the last step, which is yours: Audiobookshelf scans no folder it has not been told about. In
the web UI, open `Libraries`, then `Add Library`, media type `Books`, folder `/audiobooks`. Back
on the server:

```bash
cd /srv/audiobookshelf
sleep 30
docker compose logs audiobookshelf | grep -F 'Library scan' | tail -2
```

You should see: a line containing `Library scan` and `completed in`, ending in `N Added |
0 Updated | 0 Missing`, with N greater than 0.

If you do not: `0 Added` means the folder was added but the files under it are not laid out as
one folder per book, so check `ls /srv/audiobookshelf/audiobooks` against that pattern. Only a
`Starting` line means the scan is still running, so wait thirty seconds and run the command
again. A running container is not success. The two results that mean success are `"isInit":true`
and a scan line with something Added.

## 8. First backup and restore

One archive: the database, the compose file and the Caddy site block. Your audio is not in it, on
purpose. It is tens of gigabytes you already own, and it belongs in whatever backup protects your
own machine. Upstream's backup page says the same of its own built-in backups, which do not
include media files either.

```bash
cd /srv/audiobookshelf
docker compose stop
sudo tar -czf /srv/audiobookshelf/backups/audiobookshelf-config-$(date +%F).tar.gz -C /srv/audiobookshelf config compose.yml -C /etc/caddy Caddyfile
docker compose start
ls -lh /srv/audiobookshelf/backups/
```

You should see: one `.tar.gz`, a few hundred kilobytes on a fresh install. The service is offline
for about five seconds while the archive is made.

If you do not: an archive of a few hundred bytes means `tar` found nothing under `config`, which
means the container never wrote its database, and that sends you back to step 7. The container is
stopped on purpose: a SQLite database copied while it is being written is not a backup, and the
copy will look fine until the day you need it.

A backup on the same disk as the data is not a backup. Run this one on your own machine, not the
server:

```bash
mkdir -p ~/backups/audiobookshelf
scp vps:/srv/audiobookshelf/backups/*.tar.gz ~/backups/audiobookshelf/
```

You should see: one file copied, and it listed by `ls -lh ~/backups/audiobookshelf/`.

If you do not: `Permission denied (publickey)` means you ran it on the server. The `vps:` prefix
only means something on your own machine, where the alias Prompt Zero created lives.

Now prove the restore, today, while the only thing at risk is one account and a scan you can run
again:

```bash
cd /srv/audiobookshelf
docker compose down
sudo rm -rf /srv/audiobookshelf/config
sudo install -d -m 700 /srv/audiobookshelf/config
sudo tar -xzf /srv/audiobookshelf/backups/audiobookshelf-config-$(date +%F).tar.gz -C /srv/audiobookshelf config compose.yml
docker compose up -d
sleep 20
curl -sS https://<DOMAIN>/status | grep -o '"isInit":[a-z]*'
```

You should see: `"isInit":true`, which means your account survived a config directory that was
deleted and rebuilt from the archive.

If you do not: `"isInit":false` means the database did not come back and Audiobookshelf created
an empty one, so read the archive listing with `tar -tzf` before you trust it with anything. The
stakes are worth stating plainly: your accounts, your libraries and every listening position live
in `config/absdatabase.sqlite`. The audio you can copy up again from your own machine; the place
you stopped in each book you cannot.

One more thing the software will not tell you: the built-in scheduled backup is off until you
turn it on, in `Settings`, then `Backups`. The compose file has already pointed it at
/srv/audiobookshelf/backups.

## 9. Updating later

New versions are listed at https://github.com/advplyr/audiobookshelf/releases. Take the backup
first, then edit the `image:` line in /srv/audiobookshelf/compose.yml to the new tag and digest.

```bash
cd /srv/audiobookshelf
docker compose pull
docker compose up -d
docker compose logs --tail 30 audiobookshelf
```

You should see: migration lines, then the server starting, and no repeating restart.

If you do not: put the old tag and digest back and run the same three commands. Then re-run the
`/healthcheck` and `/status` checks from step 7 before you call the update done, and open the web
player and start one book as well, because a server that answers `/healthcheck` can still be
failing to stream if a migration stopped halfway.

## 10. What will probably go wrong

The library will look empty and the install will look broken. Mine did. Copying files into
/srv/audiobookshelf/audiobooks does nothing on its own: the folder is mounted, but no library
points at it until somebody adds one in the web UI, and until then the home screen is an empty
state with no error anywhere and nothing in the log. I spent ten minutes checking mount syntax
before I opened `Libraries` and found there were none. Add the library first, then judge the
scan.

## 11. Out of scope

- Do not mount the audiobook library read-write to make the upload button work. Uploading,
  embedding metadata into the audio files and storing metadata beside the items all write into
  the library, and this install keeps it read-only on purpose.
- Do not configure OIDC single sign-on. It needs an identity provider registered elsewhere, and
  the root account you created in step 7 is enough for one household.
- Do not set `JWT_SECRET_KEY`. Upstream generates that value on first start and stores it in the
  database, which step 8 already backs up.
- Do not configure SMTP or the notification hooks. Audiobookshelf runs without either.

309 lines · 14,990 bytes

What this prompt will do
  1. Preflight
  2. Docker
  3. Layout
  4. Secrets
  5. compose.yml
  6. Nothing is public
  7. Start and verify
  8. First backup and restore
  9. Updating later
  10. What will probably go wrong
  11. Out of scope

Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.

paste it into Claude Code in a terminal on this computer · installs Docker Desktop if it is missing · no server, no domain

You are Claude Code on the user's own computer. There is no server and no Prompt Zero:
everything in this prompt runs on this machine and stays on it.

Run every command on this computer, in the shell you are already in. Nothing in this prompt
uses ssh.

Install Audiobookshelf 2.36.0 under ~/selfhost/audiobookshelf, answering at
http://localhost:8125.

## 1. Preflight

Say both of these to the user before step 2 runs; they decide whether they want this install at
all. Audiobookshelf plays audio files already on this computer: no store, no credit, nothing
they have not copied in themselves, and the `.aax` and `.aaxc` files Audible's own apps download
are locked to Audible and will not play here. And it answers only at http://localhost:8125, so
the phone in their pocket cannot reach it and the Audiobookshelf apps that make a listening
server worth having stay unused. The browser here is the whole player.

Detect the OS and measure the machine:

```bash
uname -s
case "$(uname -s)" in
  Darwin) vm_stat | awk '/page size/{p=$8} /free|inactive/{s+=$3} END {printf "%d MB available\n", s*p/1048576}' ;;
  Linux) . /etc/os-release && echo "$ID $VERSION_CODENAME"; free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}' ;;
  MINGW*|MSYS*) powershell -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory" | awk '$1+0 {printf "%d MB available\n", $1/1024}' ;;
esac
df -h ~
```

`Darwin` is macOS, `Linux` is Linux, `MINGW` or `MSYS` is Windows under Git Bash. On Linux the
distribution ID and codename print next, for step 2. Audiobookshelf needs 1024 MB of RAM
available and 5 GB free on the home disk before any audio, and the image publishes amd64 and
arm64. Every branch prints free memory, so one floor covers all three; on macOS and Windows that
is the host's, and Docker Desktop takes its share out of it. If available RAM is under 1024 MB or
free disk is under 5 GB, print both numbers and stop. Do not install and hope.

## 2. Docker

Check before installing anything:

```bash
docker info >/dev/null 2>&1 && echo "docker OK" || echo "docker MISSING"
docker compose version 2>/dev/null || true
```

If that printed `docker OK` and a compose version, skip to step 3.

Otherwise, install Docker for the OS step 1 detected:

- macOS: if `command -v brew` succeeds, run `brew install --cask docker`. If there is no
  Homebrew, STOP: tell the user to download Docker Desktop from
  https://www.docker.com/products/docker-desktop/ and install it, and wait until they
  confirm. Either way, then STOP: tell the user to open Docker Desktop once, accept its
  terms, and wait for the whale icon to say it is running. Do not continue until they
  confirm.
- Windows: run `winget install -e --id Docker.DockerDesktop`. If winget is missing or the
  install fails, STOP: tell the user to download Docker Desktop from the URL above and
  install it, and wait until they confirm. Docker Desktop configures WSL 2 itself and may
  ask for a reboot; if it does, STOP and tell the user to reboot and come back, this
  prompt resumes at this step. Then STOP: have the user open Docker Desktop, accept its
  terms, and confirm it says running.
- Linux, Debian or Ubuntu: install Docker Engine from download.docker.com's apt
  repository, with its signing key saved to a file first, never piped into a shell. The
  fence is guarded, a no-op on anything but a Linux with apt:

```bash
if [ "$(uname -s)" = "Linux" ] && command -v apt-get >/dev/null 2>&1; then
  sudo apt-get update
  sudo apt-get install -y ca-certificates curl
  sudo install -m 0755 -d /etc/apt/keyrings
  sudo curl -fsSL https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg -o /etc/apt/keyrings/docker.asc
  sudo chmod a+r /etc/apt/keyrings/docker.asc
  echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$(. /etc/os-release && echo "$ID") $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
  sudo apt-get update
  sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
  sudo usermod -aG docker "$USER"
fi
```

  Adding the user to the docker group is root-equivalent on this machine; say that to the
  user in one sentence, and tell them the group change lands at their next login.
- Linux, anything else: STOP. Tell the user to install Docker Engine and the compose
  plugin with their distribution's package manager, and to run this prompt again once
  `docker info` works.

Assert: `docker info` exits 0 and `docker compose version` prints a version. Do not
continue without both.

## 3. Layout

```bash
mkdir -p ~/selfhost/audiobookshelf/config ~/selfhost/audiobookshelf/metadata ~/selfhost/audiobookshelf/backups ~/selfhost/audiobookshelf/audiobooks ~/selfhost/audiobookshelf/podcasts
ls -la ~/selfhost/audiobookshelf
```

Assert: `ls -la` shows five folders. No ownership fix runs on any of the three systems: upstream
states that Audiobookshelf does not read PUID or PGID, so the container runs as root and writes
into a folder whoever owns it.

Now the library. This prompt copies audio in rather than pointing at the folder it lives in: a
relative path inside ~/selfhost/audiobookshelf behaves the same on all three systems. That is a second copy on the same disk, so size it first, with the user's own path in
place of ~/Audiobooks:

```bash
du -sh ~/Audiobooks
```

STOP: tell the user that size and the free space step 1 printed, and if it fits, tell them to
copy the library in with the command below and wait. Do not continue until they confirm. One
book is enough to go on. Upstream expects one folder per book, `{Author}/{Book}` or
`{Author}/{Series}/{Book}`.

```bash
rsync -a --info=progress2 ~/Audiobooks/ ~/selfhost/audiobookshelf/audiobooks/
```

Assert: `ls ~/selfhost/audiobookshelf/audiobooks` is not empty.

## 4. Secrets

No secret is generated for this install, and there is no `.env` file. Upstream generates the key
that signs sessions on first start and stores it in the database under `config/`, so there is
nothing for `openssl` to make and step 8's backup already carries it.

The only credential this install has is the root account, created in a browser in step 7. Tell
the user that a password still matters on a machine other people use.

## 5. compose.yml

```bash
cat > ~/selfhost/audiobookshelf/compose.yml <<'EOF'
# Audiobookshelf · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
#   docker install ...... https://www.audiobookshelf.org/docs/documentation/install/docker
#   configuration ....... https://www.audiobookshelf.org/docs/documentation/install/configuration
#   backups ............. https://www.audiobookshelf.org/docs/documentation/server-management/backups
#
# One service on the computer you are sitting at. Every path is relative to
# ~/selfhost/audiobookshelf/, so one file works on macOS, Linux and Windows and
# you can open config/, audiobooks/ and backups/ in Finder or Explorer. No
# named volume is needed: upstream states that Audiobookshelf does not read
# PUID or PGID, the container runs as root, and nothing chowns its own data
# directory. The image already sets PORT=80, CONFIG_PATH=/config and
# METADATA_PATH=/metadata. The library is read-only: no media file is ever
# written. Digest read from ghcr.io on 2026-08-06; amd64 and arm64 published.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  audiobookshelf:
    image: ghcr.io/advplyr/audiobookshelf:2.36.0@sha256:180acad33d69c99ed208676465d8edcb268fa46967735579a7810859885b1a8e
    container_name: audiobookshelf
    restart: unless-stopped
    environment:
      TZ: UTC
      # Backups belong in a folder you can see, not inside the metadata cache.
      # The scheduler that fills it stays off until turned on in the web UI.
      BACKUP_PATH: /backups
    volumes:
      - ./config:/config
      - ./metadata:/metadata
      - ./backups:/backups
      # Read-only: nothing here writes to the library, and the mount keeps a
      # mis-click in the UI from writing to it either.
      - ./audiobooks:/audiobooks:ro
      # Read-write, because podcast episodes are downloaded into this folder.
      - ./podcasts:/podcasts
    ports:
      # Loopback only: no other device on the wifi can reach 8125.
      - "127.0.0.1:8125:80"
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/healthcheck"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 30s
EOF
cd ~/selfhost/audiobookshelf && docker compose config >/dev/null && echo "compose OK"
```

Assert: that prints `compose OK`. One service, one port, five folders you can open.

## 6. Nothing is public

No reverse proxy, no certificate, no firewall rule. Each is a decision:

- No DNS. There is no hostname, so nothing to resolve and nothing to wait for.
- No TLS. A certificate attests a public name and nothing here has one. Browsers treat
  http://localhost as a secure context, so the login page works without one.
- No firewall rule. Nothing is published beyond loopback, so no port needs closing.

8125 is bound to 127.0.0.1, this computer only. For a listening server that is the sharp edge of
this path: the phone, the car and the tablet cannot reach it. That is the trade, not a fault.
Confirm it:

```bash
grep -n '127.0.0.1' ~/selfhost/audiobookshelf/compose.yml
```

Assert: two lines, the health check inside the container and `- "127.0.0.1:8125:80"`.

## 7. Start and verify

```bash
cd ~/selfhost/audiobookshelf
docker compose pull
docker compose up -d
for i in $(seq 1 24); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://localhost:8125/healthcheck); echo "$i $code"; [ "$code" = 200 ] && break; sleep 5; done
curl -sS http://localhost:8125/status | grep -o '"isInit":[a-z]*'
```

Assert both, printing what you received for each: the loop ends on `200`, and the status line
prints `"isInit":false`, upstream's way of saying no root user exists yet. If the loop never
reaches 200, stop, run `docker compose logs --tail 40 audiobookshelf`, and name the likely
cause: a container that exits at once usually cannot write `config/`. If `port is already
allocated` came back, find what holds 8125 (`lsof -nP -iTCP:8125 -sTCP:LISTEN`, or
`netstat -ano | findstr :8125` on Windows) and stop until it is free. A running container is not
success.

STOP: tell the user to open http://localhost:8125 and create their account, and wait. Do not
continue until they confirm. The first screen reads `Initial Server Setup` above `Create Root
User`, with Username, Password and Confirm Password boxes and the config and metadata paths below
them. Tell them to set a real password: the form will accept an empty one behind a
confirmation dialog.

Then tell them the last step is theirs: Audiobookshelf scans no folder it was not told about.
In the web UI, `Libraries`, `Add Library`, media type `Books`, folder `/audiobooks`. Then read
both results:

```bash
curl -sS http://localhost:8125/status | grep -o '"isInit":[a-z]*'
sleep 30
docker compose logs audiobookshelf | grep -F 'Library scan' | tail -2
```

Assert both, and print both. `"isInit":true` is the setup screen closed for good, the security
check here rather than a formality. The scanner line contains `Library scan` and
`completed in`, and ends in `N Added | 0 Updated | 0 Missing`, with N greater than 0. `0 Added` means those
files are not laid out as one folder per book. A lone `Starting` line means the scan is still
running, so wait and run it again.

## 8. First backup and restore

One archive: the database and the compose file. The audio is deliberately not in it: it is a
copy of a library the user already had, and it belongs in whatever backup already protects this
computer. Upstream's backup page says the same of its own.

```bash
cd ~/selfhost/audiobookshelf
docker compose stop
tar -C ~/selfhost/audiobookshelf -czf ~/selfhost/audiobookshelf/backups/audiobookshelf-config-$(date +%F).tar.gz config compose.yml
docker compose start
ls -lh ~/selfhost/audiobookshelf/backups/
```

Assert: the archive exists and is non-empty. Print its size. The container stops for about five
seconds on purpose: a SQLite database copied mid-write is not a backup.

That archive sits on the same disk as the data, and on a laptop the disk and the machine fail
together. Ask the user for a destination that leaves this computer, a folder a sync service
watches or a USB stick, and copy it there with `cp`. In Git Bash a Windows drive is `/d/Backups`,
not `D:\Backups`. Assert: the user confirms the filename is listed there. If they have nowhere,
say plainly that this install has no backup.

To restore: `cd ~/selfhost/audiobookshelf`, `docker compose down`, `rm -rf config`, untar the
archive back in, then `docker compose up -d`. The accounts, the libraries and every listening
position live in `config/absdatabase.sqlite`. That file is the whole product: the audio can be
copied again; the place they stopped in each book cannot.

One more thing the software will not say: the built-in scheduled backup is off until the user
turns it on, in `Settings`, then `Backups`. Compose has already pointed it at
~/selfhost/audiobookshelf/backups.

## 9. Updating later

New versions are listed at https://github.com/advplyr/audiobookshelf/releases. Back up first,
then edit the image line in ~/selfhost/audiobookshelf/compose.yml to the new tag and digest:

```bash
cd ~/selfhost/audiobookshelf
docker compose pull
docker compose up -d
docker compose logs --tail 30 audiobookshelf
```

Audiobookshelf migrates its own database on the way up. Watch that log until it settles, then
re-run step 7's checks.

## 10. What will probably go wrong

I closed the lid halfway through the first scan. When I came back the library showed four books out
of ninety, and nothing in the interface said the scan had been interrupted rather than
finished. Docker Desktop had suspended with the machine, which is what a laptop does and a
server does not. Leave the computer awake until the scanner line from step 7 prints
`completed in`, and if the count looks short, run the scan again from `Libraries` rather than
concluding the mount is wrong.

## 11. Out of scope

- Do not expose this to the internet.
- Do not configure port forwarding on the router.
- Do not add a reverse proxy or TLS.
- Do not rebind 8125 to 0.0.0.0 so a phone on the same wifi can reach it. That puts a server with a
  login form on every network this computer joins.
- Do not mount the audiobooks folder read-write to make the upload button work. Uploading and
  embedding metadata both write into the library, which this install keeps read-only on purpose.
- Do not set `JWT_SECRET_KEY` and do not configure OIDC single sign-on. Upstream generates the
  session key itself, and one root account is enough for one computer.
compose.local.ymlthe services, pinned · local layout45 lines

authored from upstream docs, never pasted · 2,215 bytes

# Audiobookshelf · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
#   docker install ...... https://www.audiobookshelf.org/docs/documentation/install/docker
#   configuration ....... https://www.audiobookshelf.org/docs/documentation/install/configuration
#   backups ............. https://www.audiobookshelf.org/docs/documentation/server-management/backups
#
# One service on the computer you are sitting at. Every path is relative to
# ~/selfhost/audiobookshelf/, so one file works on macOS, Linux and Windows and
# you can open config/, audiobooks/ and backups/ in Finder or Explorer. No
# named volume is needed: upstream states that Audiobookshelf does not read
# PUID or PGID, the container runs as root, and nothing chowns its own data
# directory. The image already sets PORT=80, CONFIG_PATH=/config and
# METADATA_PATH=/metadata. The library is read-only: no media file is ever
# written. Digest read from ghcr.io on 2026-08-06; amd64 and arm64 published.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  audiobookshelf:
    image: ghcr.io/advplyr/audiobookshelf:2.36.0@sha256:180acad33d69c99ed208676465d8edcb268fa46967735579a7810859885b1a8e
    container_name: audiobookshelf
    restart: unless-stopped
    environment:
      TZ: UTC
      # Backups belong in a folder you can see, not inside the metadata cache.
      # The scheduler that fills it stays off until turned on in the web UI.
      BACKUP_PATH: /backups
    volumes:
      - ./config:/config
      - ./metadata:/metadata
      - ./backups:/backups
      # Read-only: nothing here writes to the library, and the mount keeps a
      # mis-click in the UI from writing to it either.
      - ./audiobooks:/audiobooks:ro
      # Read-write, because podcast episodes are downloaded into this folder.
      - ./podcasts:/podcasts
    ports:
      # Loopback only: no other device on the wifi can reach 8125.
      - "127.0.0.1:8125:80"
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/healthcheck"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 30s

agent-readable mirror: /self-host/audible.md

The files, if you'd rather do it yourself

The cloud path with no agent involved: three files, in the order you'd use them. The cloud prompt above writes exactly these — if the two ever disagree, the files are the ones CI diffs. The local path ships its own compose file, collapsed under its own prompt.

compose.ymlthe services, pinned47 lines

authored from upstream docs, never pasted · 2,494 bytes

# Audiobookshelf · the deterministic fallback. Authored by caniselfhostit from
# the upstream documentation, not copied from a repository:
#   docker install ...... https://www.audiobookshelf.org/docs/documentation/install/docker
#   configuration ....... https://www.audiobookshelf.org/docs/documentation/install/configuration
#   backups ............. https://www.audiobookshelf.org/docs/documentation/server-management/backups
#   reverse proxy ....... https://www.audiobookshelf.org/docs/documentation/install/reverse-proxy/caddy
#
# One service. The whole state is a SQLite database under /config, which
# upstream says must be local disk and never a network mount. The image already
# sets PORT=80, CONFIG_PATH=/config and METADATA_PATH=/metadata, so this file
# does not repeat them. Upstream states that Audiobookshelf does not read PUID
# or PGID, so the container runs as root and the two state directories are
# root-owned on the host to match. The audiobook library is mounted read-only,
# because nothing in this configuration writes to it. Tag and digest read from
# ghcr.io on 2026-08-06; the image publishes linux/amd64 and linux/arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  audiobookshelf:
    image: ghcr.io/advplyr/audiobookshelf:2.36.0@sha256:180acad33d69c99ed208676465d8edcb268fa46967735579a7810859885b1a8e
    container_name: audiobookshelf
    restart: unless-stopped
    environment:
      TZ: UTC
      # Backups belong beside the other backups on this box rather than inside
      # the metadata cache. The scheduler that fills this folder ships off and
      # stays off until it is turned on in the web UI.
      BACKUP_PATH: /backups
    volumes:
      - /srv/audiobookshelf/config:/config
      - /srv/audiobookshelf/metadata:/metadata
      - /srv/audiobookshelf/backups:/backups
      # Read-only. Nothing in this configuration writes to the library, and the
      # mount is what keeps a mis-click in the UI from writing to it either.
      - /srv/audiobookshelf/audiobooks:/audiobooks:ro
      # Read-write, because podcast episodes are downloaded into this folder.
      - /srv/audiobookshelf/podcasts:/podcasts
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8125.
      - "127.0.0.1:8125:80"
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/healthcheck"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 30s
Caddyfilethe hostname and TLS34 lines

authored from upstream docs, never pasted · 1,515 bytes

# Audiobookshelf · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://www.audiobookshelf.org/docs/documentation/install/reverse-proxy/caddy,
# https://caddyserver.com/docs/caddyfile/directives/reverse_proxy and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname pointed at this box. Upstream's own
# Caddy page is two directives, an encode and a reverse_proxy, and notes that
# the web player needs a websocket connection. Caddy upgrades that connection
# with no extra directive, so the only additions here are the headers.

<DOMAIN> {
	# The player bundle and its JSON API compress well. Audio does not, and
	# Caddy's default encode matcher covers text, JSON, JavaScript and SVG
	# only, so the audio streams pass through untouched.
	encode zstd gzip

	# Audiobookshelf already sets Content-Security-Policy frame-ancestors
	# 'self' and Referrer-Policy no-referrer on every response, so this block
	# does not repeat either. HSTS is here because every request to this host
	# carries a session cookie or a bearer token.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		-Server
	}

	# 8125 is the loopback port compose publishes on this host. It is not a
	# container port and it is not open in the firewall. The websocket the
	# player opens rides this same proxy.
	reverse_proxy 127.0.0.1:8125
}
install.shthe same install, no agent147 lines

authored from upstream docs, never pasted · 7,234 bytes

#!/usr/bin/env bash
# Audiobookshelf · the agent-free install.
#
# Everything prompt.md tells an agent to do, as a script you can read first.
# Run it on the VPS, as a non-root user who is in the docker group:
#
#   DOMAIN_HOST=books.example.com ./install.sh
#
# Authored by caniselfhostit from the upstream documentation:
#   https://www.audiobookshelf.org/docs/documentation/install/docker
#   https://www.audiobookshelf.org/docs/documentation/install/configuration
#   https://www.audiobookshelf.org/docs/documentation/install/reverse-proxy/caddy
#   https://www.audiobookshelf.org/docs/documentation/server-management/backups
#
# No secret is generated here and no .env file is written. Upstream creates the
# key that signs sessions on first start and keeps it in the database under
# /srv/audiobookshelf/config, which the backup at the end of this script
# already covers.
#
# This script does not create your Audiobookshelf account. Only a browser can,
# and until you do, whoever loads the hostname first becomes the root user. The
# summary at the end tells you to go and do it now, and gives you the one
# command that proves the setup screen is shut.
#
# It does not add your library either. Audiobookshelf scans no folder it has
# not been told about, so copying audio into audiobooks/ does nothing until you
# add a library in the web UI. The summary says how.
#
# NOT YET VERIFIED: no harness run has been recorded against this script.
set -euo pipefail

APP_DIR="${APP_DIR:-/srv/audiobookshelf}"
DOMAIN_HOST="${DOMAIN_HOST:-}"

die() { printf 'install.sh: %s\n' "$1" >&2; exit 1; }

# --- 1. Refuse to start on a machine that is not ready -----------------------

[ -n "$DOMAIN_HOST" ] || die "set DOMAIN_HOST to the hostname you pointed at this server, e.g. books.example.com"
command -v docker >/dev/null 2>&1 || die "docker is not installed. Run Prompt Zero first."
docker compose version >/dev/null 2>&1 || die "the docker compose plugin is missing"
command -v caddy >/dev/null 2>&1 || die "caddy is not installed on the host. Run Prompt Zero first."
command -v curl >/dev/null 2>&1 || die "curl is not installed"

avail_mb="$(free -m | awk '/^Mem:/ {print $7}')"
[ "$avail_mb" -ge 1024 ] || die "only ${avail_mb} MB of RAM available; this install wants 1024 MB"
avail_gb="$(df -BG --output=avail /srv | tail -1 | tr -dc '0-9')"
[ "$avail_gb" -ge 5 ] || die "only ${avail_gb} GB free on /srv; this install wants 5 GB before any audio"

resolved="$(getent hosts "$DOMAIN_HOST" | awk '{print $1; exit}' || true)"
[ -n "$resolved" ] || die "$DOMAIN_HOST does not resolve yet. Add the A record, wait a minute, run this again."

# --- 2. Lay the files out ----------------------------------------------------
#
# config and metadata stay root-owned at mode 700: upstream states that
# Audiobookshelf does not read PUID or PGID, so the container runs as root and
# writes its database as root, and nothing else on this box needs to read it.
# audiobooks and podcasts are yours, so you can copy your library in.

sudo install -d -m 750 -o "$(id -u)" -g "$(id -g)" "$APP_DIR" "$APP_DIR/backups"
sudo install -d -m 700 "$APP_DIR/config" "$APP_DIR/metadata"
sudo install -d -m 755 -o "$(id -u)" -g "$(id -g)" "$APP_DIR/audiobooks" "$APP_DIR/podcasts"
install -m 0644 "$(dirname "$0")/compose.yml" "$APP_DIR/compose.yml"
install -m 0644 "$(dirname "$0")/Caddyfile" "$APP_DIR/Caddyfile"

cd "$APP_DIR"
docker compose config >/dev/null

# --- 3. Caddy site block, on the host ----------------------------------------

if ! sudo grep -qF "$DOMAIN_HOST {" /etc/caddy/Caddyfile; then
	sudo cp /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.before-audiobookshelf"
	printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
	sed "s|<DOMAIN>|${DOMAIN_HOST}|g" "$APP_DIR/Caddyfile" | sudo tee -a /etc/caddy/Caddyfile >/dev/null
fi
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

# --- 4. Ports: two open, and 8125 is not one of them -------------------------

if command -v ufw >/dev/null 2>&1; then
	echo "==> 80/tcp and 443/tcp for Caddy, 443/udp for HTTP/3; 8125 stays closed"
	sudo ufw allow 80/tcp
	sudo ufw allow 443/tcp
	sudo ufw allow 443/udp
	sudo ufw status verbose
fi

# --- 5. Start it -------------------------------------------------------------

docker compose pull
docker compose up -d

echo "==> waiting for https://${DOMAIN_HOST}/healthcheck"
for _ in $(seq 1 24); do
	code="$(curl -sS -o /dev/null -w '%{http_code}' "https://${DOMAIN_HOST}/healthcheck" || true)"
	[ "$code" = "200" ] && break
	sleep 5
done
[ "${code:-}" = "200" ] || die "/healthcheck answered ${code:-nothing}. Check: docker compose logs --tail 40 audiobookshelf"

# Nobody has claimed the root account yet. That is expected at this point and
# it is also the thing you have to go and fix in a browser.
state="$(curl -sS "https://${DOMAIN_HOST}/status" | grep -o '"isInit":[a-z]*' || true)"
[ "$state" = '"isInit":false' ] || die "/status reported ${state:-nothing}, not \"isInit\":false. Stop and investigate."

# --- 6. The first backup, before day one ends --------------------------------
#
# The database, the compose file and the live Caddy config. Not the audio: that
# is your library, and it belongs in the backup that already protects the
# machine you copied it from. Upstream's own backups exclude media too.

STAMP="$(date +%Y%m%d-%H%M%S)"
docker compose stop
sudo tar -czf "$APP_DIR/backups/audiobookshelf-config-${STAMP}.tar.gz" -C "$APP_DIR" config compose.yml -C /etc/caddy Caddyfile
docker compose start
ls -lh "$APP_DIR/backups/"
[ -s "$APP_DIR/backups/audiobookshelf-config-${STAMP}.tar.gz" ] || die "the config archive is empty"

cat <<-DONE

	Audiobookshelf is answering at https://${DOMAIN_HOST}/healthcheck

	  1. Do this first, now, before anything else. Open https://${DOMAIN_HOST}
	     and create your account on the screen that reads "Initial Server
	     Setup". Until you do, the first person to load that hostname becomes
	     the root user of this server. Choose a real one, because the form
	     will accept an empty password behind a confirmation dialog. Then
	     prove the setup screen is shut:
	       curl -sS https://${DOMAIN_HOST}/status | grep -o '"isInit":[a-z]*'
	     That must print "isInit":true.
	  2. Copy your audiobooks into $APP_DIR/audiobooks, from your own machine.
	     Upstream expects one folder per book, {Author}/{Book}:
	       rsync -av --info=progress2 ~/Audiobooks/ vps:$APP_DIR/audiobooks/
	     Then add the library in the web UI: Libraries, Add Library, media
	     type Books, folder /audiobooks. Nothing is scanned until you do.
	     Watch what the scanner found:
	       cd $APP_DIR && docker compose logs audiobookshelf | grep -F 'Library scan' | tail -2
	  3. That folder is mounted read-only, so the upload button in the UI will
	     not write to it. Nothing else in this install needs write access to
	     your library, which is the point.
	  4. First backup written to $APP_DIR/backups. The scheduled backup inside
	     Audiobookshelf is off until you turn it on, in Settings, Backups; it
	     is already pointed at this folder. Both sit on the same disk as the
	     data, which is not a backup. Copy them somewhere else tonight.

DONE

What you're signing up for

The part a vendor's comparison page leaves out. None of it is a reason not to do this; all of it is yours the moment you cancel Audible.

  • You bring the audio. This plays files that are already on the disk, and nothing appears in it that you did not put there. The .aax and .aaxc files Audible's apps download are locked to Audible and will not play here. Audible's catalog, its credits and its Originals are the actual product, and no self-hosted server replaces them. What you get back is the player, the library and the bill.
  • The apps are the reason this works, with one caveat worth knowing first. Upstream publishes its own Android app on Google Play and its own iOS app as a TestFlight beta, so the phone keeps your place the way the paid app does. The iOS side is a beta invite rather than an App Store listing, and TestFlight builds expire and have to be re-installed.
  • Your library is not in the backup. The install backs up the database, which holds your accounts, your libraries and every listening position, and that archive is small and quick. The audio is tens of gigabytes and belongs in whatever protects the machine you copied it from. Upstream's own backups exclude media files for the same reason.
  • Nothing backs itself up until you say so. The scheduled backup inside Audiobookshelf ships turned off, and this install points it at a folder and then tells you to go and enable it. A backup setting you never opened is not a backup.
  • No credits, no Originals, no Whispersync with a Kindle, and no way to buy the book you wanted at eleven at night. Those are the subscription, not missing features of this.

Where this came from

“no media files or covers stored with library items are backed up”

  • The published image sets PORT to 80, CONFIG_PATH to /config and METADATA_PATH to /metadata, and exposes port 80. source
  • Upstream states that Audiobookshelf does not use PUID or PGID environment variables, and that the /config mount must be local storage rather than a network mount. source
  • The server answers /healthcheck with a bare 200 and /status with a JSON object whose isInit field stays false until a root user exists. source
  • Scheduled backups are off by default: backupSchedule ships as false in the server settings, and the BACKUP_PATH environment variable moves where backups are written. source
  • The Android app is published on Google Play and the iOS app is distributed as a TestFlight beta, both linked from the project's own home page. source

Questions people actually ask

Answered from this page's own data — the same numbers, in sentences.

  • Can I self-host Audible?

    Not Audible itself — the vendor does not ship a version you can run on your own server. What you can self-host is the job people pay it for, and the answer to that is Audiobookshelf. An audiobook and podcast server for the files you already own, with first-party phone apps that keep your place. The install is one command: one container behind Caddy with automatic TLS, secrets generated on the server rather than in a chat window, and a first backup taken before the agent says it is done, in about 10 minutes. The prompt on this page does it; the compose.yml, Caddyfile and install.sh below do the same install with no agent at all.

  • What replaces Audible?

    Audiobookshelf. An audiobook and podcast server for the files you already own, with first-party phone apps that keep your place. The honest answer to a narrower question. It does not replace the catalog and nothing self-hosted does; it replaces the player, the library and the progress bar. Point it at audiobooks you already hold in an ordinary format and one container gives you a web player, per-user listening positions, series and author browsing, and a podcast library beside it. The reason it is first is the client story: upstream ships its own phone apps, Android on Google Play and iOS as a TestFlight beta, and those keep your place across devices the way the paid app does. A listening server your phone cannot reach is a folder with extra steps. Audiobookshelf is GPL-3.0-licensed and free; nothing on this page is a hosted service we sell you.

  • What does self-hosting cost compared to Audible?

    1024 MB of RAM and 5 GB of disk — the smallest tier most VPS hosts sell, about $5 a month. Audiobookshelf itself is free and GPL-3.0-licensed; the bill is the server, plus a domain you probably already own. What you stop paying: Audible Premium Plus, $14.95/mo — $179.40 a year.

  • How hard is it really?

    ONE COMMAND — under 10 minutes. The rule that produced that verdict: one container, no database, no outside integration, at most one secret. Nothing to negotiate with anyone else, nothing to back up separately, at most one secret to generate. This is the case where the compose file honestly is the whole install. The tier is derived from seven countable facts about the Audiobookshelf install, not from anyone's impression of it, and the whole rubric is published on the methodology page.

  • Can I run Audiobookshelf on my own computer instead of a server?

    Yes — that is the second path in the prompt box above. "On my computer" installs the same Audiobookshelf on the machine you are sitting at: no VPS, no domain, no DNS, and nothing exposed to the internet. It checks for Docker first and installs Docker Desktop if the machine does not have it — macOS, Windows and Linux each get their own step — then binds everything to loopback, so the app answers on http://localhost and only on that computer. The catch: On your own computer this answers only at http://localhost:8125, so the phone and the car cannot reach it and the Audiobookshelf apps that are the best part of it stay unused: the browser on that one machine is the whole player. Same discipline as the cloud path: pinned images, secrets generated on the machine, and a first backup taken before the prompt says it is done.

Content last checked 2026-08-06. Verdicts are derived from the published rubric on /methodology; corrections go through the issue tracker.