Audiobooks & Podcasts
    Docker Compose

    Deploy Audiobookshelf on a VPS

    Self-host Audiobookshelf, an audiobook and podcast server with progress sync, on a RamNode VPS with Docker Compose and Nginx TLS with WebSocket support.

    Audiobookshelf is a self-hosted audiobook and podcast server. It streams any common audio format to the browser or the official mobile apps, tracks listening progress per user across devices, pulls metadata and cover art from online providers, and can subscribe to podcast feeds and download new episodes automatically. Think of it as a private Audible plus a podcast catcher, running on hardware you control.

    This guide deploys Audiobookshelf on a single RamNode VPS using Docker Compose, with the container bound to localhost, Nginx terminating TLS in front of it, and the library stored on the VPS disk or an attached volume.

    Architecture Overview

    • Audiobookshelf container. A Node.js application that serves the web UI, the REST API, and the Socket.IO connection that pushes progress and library updates to clients in real time. FFmpeg is bundled in the image for probing files and transcoding on the fly when a client cannot play a format natively.
    • /config. Holds the SQLite database. This is your users, libraries, listening progress, and settings.
    • /metadata. Holds cached covers, author images, logs, and the built-in backups.
    • Media mounts. One host directory per library type. This guide uses /srv/media/audiobooks and /srv/media/podcasts.
    • Nginx. Handles TLS and proxies HTTP and WebSocket traffic to the container on 127.0.0.1:13378.

    The single most common broken install is a reverse proxy that does not pass the WebSocket upgrade. The UI loads, but progress never syncs and the library never refreshes. The Nginx config in Step 6 handles this.

    What You Will Need

    • A RamNode VPS running Ubuntu 24.04 LTS. 1 vCPU and 2 GB RAM is comfortable for a handful of users. Audiobookshelf idles at a few hundred MB. Transcoding is the main CPU cost, and most clients play MP3 and M4B directly without it.
    • Disk sized for your library, not the application. A typical audiobook runs 200 MB to 1 GB. A collection of a few hundred titles needs a few hundred GB. If your plan supports additional block storage volumes, put media there and keep the root disk for the OS and Docker.
    • Root or sudo access.
    • A domain or subdomain with an A record pointing at the VPS. This guide uses abs.example.com.

    Step 1: Prepare the System

    shell
    apt update && apt upgrade -y
    apt install -y ca-certificates curl gnupg ufw rsync
    timedatectl set-timezone America/New_York

    Set the timezone to your own. Podcast download schedules and backup times use it.

    Add swap on 2 GB plans. Metadata matching during a large initial scan can spike memory.

    shell
    fallocate -l 2G /swapfile
    chmod 600 /swapfile
    mkswap /swapfile
    swapon /swapfile
    echo '/swapfile none swap sw 0 0' >> /etc/fstab

    Optional: Mount a Block Storage Volume for Media

    If you attached a separate volume for the library, format and mount it before going further. Confirm the device name with lsblk first.

    shell
    lsblk
    mkfs.ext4 /dev/vdb
    mkdir -p /srv/media
    echo "UUID=$(blkid -s UUID -o value /dev/vdb) /srv/media ext4 defaults,nofail 0 2" >> /etc/fstab
    mount -a
    df -h /srv/media

    Keep /config on the root disk regardless. The Audiobookshelf docs are explicit that the config directory must be on local storage on the same machine, not a network share, because SQLite locking does not survive network filesystems.

    Step 2: Install Docker

    Install Docker Engine and the Compose plugin from Docker's repository. The docker.io package in Ubuntu's archive lags behind.

    shell
    install -m 0755 -d /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
    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/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
      > /etc/apt/sources.list.d/docker.list
    
    apt update
    apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    systemctl enable --now docker

    Cap container log size so a chatty container cannot fill the disk.

    shell
    cat > /etc/docker/daemon.json <<'EOF'
    {
      "log-driver": "json-file",
      "log-opts": { "max-size": "10m", "max-file": "3" }
    }
    EOF
    systemctl restart docker

    Step 3: Create the Service User and Directories

    Audiobookshelf does not read PUID/PGID variables. It honors the Compose user: directive instead, so create a dedicated system user and run the container as that UID.

    shell
    useradd --system --no-create-home --shell /usr/sbin/nologin abs
    
    mkdir -p /opt/audiobookshelf/{config,metadata}
    mkdir -p /srv/media/{audiobooks,podcasts}
    
    chown -R abs:abs /opt/audiobookshelf /srv/media/audiobooks /srv/media/podcasts

    Record the IDs for the Compose file.

    shell
    cat > /opt/audiobookshelf/.env <<EOF
    ABS_UID=$(id -u abs)
    ABS_GID=$(id -g abs)
    TZ=$(timedatectl show -p Timezone --value)
    EOF
    cat /opt/audiobookshelf/.env

    Step 4: Write the Compose File

    shell
    pico /opt/audiobookshelf/compose.yaml
    shell
    services:
      audiobookshelf:
        image: ghcr.io/advplyr/audiobookshelf:latest
        container_name: audiobookshelf
        user: "${ABS_UID}:${ABS_GID}"
        environment:
          - TZ=${TZ}
        ports:
          - "127.0.0.1:13378:80"
        volumes:
          - /opt/audiobookshelf/config:/config
          - /opt/audiobookshelf/metadata:/metadata
          - /srv/media/audiobooks:/audiobooks
          - /srv/media/podcasts:/podcasts
        restart: unless-stopped

    Two details matter here:

    • 127.0.0.1:13378:80. Docker writes its own iptables rules and bypasses UFW. Publishing as 13378:80 would expose the unencrypted port to the internet regardless of your firewall. Binding to loopback means only Nginx can reach it.
    • Leave the internal port at 80. Change only the host side if 13378 conflicts with something.

    For production, replace latest with a specific release tag from the project's GitHub releases page so upgrades happen when you choose, not whenever the container is recreated.

    Start it.

    shell
    cd /opt/audiobookshelf
    docker compose up -d
    docker compose logs -f

    Wait for the log line saying the server is listening, then Ctrl+C out of the log stream. Confirm it answers locally.

    shell
    curl -sI http://127.0.0.1:13378 | head -1

    Step 5: Configure the Firewall

    shell
    ufw allow OpenSSH
    ufw allow 'Nginx Full'
    ufw enable
    ufw status

    Nginx Full will not exist until Nginx is installed in the next step. Run Step 6's install line first if UFW complains.

    Step 6: Install Nginx and Configure the Reverse Proxy

    shell
    apt install -y nginx certbot python3-certbot-nginx

    Create the site.

    shell
    pico /etc/nginx/sites-available/audiobookshelf
    shell
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }
    
    server {
        listen 80;
        listen [::]:80;
        server_name abs.example.com;
    
        # Large uploads through the web UI. A single M4B can exceed 1 GB.
        client_max_body_size 10G;
    
        location / {
            proxy_pass http://127.0.0.1:13378;
            proxy_http_version 1.1;
    
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
    
            # Required. Socket.IO carries progress sync and live library updates.
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
    
            # Long-lived sockets and slow uploads.
            proxy_read_timeout 3600s;
            proxy_send_timeout 3600s;
    
            # Stream audio to the client as it arrives instead of buffering it.
            proxy_buffering off;
            proxy_request_buffering off;
    
            proxy_redirect http:// $scheme://;
        }
    }

    Enable it and get a certificate.

    shell
    ln -s /etc/nginx/sites-available/audiobookshelf /etc/nginx/sites-enabled/
    rm -f /etc/nginx/sites-enabled/default
    nginx -t && systemctl reload nginx
    
    certbot --nginx -d abs.example.com --redirect -m you@example.com --agree-tos --no-eff-email

    Certbot edits the server block in place, adds the 443 listener, and installs a systemd timer for renewal. Verify the timer.

    shell
    systemctl list-timers | grep certbot

    Step 7: First-Run Setup

    Browse to https://abs.example.com. The first visit prompts you to create the root user. Use a strong password. This account can manage every user and library on the server.

    Then create libraries:

    1. Settings > Libraries > Add Library.
    2. Name it, set the media type to Books, and add the folder /audiobooks. This is the path inside the container, not the host path.
    3. Pick a metadata provider. Audible works well for commercial titles.
    4. Repeat with media type Podcasts and folder /podcasts.

    Create a non-root user for daily listening under Settings > Users. Use that account in the mobile apps rather than root.

    Step 8: Load Your Library

    Audiobookshelf infers author, series, and title from folder structure. Getting this right up front saves hours of manual matching later.

    shell
    /srv/media/audiobooks/
    ├── Terry Pratchett/
    │   └── Discworld/
    │       ├── 01 - The Colour of Magic/
    │       │   └── The Colour of Magic.m4b
    │       └── 02 - The Light Fantastic/
    │           ├── Part 01.mp3
    │           └── Part 02.mp3
    └── Andy Weir/
        └── Project Hail Mary/
            └── Project Hail Mary.m4b

    The pattern is Author/Series/Title/files or Author/Title/files for standalone books. Each book gets its own folder, even when it is a single file. Multi-file books stay together in one folder and are ordered by filename.

    Copy media up from your workstation with rsync. It resumes cleanly if the connection drops, which matters for multi-hundred-GB transfers.

    shell
    rsync -avP --partial ~/Audiobooks/ root@your-vps-ip:/srv/media/audiobooks/

    Fix ownership after the copy, then trigger a scan from the library's menu, or let the file watcher pick it up.

    shell
    chown -R abs:abs /srv/media/audiobooks

    Step 9: Connect the Mobile Apps

    Install the official Audiobookshelf app, enter https://abs.example.com as the server address, and sign in with your non-root user. Downloads for offline listening and progress sync work immediately once the WebSocket proxy is correct.

    Third-party players that speak the Audiobookshelf API also work against the same URL.

    Backups

    Audiobookshelf has a built-in backup task under Settings > Backups. It writes a zip of the database and metadata to /metadata/backups on a schedule. Enable it and set retention, then copy those zips off the server.

    shell
    ls -lh /opt/audiobookshelf/metadata/backups/

    A simple off-box copy with rsync over SSH from another host:

    shell
    rsync -avP root@your-vps-ip:/opt/audiobookshelf/metadata/backups/ /backups/abs/

    The media library is not in these backups. Your originals should live somewhere else as well. The VPS is a serving copy, not the archive.

    For a full cold backup of application state:

    shell
    cd /opt/audiobookshelf
    docker compose stop
    tar czf /root/abs-state-$(date +%F).tar.gz config metadata
    docker compose start

    Upgrading

    Back up first, then pull and recreate.

    shell
    cd /opt/audiobookshelf
    docker compose pull
    docker compose up -d
    docker image prune -f

    If you pinned a version tag, edit compose.yaml to the new tag before pulling. Read the release notes for anything flagged as a database migration. Migrations run automatically on startup and are one-way, so the backup from before the upgrade is your rollback path.

    Troubleshooting

    UI loads but progress does not sync, or "socket disconnected" errors appear. The WebSocket upgrade is not reaching the container. Confirm the Upgrade and Connection headers are in the Nginx block that Certbot modified, not only in a copy you edited earlier, and that nothing else (Cloudflare proxy in front, for example) strips them.

    Uploads fail partway through with 413. client_max_body_size is too small or is set in a different server block than the one serving 443. Check with nginx -T | grep client_max_body_size.

    Library scan finds nothing. Check permissions from inside the container.

    shell
    docker exec -it audiobookshelf ls -la /audiobooks

    If you see Permission denied, the host directory is not owned by the abs UID. Rerun the chown.

    Books split into one entry per file, or several books merge into one. Folder structure. Each book needs its own folder. Move files into per-title folders and rescan.

    Container restarts in a loop after an upgrade. Read docker compose logs --tail=100. A failed migration shows here. Restore the pre-upgrade backup and pin the previous tag while you check the project's issue tracker.

    Hardening Notes

    • The container port is bound to loopback. Keep it that way. Check with ss -tlnp | grep 13378, which should show 127.0.0.1 only.
    • Disable root SSH password login and use keys.
    • Create separate accounts per listener and grant only the libraries each person needs.
    • If the server is only for you and a few family members, consider putting it behind a VPN such as WireGuard or Tailscale and removing the public DNS record entirely.
    • Keep the image updated. Audiobookshelf is under active development and security fixes ship in regular releases.

    Next Steps

    Audiobookshelf handles audio. For ebooks, comics, and manga on the same VPS, pair it with Kavita or Komga, each covered in its own RamNode guide. All three run comfortably side by side on a 4 GB plan behind the same Nginx instance, one subdomain each.