Arr-Stack Media Automation¶
The arr-stack is a comprehensive media automation system running on VM 100 (10.89.97.50) that manages TV shows, movies, music, and subtitles.
Overview¶
VM: 100
IP: 10.89.97.50
Location: /opt/arr-stack
Stack: Docker Compose
Access: SSH via ssh root@10.89.97.50
Architecture¶
VPN-Routed Downloads¶
All download traffic routes through Gluetun VPN container (Mullvad WireGuard): - SABnzbd (Usenet) - Port 8080 - Deluge (Torrents) - Port 8112
Management Services¶
Indexer Management: - Prowlarr (Port 9696) - Centralized indexer management
Content Management: - Sonarr (Port 8989) - TV show automation - Radarr (Port 7878) - Movie automation - Lidarr (Port 8686) - Music automation - Bazarr (Port 6767) - Subtitle automation
Request Management: - Overseerr (Port 5055) - Plex-focused request system - Jellyseerr (Port 5056) - Jellyfin-focused request system
Storage¶
Media storage is mounted at /mnt from NAS (LXC 101):
Configuration data persists in /opt/arr-stack/configs/.
Automatic Updates¶
Watchtower runs daily at 3:00 AM to check for and apply container updates automatically.
Update schedule: Every day at 3:00 AM Cleanup: Removes old images after updates Mode: Label-based (only updates containers with watchtower.enable label)
Manual Updates¶
Update a specific service:
Update all services:
Check Watchtower logs:
Common Operations¶
View Running Containers¶
Restart a Service¶
View Logs¶
Full Stack Restart¶
Service Configuration¶
All services use consistent configuration: - PUID/PGID: 1000 (matches host user) - Timezone: America/New_York - Restart policy: unless-stopped
VPN Configuration¶
Gluetun uses Mullvad WireGuard configured for Boston MA exit node. VPN credentials are stored in the docker-compose.yml environment section.
Troubleshooting¶
Service Won't Update¶
If a service shows "Unable to update directly, Update the docker container":
- This is expected behavior for containerized applications
- Use manual update commands above or wait for Watchtower
- Updates happen at container level, not application level
VPN Connection Issues¶
Check Gluetun status:
Restart VPN (will briefly interrupt downloads):
Download Client Not Connecting¶
Services behind VPN (SABnzbd, Deluge) are accessed through Gluetun container. Check:
- Gluetun is running:
docker ps | grep gluetun - VPN connection is up:
docker logs gluetun | grep "ip" - Port mappings are correct in docker-compose.yml
Storage/Mount Issues¶
Verify NAS mount:
Root Folder Path Mismatch¶
Symptom: "Root folder '/mnt/media/tv' was not found" errors in Sonarr/Radarr logs.
Cause: Container paths vs host paths mismatch. All arr containers mount /mnt → /data, so:
- Host path: /mnt/media/tv or /mnt/media/movies
- Container path: /data/media/tv or /data/media/movies (use this!)
Common source: Jellyseerr configured with host paths instead of container paths.
Fix:
-
Check Jellyseerr settings:
-
Fix paths (should show
/data/media/...): -
Fix affected series in Sonarr (via API or UI Settings → Media Management → Root Folders)
-
Remove incorrect root folders from Sonarr/Radarr
Prevention: Always use /data/media/... paths when configuring Jellyseerr, Overseerr, or directly in Sonarr/Radarr.
Lidarr Import Failures (Permissions Error / Cross-Device Link)¶
Symptom: Music downloads stuck in queue with "Failed to import track, Permissions error" messages.
Root Cause: The NFS mounts for /mnt/media and /mnt/downloads are separate exports from the NAS. Even though they're nested paths on the same ZFS pool, Linux sees them as different devices. This causes hardlink creation to fail with "Cross-device link" errors, which Lidarr reports as "Permissions error".
Fix - Disable Hardlinks:
ssh root@10.89.97.50
API_KEY=$(grep -oP '(?<=<ApiKey>)[^<]+' /opt/arr-stack/configs/lidarr/config.xml)
# Check current setting
curl -s "http://localhost:8686/api/v1/config/mediamanagement?apikey=$API_KEY" | jq '{copyUsingHardlinks}'
# Disable hardlinks (use copy instead)
curl -s "http://localhost:8686/api/v1/config/mediamanagement?apikey=$API_KEY" > /tmp/lidarr_mm.json
jq '.copyUsingHardlinks = false' /tmp/lidarr_mm.json | \
curl -s -X PUT "http://localhost:8686/api/v1/config/mediamanagement?apikey=$API_KEY" \
-H "Content-Type: application/json" -d @-
After fixing, retry stuck imports:
# Remove from queue to clear cached error state
curl -s "http://localhost:8686/api/v1/queue?apikey=$API_KEY" | \
jq '.records[] | select(.trackedDownloadState == "importFailed") | .id' | \
xargs -I{} curl -s -X DELETE "http://localhost:8686/api/v1/queue/{}?apikey=$API_KEY&removeFromClient=false&blocklist=false"
# Trigger rescan of downloads folder
curl -s -X POST "http://localhost:8686/api/v1/command?apikey=$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "DownloadedAlbumsScan", "path": "/data/downloads"}'
Note: This also affects Sonarr/Radarr but they handle it more gracefully. The permanent fix would be restructuring NFS to use a single mount point with subdirectories.
Lidarr Boxset/Compilation Matching Issues¶
Symptom: Multi-disc releases (boxsets, compilations, greatest hits) fail with "Has missing tracks" or "Album match is not close enough".
Cause: Lidarr's fingerprinting can't match multi-disc releases to standard album metadata in MusicBrainz.
Workarounds: 1. Manual Import via UI: Lidarr → Wanted → Manual Import → select folder → manually assign album 2. Direct Copy: Copy files directly to music folder with proper structure:
ssh root@10.89.97.50
docker exec lidarr sh -c 'mkdir -p "/data/media/music/Artist Name/Album Name (Year)" && \
cp -v "/data/downloads/Release-Name/"*.flac "/data/media/music/Artist Name/Album Name (Year)/"'
curl -s -X POST "http://localhost:8686/api/v1/command?apikey=$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "RefreshArtist"}'
Watchtower Rate Limits¶
Symptom: toomanyrequests errors in Watchtower logs, some containers not updating.
Cause: Docker Hub rate limits when pulling multiple images in quick succession during a single update run.
Impact: Affected containers skip that update cycle but will update the next day.
Mitigation: This is a Docker Hub limitation. Options:
- Accept occasional missed updates (they'll catch up next run)
- Manually update affected containers: docker compose pull <service> && docker compose up -d <service>
- Use a Docker Hub paid account for higher rate limits
Wrong Language Downloads¶
Symptom: Downloads in wrong language (e.g., German instead of English).
Why it happens: Sonarr/Radarr search indexers and grab releases based on quality profile without language filtering. Whatever matches first gets grabbed.
Fix using Custom Formats:
-
Create a custom format to identify unwanted language:
ssh root@10.89.97.50 API_KEY=$(grep -oP '(?<=ApiKey>)[^<]+' /opt/arr-stack/configs/sonarr/config.xml) curl -s -X POST "http://localhost:8989/api/v3/customformat" \ -H "X-Api-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "German", "includeCustomFormatWhenRenaming": false, "specifications": [{ "name": "German", "implementation": "ReleaseTitleSpecification", "negate": false, "required": false, "fields": [{ "name": "value", "value": "\\b(german|ger)\\b" }] }] }' -
Add to quality profile with negative score (blocks it):
# Get quality profile ID (e.g., 7 for "<8GB") curl -s "http://localhost:8989/api/v3/qualityprofile" -H "X-Api-Key: $API_KEY" | jq ".[] | {id, name}" # Get profile, add format with -10000 score, update PROFILE=$(curl -s "http://localhost:8989/api/v3/qualityprofile/7" -H "X-Api-Key: $API_KEY") UPDATED=$(echo "$PROFILE" | jq '.formatItems += [{"format": 2, "name": "German", "score": -10000}]') curl -s -X PUT "http://localhost:8989/api/v3/qualityprofile/7" \ -H "X-Api-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d "$UPDATED" -
Delete wrong downloads and research:
-
Clear from download client history (see SABnzbd API Operations below)
Note: For Radarr, use port 7878 and config path /opt/arr-stack/configs/radarr/config.xml.
Stuck Downloads (Orange Status)¶
Symptom: Episodes show orange status in Sonarr Activity with "No files found are eligible for import" even though files were deleted.
Cause: Download client (SABnzbd/Deluge) still has "Completed" history entries. Sonarr sees these and expects files to exist.
Fix:
-
Identify entries in download client:
-
Delete from SABnzbd history:
# Delete single entry curl -s "http://localhost:8080/api?mode=history&name=delete&value=SABnzbd_nzo_xxxxx&apikey=$API_KEY" # Delete multiple entries for NZO_ID in SABnzbd_nzo_id1 SABnzbd_nzo_id2 SABnzbd_nzo_id3; do curl -s "http://localhost:8080/api?mode=history&name=delete&value=$NZO_ID&apikey=$API_KEY" > /dev/null done -
Refresh Sonarr - entries will change from orange to red (missing)
Sonarr/Radarr API Operations¶
All arr services have REST APIs for automation. Get API key from config:
ssh root@10.89.97.50
# Sonarr
API_KEY=$(grep -oP '(?<=ApiKey>)[^<]+' /opt/arr-stack/configs/sonarr/config.xml)
# Radarr
API_KEY=$(grep -oP '(?<=ApiKey>)[^<]+' /opt/arr-stack/configs/radarr/config.xml)
Scan Downloads Folder¶
Trigger import of completed downloads:
curl -s -X POST "http://localhost:8989/api/v3/command" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "DownloadedEpisodesScan", "path": "/data/media/downloads"}'
Search for Series/Movie¶
Search all episodes for a series:
# First get series ID
curl -s "http://localhost:8989/api/v3/series" -H "X-Api-Key: $API_KEY" | jq '.[] | {id, title}'
# Trigger search
curl -s -X POST "http://localhost:8989/api/v3/command" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "SeriesSearch", "seriesId": 129}'
Search for a movie (Radarr):
curl -s -X POST "http://localhost:7878/api/v3/command" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "MoviesSearch", "movieIds": [123]}'
Check Command Status¶
curl -s "http://localhost:8989/api/v3/command/1516875" -H "X-Api-Key: $API_KEY" | jq "{name, status, message}"
View/Modify Root Folders¶
# List root folders
curl -s "http://localhost:8989/api/v3/rootfolder" -H "X-Api-Key: $API_KEY" | jq .
# Delete incorrect root folder
curl -s -X DELETE "http://localhost:8989/api/v3/rootfolder/1" -H "X-Api-Key: $API_KEY"
Update Series Path¶
Fix series with wrong root folder:
# Get series details
SERIES=$(curl -s "http://localhost:8989/api/v3/series/129" -H "X-Api-Key: $API_KEY")
# Update paths
UPDATED=$(echo "$SERIES" | jq '.path = "/data/media/tv/Show Name" | .rootFolderPath = "/data/media/tv"')
# Apply update
curl -s -X PUT "http://localhost:8989/api/v3/series/129?moveFiles=false" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "$UPDATED"
View Queue¶
curl -s "http://localhost:8989/api/v3/queue" -H "X-Api-Key: $API_KEY" | \
jq '.records[] | {title, status, errorMessage}'
List Custom Formats¶
API Documentation¶
Full API docs available at: - Sonarr: http://10.89.97.50:8989/docs - Radarr: http://10.89.97.50:7878/docs
Lidarr API Operations¶
Get Lidarr API key:
ssh root@10.89.97.50
API_KEY=$(grep -oP '(?<=<ApiKey>)[^<]+' /opt/arr-stack/configs/lidarr/config.xml)
View Queue¶
curl -s "http://localhost:8686/api/v1/queue?apikey=$API_KEY" | \
jq '.records[] | {title, status: .trackedDownloadStatus, state: .trackedDownloadState}'
Scan Downloads Folder¶
curl -s -X POST "http://localhost:8686/api/v1/command?apikey=$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "DownloadedAlbumsScan", "path": "/data/downloads"}'
Scan Specific Album Folder¶
curl -s -X POST "http://localhost:8686/api/v1/command?apikey=$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "DownloadedAlbumsScan", "path": "/data/downloads/Artist-Album-FLAC-2025"}'
Refresh All Artists¶
curl -s -X POST "http://localhost:8686/api/v1/command?apikey=$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "RefreshArtist"}'
Remove Item from Queue¶
# Remove without deleting files or blocklisting
curl -s -X DELETE "http://localhost:8686/api/v1/queue/QUEUE_ID?apikey=$API_KEY&removeFromClient=false&blocklist=false"
# Remove and delete from download client
curl -s -X DELETE "http://localhost:8686/api/v1/queue/QUEUE_ID?apikey=$API_KEY&removeFromClient=true&blocklist=false"
Check Media Management Settings¶
View Root Folders¶
SABnzbd API Operations¶
Get SABnzbd API key:
ssh root@10.89.97.50
API_KEY=$(grep -oP '(?<=api_key = )[^\n]+' /opt/arr-stack/configs/sabnzbd/sabnzbd.ini)
View History¶
curl -s "http://localhost:8080/api?mode=history&output=json&apikey=$API_KEY" | \
jq '.history.slots[] | {nzo_id, name, status}'
Search History¶
# Case-insensitive search
curl -s "http://localhost:8080/api?mode=history&output=json&apikey=$API_KEY" | \
jq '.history.slots[] | select(.name | test("SearchTerm"; "i")) | {nzo_id, name, status}'
Delete History Entry¶
curl -s "http://localhost:8080/api?mode=history&name=delete&value=SABnzbd_nzo_xxxxx&apikey=$API_KEY"
View Queue¶
curl -s "http://localhost:8080/api?mode=queue&output=json&apikey=$API_KEY" | \
jq '.queue.slots[] | {nzo_id, filename, status, percentage}'
Pause/Resume Queue¶
# Pause all
curl -s "http://localhost:8080/api?mode=pause&apikey=$API_KEY"
# Resume all
curl -s "http://localhost:8080/api?mode=resume&apikey=$API_KEY"
API Documentation¶
SABnzbd API docs: http://10.89.97.50:8080/config/general/ (scroll to API Key section)
Integration with Other Services¶
Plex/Jellyfin:
- Media libraries point to /vault/subvol-101-disk-0/media/ on host
- Arr services write to same location via /mnt mount
Home Portal: - Dashboard integration for quick service access - Status monitoring for all arr services
Backup and Disaster Recovery¶
Configuration backup:
Restore from backup:
Docker Compose backup:
Tdarr (Media Transcoding)¶
Tdarr handles automated media transcoding using the "One Flow" workflow.
Container: tdarr
Port: 8265 (Web UI)
Config: /opt/arr-stack/configs/tdarr/
GPU: Quadro M2000 (Maxwell, passed through to container)
Job Reports Location¶
Job reports are stored at:
File naming pattern:
Find logs for a specific file:
ssh root@10.89.97.50
grep -r 'FILENAME' /opt/arr-stack/configs/tdarr/server/Tdarr/DB2/JobReports/ | head -1
One Flow Troubleshooting¶
"medium: Invalid argument" Error¶
Symptom: ffmpeg fails with Unable to choose an output format for 'medium'
Cause: The CPU quality variable -preset medium is placed after -x265-params, making ffmpeg interpret medium as an output filename.
Fix: In Flow "1 - Input" → "Input - Set Flow Variable fl_cpu_quality🎯":
- From: -preset medium
- To: (leave empty)
Then update fl_cpu_main to include preset:
- From: "lookahead=32"
- To: preset=medium:lookahead=32
"Invalid bitrate_576p value" Error¶
Symptom: Flow fails at JS bitrate calculation step.
Cause: Missing library variable for 576p resolution bitrate.
Fix: In Tdarr UI → Libraries → Movies → Variables, add:
- Key: bitrate_576p
- Value: 1500k
"Max B-frames 4 exceed 0" / NVENC Not Working¶
Symptom: GPU encoding fails, falls back to CPU (libx265).
Cause: Quadro M2000 is Maxwell generation - doesn't support B-frames for HEVC.
Fix: In Flow "1 - Input" → "Input - Set Flow Variable fl_nvenc_b-frames🎯":
- From: -bf 4
- To: -bf 0 (or leave empty)
GPU Busy / ErsatzTV Blocking NVENC¶
Symptom: Tdarr uses CPU encoding even though GPU is configured.
Cause: Quadro M2000 has limited NVENC sessions (~2). ErsatzTV live transcoding can block Tdarr.
Diagnosis:
Workaround: The node config has allowGpuDoCpu: true which falls back to CPU when GPU is busy. To prioritize Tdarr, reduce ErsatzTV's concurrent streams during heavy transcode periods.
Library Variables Reference¶
These variables should be set in Tdarr UI → Libraries → Movies → Variables:
| Variable | Recommended Value | Notes |
|---|---|---|
bitrate_480p |
1250k |
Target bitrate for 480p |
bitrate_576p |
1500k |
Often missing - add this! |
bitrate_720p |
2000k |
Target bitrate for 720p |
bitrate_1080p |
2500k |
Target bitrate for 1080p |
bitrate_1440p |
3800k |
Target bitrate for 1440p |
bitrate_4k |
10000k |
Target bitrate for 4K |
bitrate_4k_hdr |
12500k |
Target bitrate for 4K HDR |
Checking NVENC Status¶
ssh root@10.89.97.50
# Check GPU availability on host
nvidia-smi
# Check if container sees GPU
docker exec tdarr nvidia-smi
# Check Tdarr encoder detection (at startup)
docker logs tdarr 2>&1 | grep -i 'nvenc\|encoder'
Security Notes¶
- VPN credentials stored in docker-compose.yml (not committed to git)
- Services accessible on LAN only (no external exposure)
- Download clients isolated behind VPN for privacy
- Regular automatic updates via Watchtower reduce security vulnerabilities
Quick Access¶
| Service | URL | Purpose |
|---|---|---|
| SABnzbd | http://10.89.97.50:8080 | Usenet downloads |
| Deluge | http://10.89.97.50:8112 | Torrent downloads |
| Prowlarr | http://10.89.97.50:9696 | Indexer management |
| Sonarr | http://10.89.97.50:8989 | TV automation |
| Radarr | http://10.89.97.50:7878 | Movie automation |
| Lidarr | http://10.89.97.50:8686 | Music automation |
| Bazarr | http://10.89.97.50:6767 | Subtitle automation |
| Tdarr | http://10.89.97.50:8265 | Media transcoding |
| Overseerr | http://10.89.97.50:5055 | Request management (Plex) |
| Jellyseerr | http://10.89.97.50:5056 | Request management (Jellyfin) |