Self-host Audiobookshelf on a VPS
Run Audiobookshelf 2.37.2 on a VPS with Docker Compose and Caddy, name folders so books match on the first scan, sync progress to your phone, and back it up.
What you are building
To self-host Audiobookshelf on a VPS, you run its Docker image with four separate folders and put Caddy in front of it for HTTPS. Then you lay out your books the way the scanner expects, and the web player and the phone app share one listening position per user. This guide uses Audiobookshelf v2.37.2. It was the latest release on 27 October 2026 and was published on 23 October 2026.
Audiobookshelf is an open-source server for audiobooks and podcasts. It keeps progress for each user and syncs it across devices. You upload your files once, and every device reads them from your server. Nothing is stored with a third party.
You need a Linux VPS with Docker and the Compose plugin installed, plus a domain name you control. If Compose is new to you, read the Docker Compose basics for a VPS first. The commands below assume Ubuntu 24.04 and a user with sudo.
Plan the folders before you install
The official Docker setup mounts four folders into the container. Each one has a different job, and that job decides where the folder may live.
/configholds the SQLite database and its migration scripts./metadataholds book metadata, cover and author images, logs and the built-in backups./audiobooksholds your book files./podcastsholds downloaded podcast episodes.
They are separate because the data in them behaves differently. The database in /config is small, and it changes every time someone plays or pauses. The media folders are large, and they change only when you add a book. Keeping them apart means you can put media on a bigger disk. It also means you can back up the small folders often without copying hundreds of gigabytes each time.
The docs give two rules here. First, each mount point must be its own directory, not a folder inside another mount point. Second, /config must be on the same machine as the server. The docs warn that a database on a network share makes performance suffer and that "you will eventually encounter database corruption." So if you add remote storage later, over NFS (network file system) or SSHFS, put only the media folders on it. Keep /config and /metadata on the VPS's local disk.
Who owns the files: user, not PUID and PGID
Many self-hosted images take PUID and PGID environment variables to choose which user the app runs as. Audiobookshelf does not. Its Docker docs say plainly: "Audiobookshelf does not use PUID or GUID environment variables." (They mean PGID.) If you set them, nothing happens, and the container keeps its default user.
To run the server as a specific user, use the Compose user: directive with a numeric UID (user ID) and GID (group ID). The folders on the host must be owned by that same UID and GID. If they are not, the server cannot write its database, or it cannot read the books you copy in. For the general idea behind matching container and host IDs, see how PUID, PGID and container file ownership work.
Find your own IDs. Then create the folders and give them to that user:
id -u
id -g
sudo mkdir -p /srv/audiobookshelf/config /srv/audiobookshelf/metadata /srv/audiobookshelf/audiobooks /srv/audiobookshelf/podcasts
sudo chown -R "$(id -u):$(id -g)" /srv/audiobookshelf
ls -ln /srv/audiobookshelfls -ln prints numeric owners. Every line should show the two numbers that id -u and id -g printed, for example 1000 1000.
Install Audiobookshelf with Docker Compose
Create /srv/audiobookshelf/compose.yaml:
services:
audiobookshelf:
image: ghcr.io/advplyr/audiobookshelf:2.37.2
container_name: audiobookshelf
user: "1000:1000"
ports:
- "127.0.0.1:13378:80"
volumes:
- ./config:/config
- ./metadata:/metadata
- ./audiobooks:/audiobooks
- ./podcasts:/podcasts
environment:
- TZ=Europe/Berlin
restart: unless-stoppedThree lines differ from the example in the docs. Each change is on purpose.
The image tag is pinned. The docs use :latest, which they describe as "the most recent stable release." A version tag such as 2.37.2 selects a release, but tags are mutable: a publisher can point the same tag at a different image. Pin by digest to guarantee that a re-pull uses the same image, and update that digest when you upgrade. There is also an :edge tag, which is rebuilt on every commit to the master branch. Do not run it on a server you depend on.
The port is bound to 127.0.0.1. Inside the container the server listens on port 80. The docs map host port 13378 to it and say to change only the external number. Adding 127.0.0.1: in front means only programs on the VPS itself can reach port 13378. Caddy will be that program. Without it, the server would answer plain HTTP on your public IP address, and passwords would cross the internet unencrypted.
user: is set. Replace 1000:1000 with the numbers from id -u and id -g. Keep the quotes so YAML reads the value as text.
Set TZ to your own time zone name from the tz database. The docs use America/Toronto as their example. Then start the server:
cd /srv/audiobookshelf
sudo docker compose up -d
sudo docker compose ps
curl -sI http://127.0.0.1:13378/ | head -n 1docker compose ps should list audiobookshelf as running. The curl line should print an HTTP status line. If it prints nothing, and curl without -s reports curl: (7) Failed to connect, the server is not listening yet. Read the log:
sudo docker compose logs --tail 50 audiobookshelfA permission error naming EACCES in that log means the UID in user: does not own /srv/audiobookshelf/config or /srv/audiobookshelf/metadata. Fix the owner with chown, then run sudo docker compose up -d again.
Put HTTPS in front with Caddy
The Audiobookshelf FAQ says that to reach the server away from home, "you must set up your own VPN or reverse proxy." A reverse proxy is a web server that accepts the public HTTPS connection and passes it to the app on localhost. Caddy is a good fit because it gets and renews TLS (transport layer security) certificates by itself through ACME (automatic certificate management environment). By default it uses Let's Encrypt and ZeroSSL, and it redirects HTTP to HTTPS.
Your domain's A record (and AAAA record, if you use IPv6) must point at the VPS. Port 443 must be open from the internet for this setup, in the VPS firewall and in your provider's network firewall if it has one. Open port 80 too for HTTP redirects and the HTTP ACME challenge. If port 80 is blocked, Caddy can still get a certificate using its default TLS-ALPN challenge on port 443. With ufw:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcpInstall Caddy from its official package repository, as shown in the guide to running Caddy as a reverse proxy on a VPS. Then replace the contents of /etc/caddy/Caddyfile with the site block from the official Audiobookshelf Caddy guide, using your own domain:
audiobooks.example.com {
encode gzip zstd
reverse_proxy 127.0.0.1:13378
}Check the file and load it:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
curl -I https://audiobooks.example.comcaddy validate should end with Valid configuration. The last curl should return a status line over HTTPS. If it fails with a certificate error in the first minute, wait and try again, because Caddy may still be getting the certificate. If it keeps failing, sudo journalctl -u caddy -n 50 shows the reason. Check that DNS points at this VPS and that port 443 is reachable; blocking port 80 alone does not prevent Caddy from getting a certificate.
Why the apps need WebSocket support
The Audiobookshelf docs state: "Audiobookshelf requires a websocket connection." A WebSocket is a long-lived two-way connection. It starts as a normal HTTP request and then asks the server to "upgrade" it. Audiobookshelf uses this connection to push live updates to the web client and the apps. A proxy that does not pass the upgrade request breaks that connection.
Caddy needs no extra line for this. Its reverse_proxy directive performs the HTTP upgrade and then tunnels the connection in both directions. That is why the official Caddy block above has only two directives. One detail: by default Caddy closes open WebSocket connections when you reload its config, so a player connected at that moment has to reconnect. If that bothers you, the Caddy docs describe stream_close_delay for reverse_proxy.
If you use nginx instead
nginx does not pass the upgrade unless you tell it to, so the official nginx guide sets it by hand. The important lines inside location / are these:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_pass http://127.0.0.1:13378;
client_max_body_size 10240M;The explicit proxy_http_version 1.1 line is needed before nginx 1.29.7; on 1.29.7 and newer, HTTP/1.1 is the default. The Upgrade and Connection headers still need to be set explicitly.
Copy the full server block from the Audiobookshelf docs, because it also sets Host, X-Forwarded-For and X-Forwarded-Proto. The docs add the client_max_body_size line to prevent the 413 Request Entity Too Large error, which nginx returns when an upload through the web UI is larger than its limit.
The docs also support serving Audiobookshelf from a subfolder at /audiobookshelf. In v2.37.2, a custom path requires matching ROUTER_BASE_PATH settings when building the web client and running the server. A subdomain such as audiobooks.example.com is simpler.
First login and accounts
Open https://audiobooks.example.com. The first time, Audiobookshelf asks you to create the root user, which is the original admin account. Use a long password, because this page is now on the public internet.
Audiobookshelf has four account types: Root, Admin, User and Guest. Admins cannot edit the Root account, and Guests cannot change their own passwords. Give every listener their own User account. Progress is stored per user, and the docs say account sharing is not supported. Two people playing different books from one account at the same time causes problems.
Lay out books so the first scan matches
The scanner reads folders, not loose files. The docs put it in one line: "Books are defined as a folder." Each book must be in its own folder, and the path is {Author}/{Series}/{Book} or {Author}/{Book}. The only exception is a book that is a single file, such as one .m4b. That file may sit in the library root.
This is the example tree from the docs:
audiobooks/
Terry Goodkind/
Heart of Black Ice - Sister of Darkness/
Audio File.m4a
Sword of Truth/
Vol 1 - 1994 - Wizards First Rule {Sam Tsoutsouvas}/
Audio Track 1.mp3
Audio Track 2.mp3
Cover.jpg
Vol 2 - 1995 - Stone of Tears/
Audiobook.m4bThe folder names carry metadata, and the rules are strict:
- Authors can be
First LastorLast, First. Separate several authors with commas, semicolons,&orand. - A series number must be followed by a dash with a space on each side, or by a period and a space. If the number is not at the start of the name, put
Vol,Vol.,VolumeorBookbefore it. - The publish year must be the first part of the name, or come directly after the series number.
- The narrator goes in curly braces, as in
{Sam Tsoutsouvas}. - The Audible ASIN (Amazon standard identification number) goes in square brackets, as in
1. Wizards First Rule [B002V0QK4C]. - Multi-disc books use subfolders named
Disc,CDorDiskfollowed by a number.
A subtitle after - is only read when you enable that option in the settings. Otherwise it stays part of the title.
Every audio file inside a book folder is one track. Multi-disc books play in disc order, then track order. You can reorder or disable tracks later with the Manage Tracks button. Audiobookshelf also reads the ID3 tags inside the audio files, so set the library's metadata priority to match how you organise things. If your folder names are cleaner than your tags, put the folder structure first.
The most common first-scan problem follows from the folder rule. If you put two books into one folder, Audiobookshelf treats every file in that folder as a track of one book. You get one item with both books merged. Move each book into its own folder, remove the merged item from the Audiobookshelf database without deleting its files, and scan again. Removing and readding the books loses their saved progress.
Copy the files and create the library
From your own computer, copy your library over SSH with rsync. It keeps the folder structure:
rsync -avh --progress ./Audiobooks/ youruser@your-vps:/srv/audiobookshelf/audiobooks/Then check the owner on the VPS. Files copied as another user may not be readable by the container:
ls -ln /srv/audiobookshelf/audiobooks | head
sudo chown -R "$(id -u):$(id -g)" /srv/audiobookshelf/audiobooksIn the web UI, open Settings, then Libraries. Add a library of type Books with the folder /audiobooks. Use the container path, not the host path, because the server only sees the folders mounted into it. Do the same for a Podcasts library with /podcasts.
The scan uses local metadata only: folder names, tags, and files such as desc.txt for a description and reader.txt for the narrator. It never contacts an online provider by itself. To pull covers and descriptions from the internet, open a book and use Match, which shows search results and lets you pick one. If books you add later do not appear, run Scan from the library's menu and check the scanner options in the library settings.
Add podcast feeds and auto-download
In a Podcasts library you can add a show by name search or by pasting an RSS (really simple syndication) feed link. You can also upload an OPML (outline processor markup language) file exported from another podcast app. A pasted RSS link is fetched directly, with no search step. Name searches use the region set in the podcast settings.
On disk, each podcast is one folder with a flat layout. There are no season subfolders. Each audio file is one episode, and one cover image sits next to the episodes.
Two defaults surprise people. First, you can only see and play episodes that have been downloaded to the server. The Find Episodes button lists every episode in the feed. From there you download them one by one, or by release date, where a limit of 0 downloads all of them. Second, automatic checking for new episodes is disabled by default. Turn it on for each podcast in the Schedule tab.
The same tab controls cleanup. By default every episode is kept forever. Set "Max episodes to keep" to a positive number and, when the limit is exceeded, v2.37.2 removes one old episode with a publication date after each successful automatic episode download. It does not look at listen state, so an episode you have not heard yet can be deleted if it is the oldest. After 24 failed feed checks in a row, the schedule turns itself off and you must enable it again. That number comes from the MAX_FAILED_EPISODE_CHECKS environment variable. Its default is 24, and 0 turns the cut-off off.
A feed hosted on a local IP address is refused by default. The server filters these requests to prevent SSRF (server-side request forgery), where the server is tricked into fetching addresses on its own network. If you really host a feed on your own network, add DISABLE_SSRF_REQUEST_FILTER=1 to the environment: list and run sudo docker compose up -d to recreate the container. The configuration page also lists SSRF_REQUEST_FILTER_WHITELIST. Read its description there before you turn the whole filter off.
Phone apps and progress sync
The official apps are open source and still in beta. The Android app is on Google Play. The iOS app is distributed through Apple's TestFlight. When this guide was written, the README said the iOS beta was full, because Apple caps a TestFlight beta at 10,000 testers. Check the Audiobookshelf README for the current state before you plan around iOS. The App Beta FAQ says the Android app has more features than the iOS app, and third-party apps exist for both platforms.
In the app, add your server's HTTPS address, https://audiobooks.example.com, and log in with that listener's own account. The app supports several connection profiles, for different users or servers. That is useful if you keep one profile for the public address and one for a VPN address.
The Android app records playback events on the phone: play, pause, save, seek and sync. Its listen history screen shows each sync. A green cloud means the position reached the server. A red exclamation mark means it did not. A failed sync does not lose anything. The position stays on the phone, and only the server is behind until the next successful sync.
Two limits are worth knowing. If you download a file and play it in a different audiobook app, that app does not sync progress back to your server. Also, the web player, including its installable PWA (progressive web app) version, does not work offline. For offline listening, download the book inside the official app.
How much storage an audiobook library needs
Size depends on the bitrate of your files, and audiobook files vary a lot. Do not trust a per-book number from anyone, including this guide. Measure your own collection on the machine where it lives now:
du -sh ~/AudiobooksFor planning, the arithmetic is simple. Take a bitrate in kilobits per second (kbps), multiply by 3,600 seconds, and divide by 8 bits per byte. The result is the size of one hour of audio. The chart below is that calculation, not a measurement of real files.
The data behind this chart
[
{
"label": "32 kbps",
"mb_per_hour": 14.4,
"gb_per_5000_hours": 72
},
{
"label": "64 kbps",
"mb_per_hour": 28.8,
"gb_per_5000_hours": 144
},
{
"label": "128 kbps",
"mb_per_hour": 57.6,
"gb_per_5000_hours": 288
},
{
"label": "192 kbps",
"mb_per_hour": 86.4,
"gb_per_5000_hours": 432
}
]At 64 kbps, one hour of audio takes 28.8 MB, so 5,000 hours fit in about 144 GB. The same hours at 128 kbps need 288 GB. Bitrate matters more than the number of books.
Plan for podcast growth too. With auto-download on and no "Max episodes to keep" value, a daily show adds files every day and never removes them. Also leave room for /metadata, which grows with covers and with the built-in backups stored inside it.
When the library outgrows the VPS disk, the usual move is a second server with large, slower disks for the media only. A storage VPS compared with a regular VPS explains that trade. Mount the remote media into the container as /audiobooks, and keep /config and /metadata on the fast local disk; the docs require local storage for /config.
Back up the config and metadata, not only the media
Audiobookshelf has built-in backups under Settings, then Backups. Read what they contain. In the docs' words, they "only include the Audiobookshelf database and cover images stored in the metadata/ directory." They contain no media files. They are stored in /metadata/backups by default, which means on the same disk as the server. A backup that lives on the disk it protects does not survive the loss of that disk.
Open the Backups page on your own server. Check whether an automatic schedule is set, how many backups it keeps, and the maximum backup size. Do not assume the defaults suit you. Then copy the folders off the VPS on a schedule. A consistent copy is easiest with the container stopped for a few seconds:
cd /srv/audiobookshelf
sudo docker compose stop
sudo tar czf ~/abs-backup-$(date +%F).tgz config metadata
sudo docker compose start
sudo chown "$(id -u):$(id -g)" ~/abs-backup-*.tgz
chmod 600 ~/abs-backup-*.tgzThe archive holds every user's password hash, which is why it gets mode 600. Copy it to another machine with rsync or scp. Back up the media folders separately, with a tool that only sends changed files, because they are large and rarely change. Losing config and metadata costs every user's progress, bookmarks and matched metadata. Losing the media costs the files themselves. You want copies of both.
To restore from a built-in backup on v2.3.0 or newer, the docs give these steps:
- Stop the container with
sudo docker compose stop. - Unzip the newest backup. If that fails, try the next newest.
- Copy
audiobookshelf.sqlitefrom the backup into theconfigfolder, replacing the existing file. - Copy the contents of
metadata-authorsandmetadata-itemsinto theauthorsanditemsfolders undermetadata/. - Start the container with
sudo docker compose start.
The Restore button on the Backups page is the other way to do the same job.
Upgrade without surprises
First read the release notes for the version you are moving to. Then edit the tag in compose.yaml, for example from 2.37.2 to the new number, and pull the new image:
cd /srv/audiobookshelf
sudo docker compose pull
sudo docker compose down
sudo docker compose up -dTake a backup of config and metadata right before an upgrade. A new release can migrate the database, and you can only go back if you have a copy from before the migration.
Feed it from Chaptarr
Chaptarr is a community-run fork of Readarr. Its README describes it as "A book collection manager for audiobooks and eBooks." It is beta software, and Docker is its only supported install. The image is chaptarr/chaptarr:latest and the web UI is on port 8789. It keeps separate root folders for audiobooks and eBooks, and the docker run example in its README mounts /audiobooks inside the container.
The link between the two tools is a shared folder. Chaptarr does not need to know that Audiobookshelf exists. Mount the same host folder into both containers. In Chaptarr's Compose file, that line is:
volumes:
- /srv/audiobookshelf/audiobooks:/audiobooksSet Chaptarr's audiobook root folder to /audiobooks in its UI. The books it imports then land in the folder your Audiobookshelf library already reads. Two checks matter. First, Chaptarr's renaming settings must produce the {Author}/{Book} or {Author}/{Series}/{Book} layout from the section above, one book per folder. Second, the files it writes must be readable by the UID in your Audiobookshelf user: line, so run ls -ln on a new import. Chaptarr's public README does not say whether it can tell Audiobookshelf to scan, so check its settings, and use the Scan button if a new book does not appear. The download side, with indexers and a download client, is covered in the guide to self-hosting a Chaptarr audiobook library.
If the same VPS also serves films or TV, Audiobookshelf sits well next to a Jellyfin media server on a VPS. Each app gets its own folders, and Caddy sends each subdomain to the right port.
FAQ
Does Audiobookshelf use PUID and PGID?
No. The Audiobookshelf Docker docs say it does not use PUID or PGID environment variables, so setting them changes nothing. Use the Compose user: directive with numeric IDs, such as user: "1000:1000". Make sure the host folders for config, metadata, audiobooks and podcasts are owned by that same UID and GID, and check with ls -ln.
Why does Audiobookshelf show two of my books as one book?
Audiobookshelf treats a folder as one book, and every audio file in that folder as one of its tracks. If two books share a folder, they merge into one item. Give each book its own folder under {Author}/{Book} or {Author}/{Series}/{Book}, remove the merged item from the Audiobookshelf database without deleting its files, then scan the library again. Removing and readding the books loses their saved progress. Only a book that is a single file may sit loose in the library root.
Does the Audiobookshelf backup include my audiobook files?
No. The built-in backup holds the SQLite database and the cover and author images from the metadata folder. It holds no media. By default it is also stored inside /metadata/backups on the same server. Copy the config and metadata folders off the VPS, and back up the audiobook and podcast folders separately.
Why do new podcast episodes not download by themselves?
Automatic episode checks are disabled by default in Audiobookshelf. Open the podcast, go to the Schedule tab and turn the schedule on. If a feed fails 24 checks in a row, the schedule turns itself off, so look at that tab again when a show stops updating. Only episodes downloaded to the server can be played.
Do I need a reverse proxy to use the Audiobookshelf app away from home?
You need a reverse proxy or a VPN. The Audiobookshelf FAQ says remote access requires one of the two. A reverse proxy such as Caddy adds HTTPS, and it must pass WebSocket connections, which Audiobookshelf requires. Caddy's reverse_proxy does this with no extra settings. nginx needs the Upgrade and Connection headers; explicitly setting proxy_http_version 1.1 is needed before version 1.29.7, while HTTP/1.1 is the default on 1.29.7 and newer.