# CC Runbook: Build ARR Media Stack on Proxmox `media` Node ## Objective Build a complete media automation stack on the Proxmox node `media` inside a single Ubuntu VM called `arr`. Each service runs in its own Docker container with a shared bridge network for inter-service communication. All services are exposed on the VM's LAN IP on their respective ports. **Services:** - Jellyfin (media server, software transcoding — no GPU) - Jellyseer (request management) - Sonarr (TV automation) - Radarr (Movie automation) - Prowlarr (indexer manager) - SABnzbd (Usenet download client) --- ## Phase 0: SSH Prereq Check **CRITICAL — Do this first. Do not skip.** ```bash ssh media "echo 'SSH OK to media node'" ``` If this fails, stop and fix SSH access before proceeding. Use sshpass or key auth per `~/.ssh/config`. Cortex is the management host — all commands originate from here. --- ## Phase 1: Create Ubuntu VM on `media` 1. SSH to `media` Proxmox node. 2. Find the next available VMID: `pvesh get /cluster/nextid` 3. Download Ubuntu 24.04 cloud image if not already cached: - URL: `https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img` - Store in appropriate Proxmox storage. 4. Create a VM named `arr` with: - **Network:** bridged to the LAN bridge (likely `vmbr0`) - **Resource allocation:** Decide based on the combined needs of all six services. Jellyfin (software transcoding) and SABnzbd (decompression) are the heaviest. Sonarr/Radarr/Prowlarr/Jellyseer are lightweight. Size the VM accordingly — suggest at minimum 4 cores and 8GB RAM, but use your judgment. - **Disk:** 30GB for OS + container configs (media lives on NFS) - Cloud-init configured with: - Default user: `zvx` - SSH key from cortex (discover from `~/.ssh/id_rsa.pub` or equivalent) - Networking: DHCP or static — check the pattern of other VMs on this node and match it 5. Start the VM, wait for boot, discover and record its LAN IP. 6. Verify SSH from cortex → arr VM works. --- ## Phase 2: Base System Setup on `arr` VM SSH into the `arr` VM: 1. `apt update && apt upgrade -y` 2. Install Docker + Docker Compose via the official Docker apt repo for Ubuntu. 3. Install NFS client: `apt install -y nfs-common` 4. Install Tailscale and join the tailnet: - `curl -fsSL https://tailscale.com/install.sh | sh` - `tailscale up` — use an auth key if available. Check how other VMs joined (look at Headscale config if self-hosted). - Record the Tailscale IP of the `arr` VM. 5. Discover appropriate PUID/PGID: - Mount the NFS share temporarily and `ls -ln` to check file ownership. - If no files exist, create a `media` user/group (e.g., PUID=1000, PGID=1000) and ensure NFS permissions align. --- ## Phase 3: NFS Mount 1. **Discover the NFS server:** - The NFS export is `/export/arr`, accessible from `100.64.0.0/10` (Tailscale) and `192.168.1.0/24` (LAN). - Find the NFS server IP by checking: - `/etc/fstab` on other VMs on this node - `showmount -e ` on LAN - Proxmox storage config: `pvesm status` or `/etc/pve/storage.cfg` 2. `mkdir -p /mnt/arr` 3. `mount -t nfs :/export/arr /mnt/arr` 4. Create subdirectories if they don't exist: ``` mkdir -p /mnt/arr/{movies,tv,downloads,downloads/complete,downloads/incomplete} ``` 5. Set ownership to discovered PUID:PGID on all subdirs. 6. Add to `/etc/fstab` for persistence: ``` :/export/arr /mnt/arr nfs defaults,_netdev 0 0 ``` 7. Verify: `umount /mnt/arr && mount -a && ls /mnt/arr` --- ## Phase 4: Docker Containers ### Setup ```bash mkdir -p /opt/arr/{jellyfin,jellyseer,sonarr,radarr,prowlarr,sabnzbd} ``` Create a Docker bridge network for inter-service communication: ```bash docker network create arr-net ``` ### Container Deployment Deploy each service as its own standalone container. All containers join `arr-net`. All get `TZ=America/Boise` and the discovered `PUID`/`PGID`. **Decide per-container resource limits** (CPU shares, memory limits) based on service needs: - **Heavy:** Jellyfin (transcoding), SABnzbd (decompression) — allocate more CPU/RAM - **Medium:** Sonarr, Radarr — moderate - **Light:** Prowlarr, Jellyseer — minimal Use lightweight images (hotio where available, official otherwise). #### Jellyfin - Image: `jellyfin/jellyfin:latest` - Container name: `jellyfin` - Port: `8096:8096` - Volumes: - `/opt/arr/jellyfin/config:/config` - `/mnt/arr/movies:/data/movies:ro` - `/mnt/arr/tv:/data/tv:ro` - Network: `arr-net` - Restart: `unless-stopped` #### Jellyseer - Image: `fallenbagel/jellyseer:latest` - Container name: `jellyseer` - Port: `5055:5055` - Volumes: - `/opt/arr/jellyseer/config:/app/config` - Network: `arr-net` - Restart: `unless-stopped` #### Sonarr - Image: `ghcr.io/hotio/sonarr:latest` - Container name: `sonarr` - Port: `8989:8989` - Volumes: - `/opt/arr/sonarr/config:/config` - `/mnt/arr:/data` - Network: `arr-net` - Restart: `unless-stopped` #### Radarr - Image: `ghcr.io/hotio/radarr:latest` - Container name: `radarr` - Port: `7878:7878` - Volumes: - `/opt/arr/radarr/config:/config` - `/mnt/arr:/data` - Network: `arr-net` - Restart: `unless-stopped` #### Prowlarr - Image: `ghcr.io/hotio/prowlarr:latest` - Container name: `prowlarr` - Port: `9696:9696` - Volumes: - `/opt/arr/prowlarr/config:/config` - Network: `arr-net` - Restart: `unless-stopped` #### SABnzbd - Image: `ghcr.io/hotio/sabnzbd:latest` - Container name: `sabnzbd` - Port: `8080:8080` - Volumes: - `/opt/arr/sabnzbd/config:/config` - `/mnt/arr/downloads:/data/downloads` - Network: `arr-net` - Restart: `unless-stopped` ### Volume Mapping Design Sonarr and Radarr both map `/mnt/arr:/data` so hardlinks/atomic moves work between `/data/downloads/complete` and `/data/movies` or `/data/tv` without cross-filesystem copies. This is critical for avoiding double disk usage. ### Verify All six containers are running: `docker ps` Curl each service on localhost to confirm they respond on their expected ports. --- ## Phase 5: Authentik OIDC Setup **Discovery:** Find the Authentik instance. - Check Caddy config on `utility` for an existing Authentik route (likely `auth.echo6.co` or `authentik.echo6.co`). - Discover the Authentik API URL and obtain/create an API token from Authentik's docker-compose environment or admin API. ### Jellyfin OIDC 1. Create OAuth2/OpenID Provider in Authentik: - Name: `jellyfin`, Client type: Confidential - Redirect URI: `https://jellyfin.echo6.co/sso/OID/redirect/Authentik` - Scopes: `openid profile email` - Signing key: use existing or create 2. Create Application: Name `Jellyfin`, slug `jellyfin`, attach provider. 3. Record Client ID + Secret. 4. Install SSO-Auth plugin in Jellyfin and configure with Authentik OIDC details (discovery URL, client ID, secret). ### Jellyseer OIDC 1. Create OAuth2/OpenID Provider in Authentik: - Name: `jellyseer`, Client type: Confidential - Redirect URI: `https://requests.echo6.co/api/v1/auth/oidc-callback` (verify actual callback path from Jellyseer docs) - Scopes: `openid profile email` 2. Create Application: Name `Jellyseer`, slug `jellyseer`, attach provider. 3. Record Client ID + Secret. 4. Configure Jellyseer OIDC via its settings. --- ## Phase 6: Caddy Reverse Proxy on `utility` SSH to `utility`. Discover the Caddyfile location and how Caddy is managed (docker, systemd, etc.). Add entries using the **Tailscale IP** of the `arr` VM as the upstream: ``` jellyfin.echo6.co { reverse_proxy :8096 } requests.echo6.co { reverse_proxy :5055 } ``` **Do NOT expose Sonarr, Radarr, Prowlarr, or SABnzbd via Caddy.** Those are internal-only, accessible via Tailscale or LAN. Reload Caddy. --- ## Phase 7: GoDaddy DNS **Discovery:** Check if GoDaddy API key/secret exists on cortex or utility. Look at how existing `echo6.co` subdomains are configured for the pattern. Create A records (via API if available, otherwise output for manual creation): | Type | Name | Value | TTL | |------|------|-------|-----| | A | `jellyfin` | Public IP of Caddy/utility (discover) | 600 | | A | `requests` | Public IP of Caddy/utility (discover) | 600 | These are publicly exposed WITHOUT Tailscale. Caddy handles TLS via Let's Encrypt. The upstream uses the Tailscale IP but DNS points to the public-facing Caddy IP. --- ## Phase 8: Validation 1. From `arr` VM, curl all six services on localhost (ports 8096, 5055, 8989, 7878, 9696, 8080) 2. `curl -sI https://jellyfin.echo6.co` → 200 with valid TLS 3. `curl -sI https://requests.echo6.co` → 200 with valid TLS 4. Authentik OIDC login works for both Jellyfin and Jellyseer 5. NFS persists after reboot: `reboot`, wait, `df -h /mnt/arr` 6. All containers auto-start after reboot: `docker ps` shows all six running --- ## Important Notes - **Do NOT configure** Prowlarr indexers, Sonarr/Radarr API connections, or SABnzbd Usenet provider credentials. That will be done in a separate prompt. - **All discovery steps are intentional** — do not hardcode IPs or paths. Find them dynamically from the running infrastructure. - **If any phase fails, stop and report the error.** Do not skip phases.