Skip to content

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):

/mnt/
├── media/
│   ├── tv/
│   ├── movies/
│   ├── music/
│   └── torrents/
└── downloads/

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:

ssh root@10.89.97.50
cd /opt/arr-stack
docker compose pull sonarr
docker compose up -d sonarr

Update all services:

ssh root@10.89.97.50
cd /opt/arr-stack
docker compose pull
docker compose up -d

Check Watchtower logs:

ssh root@10.89.97.50
docker logs watchtower

Common Operations

View Running Containers

ssh root@10.89.97.50
docker ps

Restart a Service

ssh root@10.89.97.50
cd /opt/arr-stack
docker compose restart sonarr

View Logs

ssh root@10.89.97.50
docker logs sonarr
docker logs -f sonarr  # Follow logs

Full Stack Restart

ssh root@10.89.97.50
cd /opt/arr-stack
docker compose down
docker compose up -d

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":

  1. This is expected behavior for containerized applications
  2. Use manual update commands above or wait for Watchtower
  3. Updates happen at container level, not application level

VPN Connection Issues

Check Gluetun status:

ssh root@10.89.97.50
docker logs gluetun | tail -20

Restart VPN (will briefly interrupt downloads):

ssh root@10.89.97.50
cd /opt/arr-stack
docker compose restart gluetun

Download Client Not Connecting

Services behind VPN (SABnzbd, Deluge) are accessed through Gluetun container. Check:

  1. Gluetun is running: docker ps | grep gluetun
  2. VPN connection is up: docker logs gluetun | grep "ip"
  3. Port mappings are correct in docker-compose.yml

Storage/Mount Issues

Verify NAS mount:

ssh root@10.89.97.50
df -h | grep mnt
ls -la /mnt/media

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:

  1. Check Jellyseerr settings:

    ssh root@10.89.97.50
    cat /opt/arr-stack/configs/jellyseerr/settings.json | jq '.radarr[].activeDirectory, .sonarr[].activeDirectory'
    

  2. Fix paths (should show /data/media/...):

    ssh root@10.89.97.50
    sed -i 's|/mnt/media/|/data/media/|g' /opt/arr-stack/configs/jellyseerr/settings.json
    docker restart jellyseerr
    

  3. Fix affected series in Sonarr (via API or UI Settings → Media Management → Root Folders)

  4. Remove incorrect root folders from Sonarr/Radarr

Prevention: Always use /data/media/... paths when configuring Jellyseerr, Overseerr, or directly in Sonarr/Radarr.

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)/"'
3. Trigger library scan after manual copy:
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:

  1. 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"
          }]
        }]
      }'
    

  2. 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"
    

  3. Delete wrong downloads and research:

    # Delete downloads from host filesystem
    ssh root@10.89.97.50
    rm -rf /mnt/media/downloads/Show.Name.S01E01.GERMAN.*
    
    # Trigger new search (see API Operations below)
    

  4. 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:

  1. Identify entries in download client:

    ssh root@10.89.97.50
    # For SABnzbd
    API_KEY=$(grep -oP '(?<=api_key = )[^\n]+' /opt/arr-stack/configs/sabnzbd/sabnzbd.ini)
    curl -s "http://localhost:8080/api?mode=history&output=json&apikey=$API_KEY" | \
      jq '.history.slots[] | select(.name | test("ShowName"; "i")) | {nzo_id, name, status}'
    

  2. 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
    

  3. 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

curl -s "http://localhost:8989/api/v3/customformat" -H "X-Api-Key: $API_KEY" | jq '.[].name'

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

curl -s "http://localhost:8686/api/v1/config/mediamanagement?apikey=$API_KEY" | jq

View Root Folders

curl -s "http://localhost:8686/api/v1/rootfolder?apikey=$API_KEY" | jq

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:

ssh root@10.89.97.50
cd /opt/arr-stack
tar -czf configs-backup-$(date +%Y%m%d).tar.gz configs/

Restore from backup:

ssh root@10.89.97.50
cd /opt/arr-stack
tar -xzf configs-backup-YYYYMMDD.tar.gz
docker compose up -d

Docker Compose backup:

ssh root@10.89.97.50
cp /opt/arr-stack/docker-compose.yml /root/docker-compose.yml.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:

/opt/arr-stack/configs/tdarr/server/Tdarr/DB2/JobReports/{footprintId}/

File naming pattern:

{footprintId}()2.58.02(){jobType}(){workerId}(){timestamp}.txt

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:

ssh root@10.89.97.50
nvidia-smi
# Check if other processes are using the encoder

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)