233 lines
8.6 KiB
Markdown
233 lines
8.6 KiB
Markdown
---
|
||
name: audiobookshelf-setup
|
||
description: Deploy Audiobookshelf on a self-hosted Linux server — Docker setup, reverse proxy with nginx + Let's Encrypt, DuckDNS for dynamic DNS, and DNS challenge for SSL when ISP blocks ports 80/443.
|
||
version: 1.0.0
|
||
author: Hermes Agent
|
||
license: MIT
|
||
platforms: [linux]
|
||
metadata:
|
||
hermes:
|
||
tags: [audiobookshelf, docker, nginx, lets-encrypt, duckdns, reverse-proxy, self-hosted]
|
||
category: self-hosting
|
||
related_skills: [plex-setup, selfhosted-migration]
|
||
---
|
||
|
||
# Audiobookshelf Setup
|
||
|
||
Deploy Audiobookshelf via Docker with secure HTTPS access — including reverse proxy, dynamic DNS, and SSL via Let's Encrypt DNS challenge (for residential ISPs that block ports 80/443).
|
||
|
||
## When to Use
|
||
|
||
- User asks to install Audiobookshelf
|
||
- User wants a self-hosted audiobook/podcast server
|
||
- User needs HTTPS access to a self-hosted service behind a residential ISP
|
||
- User needs a reverse proxy pattern for any Docker-hosted web service
|
||
|
||
## Prerequisites
|
||
|
||
- Docker + docker-compose-v2 installed
|
||
- A domain or DuckDNS subdomain (for SSL)
|
||
- Port forwarding in router (for external access)
|
||
|
||
## Setup Steps
|
||
|
||
### 1. Create directory structure
|
||
|
||
```bash
|
||
mkdir -p ~/docker/audiobookshelf/{config,metadata}
|
||
```
|
||
|
||
### 2. Create docker-compose.yml
|
||
|
||
```yaml
|
||
services:
|
||
audiobookshelf:
|
||
image: ghcr.io/advplyr/audiobookshelf:latest
|
||
container_name: audiobookshelf
|
||
network_mode: host
|
||
environment:
|
||
- TZ=America/New_York
|
||
- PORT=13378
|
||
volumes:
|
||
- /home/ray/docker/audiobookshelf/config:/config
|
||
- /home/ray/docker/audiobookshelf/metadata:/metadata
|
||
- /path/to/audiobooks:/audiobooks:ro
|
||
restart: unless-stopped
|
||
```
|
||
|
||
**Key points:**
|
||
|
||
- **`network_mode: host`** — Audiobookshelf defaults to port 80 internally. Set `PORT=13378` to move it off port 80 (needed for nginx later). With host networking, the port binds directly on the host.
|
||
- **Audiobook mount** — Use `:ro` (read-only). Audiobookshelf only reads files; no write access needed.
|
||
- The container needs ~5-10 seconds on first run to initialize its SQLite database and generate a JWT secret.
|
||
|
||
### 3. Start and verify
|
||
|
||
```bash
|
||
cd ~/docker/audiobookshelf && docker compose pull && docker compose up -d
|
||
sleep 10
|
||
curl -s -o /dev/null -w "HTTP %{http_code}" http://localhost:13378/
|
||
# Should return 200
|
||
```
|
||
|
||
### 4. Set up DuckDNS (free dynamic DNS)
|
||
|
||
If using a residential ISP without a static IP:
|
||
|
||
1. Go to https://duckdns.org → sign in with GitHub/Google
|
||
2. Create a subdomain (e.g., `mybooks`)
|
||
3. Copy the token from the top of the page
|
||
|
||
Install the updater script and cron job:
|
||
|
||
```bash
|
||
sudo mkdir -p /opt/duckdns
|
||
|
||
sudo tee /opt/duckdns/update.sh << 'EOF' > /dev/null
|
||
#!/bin/bash
|
||
echo url="https://www.duckdns.org/update?domains=<YOUR_SUBDOMAIN>&token=<YOUR_TOKEN>&ip=" | curl -k -s -o /opt/duckdns/duck.log -K -
|
||
EOF
|
||
|
||
sudo chmod +x /opt/duckdns/update.sh
|
||
|
||
# Run once to register
|
||
sudo /opt/duckdns/update.sh && cat /opt/duckdns/duck.log
|
||
# Should output "OK"
|
||
|
||
# Auto-update every 5 minutes
|
||
(sudo crontab -l 2>/dev/null | grep -v duckdns; echo "*/5 * * * * /opt/duckdns/update.sh") | sudo crontab -
|
||
```
|
||
|
||
Verify the domain resolves: `dig +short <subdomain>.duckdns.org`
|
||
|
||
### 5. Install nginx + certbot
|
||
|
||
```bash
|
||
sudo apt-get update && sudo apt-get install -y nginx certbot python3-certbot-nginx
|
||
|
||
# Install DuckDNS plugin for Let's Encrypt DNS challenge
|
||
sudo pip3 install --break-system-packages certbot-dns-duckdns
|
||
```
|
||
|
||
### 6. Get SSL certificate via DNS challenge
|
||
|
||
**Why DNS challenge:** Residential ISPs often block incoming ports 80/443. Let's Encrypt HTTP challenge requires port 80. DNS challenge works without any open ports — it adds a TXT record to the domain.
|
||
|
||
```bash
|
||
# Create credentials file
|
||
sudo mkdir -p /etc/letsencrypt
|
||
sudo tee /etc/letsencrypt/duckdns.ini << 'EOF' > /dev/null
|
||
dns_duckdns_token=<YOUR_TOKEN>
|
||
EOF
|
||
sudo chmod 600 /etc/letsencrypt/duckdns.ini
|
||
|
||
# Get certificate
|
||
sudo certbot certonly \
|
||
--authenticator dns-duckdns \
|
||
--dns-duckdns-credentials /etc/letsencrypt/duckdns.ini \
|
||
--dns-duckdns-propagation-seconds 30 \
|
||
-d <domain>.duckdns.org \
|
||
--non-interactive --agree-tos --email <your-email>
|
||
```
|
||
|
||
Cert will auto-renew via certbot's built-in systemd timer. No cron needed.
|
||
|
||
### 7. Configure nginx reverse proxy with SSL
|
||
|
||
Pick an **available high port** (since 80/443 are blocked by ISP). Check what's free:
|
||
|
||
```bash
|
||
sudo ss -tlnp | grep -E "3443|4443|5443"
|
||
```
|
||
|
||
Create the nginx config:
|
||
|
||
```nginx
|
||
server {
|
||
listen 3443 ssl;
|
||
server_name <domain>.duckdns.org;
|
||
|
||
ssl_certificate /etc/letsencrypt/live/<domain>.duckdns.org/fullchain.pem;
|
||
ssl_certificate_key /etc/letsencrypt/live/<domain>.duckdns.org/privkey.pem;
|
||
ssl_protocols TLSv1.2 TLSv1.3;
|
||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||
|
||
client_max_body_size 500M;
|
||
|
||
location / {
|
||
proxy_pass http://127.0.0.1:13378;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
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;
|
||
proxy_read_timeout 86400;
|
||
}
|
||
}
|
||
|
||
# Optional: redirect HTTP → HTTPS
|
||
server {
|
||
listen 80;
|
||
server_name <domain>.duckdns.org;
|
||
return 301 https://$host:3443$request_uri;
|
||
}
|
||
```
|
||
|
||
Enable and reload:
|
||
|
||
```bash
|
||
sudo cp audiobookshelf.conf /etc/nginx/sites-available/audiobookshelf
|
||
sudo ln -sf /etc/nginx/sites-available/audiobookshelf /etc/nginx/sites-enabled/
|
||
sudo rm -f /etc/nginx/sites-enabled/default
|
||
sudo nginx -t && sudo systemctl reload nginx
|
||
```
|
||
|
||
### 8. Port forwarding
|
||
|
||
Forward the chosen high port (e.g., 3443 TCP) in your router to the server's LAN IP.
|
||
|
||
### 9. Access and create library
|
||
|
||
1. Visit `https://<domain>.duckdns.org:3443`
|
||
2. Create an admin account on first visit
|
||
3. Add a library → point it to `/audiobooks` (the container's internal mount path)
|
||
4. Audiobookshelf will scan and populate automatically
|
||
|
||
### 10. Client apps
|
||
|
||
- **Web player** — built-in, at the same URL
|
||
- **iOS** — [Audiobookshelf App](https://apps.apple.com/us/app/audiobookshelf/id6446379655)
|
||
- **Android** — [Audiobookshelf on Google Play](https://play.google.com/store/apps/details?id=com.audiobookshelf.app)
|
||
|
||
Connect the app to `https://<domain>.duckdns.org:3443` with your account. Supports offline downloads, sleep timer, and playback speed controls.
|
||
|
||
## Pitfalls
|
||
|
||
- **Port 80 conflicts with host networking** — Audiobookshelf defaults to port 80 internally. With `network_mode: host`, this binds directly on the host, conflicting with nginx. Set `PORT=13378` in the environment to move it before nginx setup.
|
||
- **Docker port conflicts** — Other containers may already use ports you want for nginx (e.g., Heimdall on 8443). Check with `docker ps --format '{{.Names}} {{.Ports}}'` before picking an nginx port.
|
||
- **Shell mangling of tokens** — If API tokens or Docker env vars contain special chars (underscores, dashes), bash may mangle them in variable expansion. Use Python for HTTP calls with tokens: `urllib.request.Request(url)` with `headers` works reliably. See `plex-setup` for the same pattern.
|
||
- **Hairpin NAT** — Testing the public URL from inside the LAN often fails (connection refused) even when the setup is correct. Always test from a mobile device on cellular data, not Wi‑Fi.
|
||
- **`certbot-dns-duckdns` not in apt** — On Ubuntu 26.04, this package isn't in apt. Use `pip3 install --break-system-packages certbot-dns-duckdns` instead.
|
||
- **Audiobookshelf first-run delay** — The container needs ~10 seconds to initialize its SQLite database, generate a JWT secret, and start listening. Don't assume it's broken if port 13378 isn't immediately available.
|
||
- **`docker compose restart` vs `up -d`** — `restart` reuses the old container config (doesn't pick up new env vars like `PORT`). Use `up -d` when changing environment variables, then `restart` for simple process cycling.
|
||
|
||
## Verification Checklist
|
||
|
||
- [ ] `curl http://localhost:13378/` returns 200
|
||
- [ ] DuckDNS updater returns "OK" on manual run
|
||
- [ ] `dig +short <domain>.duckdns.org` returns the correct public IP
|
||
- [ ] SSL cert obtained (`sudo certbot certificates`)
|
||
- [ ] nginx config passes `sudo nginx -t`
|
||
- [ ] `curl -sk https://localhost:3443/` returns 200 (SSL working locally)
|
||
- [ ] Port forwarding rule added in router
|
||
- [ ] External access confirmed from cellular data
|
||
|
||
## Support Files
|
||
|
||
- `templates/docker-compose.yml` — Starter Docker Compose file
|
||
- `templates/nginx-audiobookshelf.conf` — nginx reverse proxy config with SSL
|
||
- `scripts/duckdns-update.sh` — DuckDNS IP update script (set DOMAIN and TOKEN)
|
||
- `references/heimdall-dashboard.md` — Add the service to Heimdall dashboard via SQLite
|