Skip to content

Latest commit

 

History

History
472 lines (323 loc) · 15.9 KB

File metadata and controls

472 lines (323 loc) · 15.9 KB

Media Server Setup Guide (Advanced)

Everything from the basic setup, plus transcoding, quality profiles, metadata automation, download watchdog, VPN failover, and automated backups.

Time to complete: About 30 minutes


Quick Option: One-Command Install

If you already have OrbStack (or Docker Desktop) and Plex installed, you can run a single command that handles the core setup:

curl -fsSL https://raw.githubusercontent.com/liamvibecodes/mac-media-stack-advanced/main/bootstrap.sh | bash

It will prompt you for VPN keys, configure all core services, auto-wire Recyclarr + Unpackerr API keys, install native Tdarr on macOS, preload the quality-first Tdarr flow, and install automation jobs. You'll still need Step 7 for Kometa and final Tdarr library mapping.

To run from a local clone with custom paths:

bash bootstrap.sh --media-dir /Volumes/T9/Media --install-dir ~/mac-media-stack-advanced

Already on an older clone and want the newest release tag:

bash <(curl -fsSL https://raw.githubusercontent.com/liamvibecodes/mac-media-stack-advanced/main/scripts/update-to-latest-release.sh)

Already running the basic stack and want an in-place migration? Use UPGRADE.md.


Prerequisites

  • A Mac (any recent macOS)
  • At least 50GB free disk space (media libraries will need more)
  • OrbStack (recommended) or Docker Desktop installed and running
  • Plex installed and signed in, OR use Jellyfin (runs in Docker, no install needed)
  • ProtonVPN WireGuard credentials
  • A free TMDB API key (https://www.themoviedb.org/settings/api) (Plex/Kometa only)

Why OrbStack? It starts in ~2 seconds (vs 30s for Docker Desktop), uses ~1GB RAM (vs 4GB), and has 2-10x faster file I/O. It's a drop-in replacement that runs the same Docker commands. Docker Desktop works fine too.


Step 1: Install a Container Runtime

You need a container runtime to run the behind-the-scenes services. Pick one:

Option A: OrbStack (Recommended)

OrbStack is faster and lighter than Docker Desktop (~2s startup, ~1GB RAM).

brew install --cask orbstack

Or download from https://orbstack.dev. Open it once after installing.

Option B: Docker Desktop

  1. Go to https://www.docker.com/products/docker-desktop/
  2. Click "Download for Mac"
    • If you have an M-series Mac (M1, M2, M3, M4): choose "Apple Silicon"
    • If you're not sure, click the Apple icon top-left of your screen > "About This Mac" and check the chip
  3. Open the downloaded .dmg file
  4. Drag Docker to your Applications folder
  5. Open Docker Desktop from Applications
  6. It will ask for your password to install components. Enter it.
  7. Wait for it to finish starting (the whale icon in your menu bar will stop animating)
  8. In Docker Desktop settings (gear icon), go to "General" and make sure "Start Docker Desktop when you sign in" is checked

Both options use the same docker and docker compose commands. Everything in this guide works identically with either one.

If you use a custom media location (MEDIA_DIR in .env), replace any ~/Media path below with that value.


Choose Your Media Server

This stack supports two media servers. Choose one:

Plex (default): Runs natively on macOS. Requires the Plex app installed. Supports Kometa metadata automation and franchise sorting.

Jellyfin: Free and open-source. Runs entirely in Docker. No app install needed. Kometa and franchise-sort are skipped automatically (Jellyfin has built-in collection management).

To use Jellyfin, pass --jellyfin to the bootstrap command, or set MEDIA_SERVER=jellyfin in your .env file.

Tdarr Mode

TDARR_MODE=native is the default and recommended on macOS (more reliable for hardware decode/encode behavior and avoids Docker VM overhead).
If you prefer containerized Tdarr, set TDARR_MODE=docker in .env or use --tdarr-docker with bootstrap.sh.


Step 2: Download and Setup

cd ~
git clone https://github.com/liamvibecodes/mac-media-stack-advanced.git
cd mac-media-stack-advanced
bash scripts/setup.sh
# or:
# bash scripts/setup.sh --media-dir /Volumes/T9/Media

Step 3: Add VPN Keys

open -a TextEdit .env

Fill in WIREGUARD_PRIVATE_KEY and WIREGUARD_ADDRESSES from your ProtonVPN account.

Get your WireGuard private key from https://account.protonvpn.com/downloads#wireguard-configuration


Step 4: Start the Stack

Run preflight checks before first startup:

bash scripts/doctor.sh

Then start the stack:

docker compose up -d
bash scripts/health-check.sh

If using Jellyfin, start with the profile enabled:

docker compose --profile jellyfin up -d

If using Docker Tdarr (TDARR_MODE=docker), also enable the Tdarr profile:

docker compose --profile tdarr-docker up -d

If using native Tdarr (TDARR_MODE=native, default), run:

bash scripts/setup-tdarr-native.sh

Wait for all containers to show OK. First pull takes 3-5 GB.

Optional: enable automatic container updates (Watchtower):

docker compose --profile autoupdate up -d watchtower

Verify Your VPN Kill Switch

Once the stack is running, confirm your real IP is never exposed through the VPN tunnel:

# 1. Check the VPN's IP (should be your VPN provider, not your ISP)
docker exec gluetun sh -c 'wget -qO- https://ipinfo.io/ip'

# 2. Check your real IP (run outside Docker)
curl -s https://ipinfo.io/ip

# 3. Confirm they're different

To test the kill switch (traffic should be blocked when the VPN drops):

# Stop the VPN container
docker stop gluetun

# Try reaching the internet from qBittorrent — should fail/timeout
# (qBittorrent is routed through gluetun and should have no network when gluetun is down)
docker exec qbittorrent wget -qO- --timeout=5 https://ipinfo.io/ip 2>&1 || echo "Kill switch works: qBittorrent has no network"

# Restore the VPN
docker start gluetun

If the second wget returns an IP instead of timing out, your kill switch isn't working. Check your Gluetun configuration.

macOS note: On macOS, Docker runs inside a Linux VM (OrbStack or Docker Desktop). The kill switch blocks traffic at the container/VM level, not at the macOS network layer. This means containers routed through Gluetun are protected, but apps running directly on macOS are not affected. This is normal and expected.


Step 5: Auto-Configure Services

bash scripts/configure.sh

This configures qBittorrent, Prowlarr (indexers), Radarr, Sonarr, and Seerr. It will print your API keys at the end. Save them. It also writes credentials/API keys to <MEDIA_DIR>/state/first-run-credentials.txt (mode 600, default path ~/Media/state/first-run-credentials.txt).


Step 6: Set Up Media Server Libraries

Plex

  1. Open http://localhost:32400/web
  2. Settings > Libraries > Add Library
  3. Add Movies (your home folder > Media > Movies)
  4. Add TV Shows (your home folder > Media > TV Shows)

Jellyfin

  1. Open http://localhost:8096
  2. Complete the setup wizard
  3. Add libraries: Movies = /data/movies, TV Shows = /data/tvshows
  4. Generate an API key (Administration > API Keys) for Jellystat and archive-media.sh --only-watched

Plugins: configure.sh auto-installs Intro Skipper (adds "Skip Intro" button on TV shows) and TMDb Box Sets (auto-creates franchise collections). A Jellyfin restart is required after plugin install. If auto-install fails, install them manually via Administration > Plugins > Catalog.

Jellystat (analytics): Open http://localhost:3000, create an account, and connect to http://jellyfin:8096 with your API key. Tracks watch history, user activity, and library stats.


Step 7: Configure Advanced Services

Jellyfin users: Skip the Kometa section below. Kometa is Plex-only and is automatically skipped when MEDIA_SERVER=jellyfin. Franchise sorting is also Plex-only; Jellyfin has built-in collection management via the Collections plugin.

Recyclarr (TRaSH quality profiles)

scripts/configure.sh now auto-injects your Sonarr/Radarr API keys into <MEDIA_DIR>/config/recyclarr/recyclarr.yml (default: ~/Media/config/recyclarr/recyclarr.yml). You only need to review it if you want to customize profile behavior.

Recyclarr runs automatically at 3am daily. To trigger a manual sync:

docker compose run --rm recyclarr sync

Kometa (Plex metadata)

Open the Kometa config and add your Plex token and TMDB API key:

open -a TextEdit ~/Media/config/kometa/config.yml

Replace YOUR_PLEX_TOKEN and YOUR_TMDB_API_KEY, then save.

Tdarr (transcoding)

  1. Open http://localhost:8265
  2. Add libraries:
    • Native mode (TDARR_MODE=native):
      • Movies source: <MEDIA_DIR>/Movies
      • TV source: <MEDIA_DIR>/TV Shows
      • Temp/cache: <MEDIA_DIR>/tdarr-transcode-cache
    • Docker mode (TDARR_MODE=docker):
      • Movies source: /movies
      • TV source: /tv
      • Temp/cache: /temp
    • If CLOUD_STORAGE_ENABLED=true, use TDARR_MODE=docker so Tdarr can read merged cloud paths.
  3. Assign the preloaded flow: Quality-First HEVC (Resolution Preserving)
  4. Keep the defaults in that flow for quality-first behavior:
    • No resolution downscale
    • No hard bitrate cap
    • H.264 -> H.265 (CRF 19, preset slow)
    • Replace only when output size ratio is 25-99%

Unpackerr

scripts/configure.sh now auto-writes UN_SONARR_0_API_KEY and UN_RADARR_0_API_KEY in .env and restarts Unpackerr. No manual edit is required unless you want to override defaults.


Step 8: Install Automation Jobs

bash scripts/install-launchd-jobs.sh

This installs:

  • Auto-healer (hourly VPN/container health check + restart)
  • Nightly backup (configs + databases, 14-day retention)
  • Log prune (daily cleanup, removes logs older than 30 days)
  • Download watchdog (stalled torrent auto-fix every 15 min)
  • Kometa scheduler (metadata refresh every 4 hours)
  • Native Tdarr launchd services (when TDARR_MODE=native)

Automation logs go to <MEDIA_DIR>/logs/ and launchd stdout/stderr logs go to <MEDIA_DIR>/logs/launchd/ (default ~/Media/...).

Optional: VPN Failover

If you have a NordVPN account as backup:

  1. Copy .env.nord.example to .env.nord
  2. Add your NordVPN WireGuard private key
  3. Install the failover watcher:
bash scripts/install-vpn-failover.sh

This checks every 2 minutes and auto-switches between Proton and Nord after 3 consecutive failures. Use docker-compose.nord-fallback.yml only for Nord mode; Proton remains the default compose path.

Check current provider anytime:

bash scripts/vpn-mode.sh status

Day-to-Day Usage

What Where
Browse and request http://localhost:5055
Watch (Plex) http://localhost:32400/web
Watch (Jellyfin) http://localhost:8096
Jellyfin analytics (Jellystat) http://localhost:3000
Check downloads http://localhost:8080
Transcode status http://localhost:8265

Everything else is fully automated.


Optional: Cloud / NAS Storage (rclone + mergerfs)

If your local disk is too small for your full library, or you have a NAS with plenty of space, remote storage lets you extend your library transparently. rclone mounts your cloud provider or NAS (via SFTP) inside Docker, and mergerfs overlays it with your local storage so all services see one unified library.

Compatibility (macOS)

  • Cloud-backed playback requires MEDIA_SERVER=jellyfin.
  • If using Tdarr with cloud storage, use TDARR_MODE=docker.
  • Native macOS apps (Plex app, native Tdarr) cannot directly read Docker FUSE merged mounts.

Setup

bash scripts/setup-cloud-storage.sh

The setup wizard will:

  1. Create the required directories
  2. Help you configure an rclone remote (interactive Docker wizard, copy existing conf, or skip)
  3. Write CLOUD_STORAGE_ENABLED=true and RCLONE_REMOTE to your .env
  4. Create Movies and TV Shows folders on your remote

Start with cloud storage

docker compose -f docker-compose.yml -f docker-compose.cloud-storage.yml --profile cloud-storage --profile jellyfin --profile tdarr-docker up -d

Or pass --jellyfin --cloud-storage to bootstrap.sh for a full install.

NAS Setup (TrueNAS, Synology, Unraid)

bash scripts/setup-cloud-storage.sh --storage-type nas

The wizard will:

  1. Ask for your NAS hostname/IP and SSH username
  2. Offer to generate an SSH key pair (recommended), use an existing key, or use password auth
  3. Ask for your media path with platform-specific examples
  4. Auto-detect Synology paths and add path_override (same effect as --sftp-path-override) for SFTP chroot compatibility
  5. Test connectivity before writing the config
  6. Write NAS-optimized VFS cache settings to .env

SSH key setup: If you generate a new key, the wizard prints the public key. Add it to your NAS:

  • TrueNAS: Accounts > Users > [user] > SSH Public Key
  • Synology: DSM > Control Panel > User > [user] > Edit > SSH Key
  • Unraid: Tools > User SSH Keys, or append to ~/.ssh/authorized_keys

Start with NAS storage:

docker compose -f docker-compose.yml -f docker-compose.cloud-storage.yml --profile cloud-storage --profile jellyfin --profile tdarr-docker up -d

Or use bootstrap: bash bootstrap.sh --jellyfin --nas-storage

How it works

  • Write path: Downloads land on local disk (fast). cloud-upload.sh periodically moves stable files to remote storage (cloud default: every 6h; NAS default: every 2h).
  • Read path: Docker services (Jellyfin, Arr apps, Tdarr Docker) read from the mergerfs overlay, which transparently serves files from local or remote storage.
  • VFS cache: rclone caches recently accessed remote files locally. Cloud defaults are larger (50GB/72h), NAS defaults are smaller/faster (10GB/1h).

Library paths when cloud storage is enabled

  • Jellyfin (Docker): keep default container paths /data/movies and /data/tvshows (mapped to merged paths by compose override).
  • Tdarr Docker mode: keep /movies and /tv (mapped to merged paths by compose override).

Troubleshooting cloud storage

# Check rclone and mergerfs health
bash scripts/health-check.sh

# View cloud upload logs
tail -50 ~/Media/logs/cloud-upload.log

# Test rclone connectivity
docker exec rclone-mount rclone ls myremote: --max-depth 1

# Restart cloud storage (order matters: rclone first, then mergerfs)
docker restart rclone-mount && sleep 15 && docker restart mergerfs

NAS-specific troubleshooting

# Verify SSH key permissions
ls -la ~/Media/config/rclone/nas_key.pem
# Should show -rw------- (600)

# Test NAS connectivity directly
docker run --rm -v ~/Media/config/rclone:/config/rclone rclone/rclone lsd mynas: --contimeout 10s

# Check SFTP connection speed
docker run --rm -v ~/Media/config/rclone:/config/rclone rclone/rclone test bandwidth mynas: --duration 10s

# Synology: if paths don't resolve, check sftp_path_override in rclone.conf

Troubleshooting

Check overall health:

bash scripts/health-check.sh

View automation logs:

tail -50 ~/Media/logs/auto-heal.log
tail -50 ~/Media/logs/download-watchdog.log
tail -50 ~/Media/logs/vpn-failover.log
tail -50 ~/Media/logs/log-prune.log

Manual VPN switch:

bash scripts/vpn-mode.sh status    # check current provider
bash scripts/vpn-mode.sh proton    # switch to Proton
bash scripts/vpn-mode.sh nord      # switch to Nord

Restart everything:

docker compose down && docker compose up -d
# If using Jellyfin:
docker compose down && docker compose --profile jellyfin up -d

Uninstall automation jobs:

for f in ~/Library/LaunchAgents/com.media-stack.*.plist; do launchctl unload "$f" && rm "$f"; done