- Documents recent infrastructure cleanup (8 CTs destroyed, 35 DNS records removed, Headscale cleanup) - Adds 24 new runbooks covering Authentik, PeerTube, Meshtastic, RECON, Proxmox, Mailcow, Internet Archive, GPU routing - Adds project documentation for headscale, vaultwarden, peertube, matrix, mmud, advbbs, arr stack - Updates services.md, environment.md, caddy.md, authentik.md to match live infrastructure - Removes 4 deprecated runbook duplicates (canonical versions live in projects/) - Adds .gitignore for binary archives and editor temp files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9.1 KiB
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.
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
- SSH to
mediaProxmox node. - Find the next available VMID:
pvesh get /cluster/nextid - 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.
- URL:
- Create a VM named
arrwith:- 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.pubor equivalent) - Networking: DHCP or static — check the pattern of other VMs on this node and match it
- Default user:
- Network: bridged to the LAN bridge (likely
- Start the VM, wait for boot, discover and record its LAN IP.
- Verify SSH from cortex → arr VM works.
Phase 2: Base System Setup on arr VM
SSH into the arr VM:
apt update && apt upgrade -y- Install Docker + Docker Compose via the official Docker apt repo for Ubuntu.
- Install NFS client:
apt install -y nfs-common - Install Tailscale and join the tailnet:
curl -fsSL https://tailscale.com/install.sh | shtailscale 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
arrVM.
- Discover appropriate PUID/PGID:
- Mount the NFS share temporarily and
ls -lnto check file ownership. - If no files exist, create a
mediauser/group (e.g., PUID=1000, PGID=1000) and ensure NFS permissions align.
- Mount the NFS share temporarily and
Phase 3: NFS Mount
- Discover the NFS server:
- The NFS export is
/export/arr, accessible from100.64.0.0/10(Tailscale) and192.168.1.0/24(LAN). - Find the NFS server IP by checking:
/etc/fstabon other VMs on this nodeshowmount -e <candidate IPs>on LAN- Proxmox storage config:
pvesm statusor/etc/pve/storage.cfg
- The NFS export is
mkdir -p /mnt/arrmount -t nfs <NFS_SERVER>:/export/arr /mnt/arr- Create subdirectories if they don't exist:
mkdir -p /mnt/arr/{movies,tv,downloads,downloads/complete,downloads/incomplete} - Set ownership to discovered PUID:PGID on all subdirs.
- Add to
/etc/fstabfor persistence:<NFS_SERVER>:/export/arr /mnt/arr nfs defaults,_netdev 0 0 - Verify:
umount /mnt/arr && mount -a && ls /mnt/arr
Phase 4: Docker Containers
Setup
mkdir -p /opt/arr/{jellyfin,jellyseer,sonarr,radarr,prowlarr,sabnzbd}
Create a Docker bridge network for inter-service communication:
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
utilityfor an existing Authentik route (likelyauth.echo6.coorauthentik.echo6.co). - Discover the Authentik API URL and obtain/create an API token from Authentik's docker-compose environment or admin API.
Jellyfin OIDC
- 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
- Name:
- Create Application: Name
Jellyfin, slugjellyfin, attach provider. - Record Client ID + Secret.
- Install SSO-Auth plugin in Jellyfin and configure with Authentik OIDC details (discovery URL, client ID, secret).
Jellyseer OIDC
- 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
- Name:
- Create Application: Name
Jellyseer, slugjellyseer, attach provider. - Record Client ID + Secret.
- 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 <ARR_TAILSCALE_IP>:8096
}
requests.echo6.co {
reverse_proxy <ARR_TAILSCALE_IP>: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
- From
arrVM, curl all six services on localhost (ports 8096, 5055, 8989, 7878, 9696, 8080) curl -sI https://jellyfin.echo6.co→ 200 with valid TLScurl -sI https://requests.echo6.co→ 200 with valid TLS- Authentik OIDC login works for both Jellyfin and Jellyseer
- NFS persists after reboot:
reboot, wait,df -h /mnt/arr - All containers auto-start after reboot:
docker psshows 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.