diff --git a/.env.example b/.env.example index 644c79f..ca55685 100644 --- a/.env.example +++ b/.env.example @@ -17,12 +17,19 @@ ND_SCANSCHEDULE=1h ND_LOGLEVEL=info ND_SESSIONTIMEOUT=24h -# Transmission (optional auth) +# qBittorrent (default download client) +# Default login: admin / adminadmin (change in WebUI after first login) + +# Transmission (legacy - use with --profile legacy) TRANSMISSION_USER=admin TRANSMISSION_PASS=changeme -# VPN (optional - for download privacy) -VPN_ENABLED=false +# VPN Configuration (optional - use with --profile vpn) +# Get Wireguard config from your VPN provider VPN_PROVIDER=mullvad -VPN_USER= -VPN_PASS= +VPN_WIREGUARD_PRIVATE_KEY= +VPN_WIREGUARD_ADDRESS= +VPN_COUNTRY=Germany + +# Jellyseerr +# Configure via web UI at https://requests.jeffemmett.com diff --git a/README.md b/README.md index b8af8c1..a6e1e2f 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,58 @@ # Media Server on Netcup RS 8000 -A self-hosted media server stack running on Netcup RS 8000 with Jellyfin for streaming. +A self-hosted media server stack with automated request management, running on Netcup RS 8000 with Jellyfin for streaming. ## Architecture ``` + Users + │ + ▼ ┌─────────────────────────────────────────────────────────────┐ -│ Netcup RS 8000 │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Jellyfin │ │ Sonarr │ │ Radarr │ │ -│ │ (Stream) │ │ (TV) │ │ (Movies) │ │ -│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ -│ │ │ │ │ -│ └────────────────┼────────────────┘ │ -│ │ │ -│ ┌───────┴───────┐ │ -│ │ Local NVMe │ │ -│ │ Storage │ │ -│ │ (3TB) │ │ -│ └───────────────┘ │ +│ Cloudflare Tunnel → Traefik │ +└─────────────────────────────────────────────────────────────┘ + │ + ┌─────────────┼─────────────┐ + ▼ ▼ ▼ +┌───────────┐ ┌───────────┐ ┌───────────┐ +│ Jellyfin │ │Jellyseerr │ │ Navidrome │ +│ (Stream) │ │(Requests) │ │ (Music) │ +└─────┬─────┘ └─────┬─────┘ └───────────┘ + │ │ + │ ▼ + │ ┌─────────────────────┐ + │ │ Request Flow │ + │ │ ┌───────────────┐ │ + │ │ │ Sonarr │ │ ← TV Shows + │ │ │ Radarr │ │ ← Movies + │ │ │ Lidarr │ │ ← Music + │ │ └───────┬───────┘ │ + │ │ │ │ + │ │ ┌───────▼───────┐ │ + │ │ │ Prowlarr │ │ ← Indexer Management + │ │ └───────┬───────┘ │ + │ │ │ │ + │ │ ┌───────▼───────┐ │ + │ │ │ qBittorrent │ │ ← Downloads (optional VPN) + │ │ └───────┬───────┘ │ + │ └──────────┼──────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Local NVMe Storage (3TB) │ +│ /media/movies /media/shows /media/music │ └─────────────────────────────────────────────────────────────┘ ``` +## How It Works + +1. **Users request media** via Jellyseerr (`requests.jeffemmett.com`) +2. **Jellyseerr forwards** requests to Sonarr/Radarr/Lidarr +3. **Prowlarr searches** configured indexers for the content +4. **qBittorrent downloads** the files (optionally through VPN) +5. **\*arr apps organize** files into proper folders +6. **Jellyfin detects** new media and makes it available for streaming + ## Server Specs - **Netcup RS 8000 G12 Pro**: €45/month @@ -35,8 +66,8 @@ A self-hosted media server stack running on Netcup RS 8000 with Jellyfin for str ### 1. Clone and Configure ```bash -git clone https://gitea.jeffemmett.com/jeffemmett/plex-media.git -cd plex-media +git clone https://gitea.jeffemmett.com/jeffemmett/jellyfin-media.git +cd jellyfin-media cp .env.example .env ``` @@ -46,55 +77,132 @@ cp .env.example .env ./scripts/deploy-to-netcup.sh ``` -### 3. Upload Your Media +### 3. Add Cloudflare Tunnel Hostnames -```bash -# Upload TV shows -./scripts/upload-to-netcup.sh /home/jeffe/Shows shows +Add these hostnames to your Cloudflare tunnel config (`/root/cloudflared/config.yml`): -# Upload movies -./scripts/upload-to-netcup.sh /path/to/movies movies +```yaml +# Media Request System +- hostname: requests.jeffemmett.com + service: http://localhost:80 +- hostname: sonarr.jeffemmett.com + service: http://localhost:80 +- hostname: radarr.jeffemmett.com + service: http://localhost:80 +- hostname: prowlarr.jeffemmett.com + service: http://localhost:80 +- hostname: lidarr.jeffemmett.com + service: http://localhost:80 +- hostname: downloads.jeffemmett.com + service: http://localhost:80 ``` +Then restart cloudflared: `docker restart cloudflared` + +### 4. Configure Services (First Time Setup) + +#### Step 1: Configure Prowlarr (Indexers) +1. Go to `https://prowlarr.jeffemmett.com` +2. Add indexers (torrent sites you have access to) +3. Go to Settings → Apps → Add Radarr, Sonarr, Lidarr + +#### Step 2: Configure Download Client +1. Go to `https://downloads.jeffemmett.com` +2. Default login: `admin` / `adminadmin` (change immediately!) +3. Settings → Downloads → Default Save Path: `/downloads` + +#### Step 3: Configure Radarr (Movies) +1. Go to `https://radarr.jeffemmett.com` +2. Settings → Media Management → Root Folder: `/movies` +3. Settings → Download Clients → Add qBittorrent: + - Host: `qbittorrent` + - Port: `8080` + +#### Step 4: Configure Sonarr (TV Shows) +1. Go to `https://sonarr.jeffemmett.com` +2. Settings → Media Management → Root Folder: `/tv` +3. Settings → Download Clients → Add qBittorrent (same as above) + +#### Step 5: Configure Lidarr (Music) +1. Go to `https://lidarr.jeffemmett.com` +2. Settings → Media Management → Root Folder: `/music` +3. Settings → Download Clients → Add qBittorrent (same as above) + +#### Step 6: Configure Jellyseerr (Requests) +1. Go to `https://requests.jeffemmett.com` +2. Sign in with Jellyfin (use your Jellyfin server URL: `http://jellyfin:8096`) +3. Add Radarr/Sonarr servers: + - Hostname: `radarr` or `sonarr` (internal Docker hostname) + - Port: `7878` (Radarr) or `8989` (Sonarr) + - API Key: Get from Radarr/Sonarr Settings → General + ## Services -| Service | Port | Description | -|---------|------|-------------| -| Jellyfin | 8096 | Video streaming (movies & TV) | -| Navidrome | 4533 | Music streaming server | -| Sonarr | 8989 | TV show management | -| Radarr | 7878 | Movie management | -| Prowlarr | 9696 | Indexer management | -| Transmission | 9091 | Download client | +| Service | URL | Description | +|---------|-----|-------------| +| Jellyfin | https://movies.jeffemmett.com | Video streaming (movies & TV) | +| Navidrome | https://music.jeffemmett.com | Music streaming server | +| Jellyseerr | https://requests.jeffemmett.com | Media request interface | +| Sonarr | https://sonarr.jeffemmett.com | TV show management | +| Radarr | https://radarr.jeffemmett.com | Movie management | +| Lidarr | https://lidarr.jeffemmett.com | Music management | +| Prowlarr | https://prowlarr.jeffemmett.com | Indexer management | +| qBittorrent | https://downloads.jeffemmett.com | Download client | -## Access +## Security Recommendations -All services accessible via Cloudflare Tunnel: -- **Movies & TV**: https://movies.jeffemmett.com (Jellyfin) -- **Music**: https://music.jeffemmett.com (Navidrome) +**Important:** The \*arr admin interfaces should be protected. Options: -### Navidrome Mobile Apps +1. **Cloudflare Access** (Recommended): Add authentication via Zero Trust dashboard +2. **SSH Tunnel**: Access admin UIs only via `ssh -L 8989:localhost:8989 netcup` +3. **Basic Auth**: Add Traefik middleware for HTTP basic auth -Navidrome is Subsonic-compatible. Use any Subsonic client: -- **Android**: Ultrasonic, Symfonium, DSub -- **iOS**: play:Sub, Amperfy, SubStreamer -- **Desktop**: Sonixd, Sublime Music +To add Cloudflare Access protection: +1. Go to Cloudflare Zero Trust → Access → Applications +2. Create application for each admin subdomain +3. Add authentication policy (email, one-time PIN, etc.) + +## VPN Support + +For download privacy, enable the VPN profile: + +```bash +# Add to .env: +VPN_PROVIDER=mullvad +VPN_WIREGUARD_PRIVATE_KEY=your_private_key +VPN_WIREGUARD_ADDRESS=10.x.x.x/32 + +# Start with VPN: +docker compose --profile vpn up -d +``` + +Then modify qBittorrent to route through gluetun: +```yaml +# In docker-compose-server.yml, uncomment: +network_mode: "service:gluetun" +# And comment out the networks/labels sections +``` ## Folder Structure ``` /opt/media-server/ ├── media/ -│ ├── movies/ # Movie files -│ ├── shows/ # TV show files -│ └── music/ # Music files +│ ├── movies/ # Movie files +│ ├── shows/ # TV show files +│ └── music/ # Music files ├── config/ -│ ├── jellyfin/ # Jellyfin config -│ ├── sonarr/ # Sonarr config -│ ├── radarr/ # Radarr config -│ └── prowlarr/ # Prowlarr config -└── downloads/ - └── complete/ # Completed downloads +│ ├── jellyfin/ # Jellyfin config +│ ├── jellyseerr/ # Jellyseerr config +│ ├── sonarr/ # Sonarr config +│ ├── radarr/ # Radarr config +│ ├── lidarr/ # Lidarr config +│ ├── prowlarr/ # Prowlarr config +│ └── qbittorrent/ # qBittorrent config +├── downloads/ +│ └── complete/ # Completed downloads +└── cache/ + └── jellyfin/ # Transcoding cache ``` ## Upload Script Usage @@ -111,6 +219,39 @@ Navidrome is Subsonic-compatible. Use any Subsonic client: The script uses rsync for efficient incremental uploads. +## Mobile Apps + +### Jellyfin (Video) +- **Android**: Jellyfin for Android +- **iOS**: Swiftfin, Jellyfin Mobile + +### Navidrome (Music) +Navidrome is Subsonic-compatible. Use any Subsonic client: +- **Android**: Ultrasonic, Symfonium, DSub +- **iOS**: play:Sub, Amperfy, SubStreamer +- **Desktop**: Sonixd, Sublime Music + +## Troubleshooting + +### Downloads not starting +1. Check Prowlarr has working indexers +2. Verify qBittorrent is accessible: `docker logs qbittorrent` +3. Check Radarr/Sonarr logs for errors + +### Media not appearing in Jellyfin +1. Verify files are in correct folder (`/media/movies`, `/media/shows`) +2. Trigger library scan in Jellyfin → Dashboard → Libraries +3. Check file permissions: should be owned by PUID:PGID (1000:1000) + +### VPN connection issues +```bash +# Check gluetun logs +docker logs gluetun + +# Verify VPN is working +docker exec gluetun curl ifconfig.me +``` + ## License MIT diff --git a/docker-compose-server.yml b/docker-compose-server.yml index fe7c606..f166c9c 100644 --- a/docker-compose-server.yml +++ b/docker-compose-server.yml @@ -23,6 +23,28 @@ services: - "traefik.http.middlewares.jellyfin-headers.headers.customRequestHeaders.X-Forwarded-Proto=https" - "traefik.docker.network=traefik-public" + # Request Management - User-facing interface for media requests + jellyseerr: + image: fallenbagel/jellyseerr:latest + container_name: jellyseerr + restart: unless-stopped + environment: + - LOG_LEVEL=debug + - TZ=Europe/Berlin + volumes: + - ./config/jellyseerr:/app/config + networks: + - media-network + - traefik-public + labels: + - "traefik.enable=true" + - "traefik.http.routers.jellyseerr.rule=Host(`requests.jeffemmett.com`)" + - "traefik.http.routers.jellyseerr.entrypoints=web" + - "traefik.http.routers.jellyseerr.middlewares=jellyseerr-headers" + - "traefik.http.services.jellyseerr.loadbalancer.server.port=5055" + - "traefik.http.middlewares.jellyseerr-headers.headers.customRequestHeaders.X-Forwarded-Proto=https" + - "traefik.docker.network=traefik-public" + navidrome: image: deluan/navidrome:latest container_name: navidrome @@ -63,10 +85,15 @@ services: - ./config/sonarr:/config - ./media/shows:/tv - ./downloads:/downloads - ports: - - 8989:8989 networks: - media-network + - traefik-public + labels: + - "traefik.enable=true" + - "traefik.http.routers.sonarr.rule=Host(`sonarr.jeffemmett.com`)" + - "traefik.http.routers.sonarr.entrypoints=web" + - "traefik.http.services.sonarr.loadbalancer.server.port=8989" + - "traefik.docker.network=traefik-public" radarr: image: linuxserver/radarr:latest @@ -80,10 +107,15 @@ services: - ./config/radarr:/config - ./media/movies:/movies - ./downloads:/downloads - ports: - - 7878:7878 networks: - media-network + - traefik-public + labels: + - "traefik.enable=true" + - "traefik.http.routers.radarr.rule=Host(`radarr.jeffemmett.com`)" + - "traefik.http.routers.radarr.entrypoints=web" + - "traefik.http.services.radarr.loadbalancer.server.port=7878" + - "traefik.docker.network=traefik-public" prowlarr: image: linuxserver/prowlarr:latest @@ -95,11 +127,93 @@ services: - TZ=Europe/Berlin volumes: - ./config/prowlarr:/config - ports: - - 9696:9696 networks: - media-network + - traefik-public + labels: + - "traefik.enable=true" + - "traefik.http.routers.prowlarr.rule=Host(`prowlarr.jeffemmett.com`)" + - "traefik.http.routers.prowlarr.entrypoints=web" + - "traefik.http.services.prowlarr.loadbalancer.server.port=9696" + - "traefik.docker.network=traefik-public" + lidarr: + image: linuxserver/lidarr:latest + container_name: lidarr + restart: unless-stopped + environment: + - PUID=1000 + - PGID=1000 + - TZ=Europe/Berlin + volumes: + - ./config/lidarr:/config + - ./media/music:/music + - ./downloads:/downloads + networks: + - media-network + - traefik-public + labels: + - "traefik.enable=true" + - "traefik.http.routers.lidarr.rule=Host(`lidarr.jeffemmett.com`)" + - "traefik.http.routers.lidarr.entrypoints=web" + - "traefik.http.services.lidarr.loadbalancer.server.port=8686" + - "traefik.docker.network=traefik-public" + + # VPN for download privacy (optional - enable in .env) + gluetun: + image: qmcgaw/gluetun:latest + container_name: gluetun + restart: unless-stopped + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun:/dev/net/tun + environment: + - VPN_SERVICE_PROVIDER=${VPN_PROVIDER:-mullvad} + - VPN_TYPE=wireguard + - WIREGUARD_PRIVATE_KEY=${VPN_WIREGUARD_PRIVATE_KEY:-} + - WIREGUARD_ADDRESSES=${VPN_WIREGUARD_ADDRESS:-} + - SERVER_COUNTRIES=${VPN_COUNTRY:-Germany} + - TZ=Europe/Berlin + volumes: + - ./config/gluetun:/gluetun + ports: + # qBittorrent WebUI + - 8080:8080 + # BitTorrent ports + - 6881:6881 + - 6881:6881/udp + networks: + - media-network + profiles: + - vpn + + # Download client (routes through VPN when vpn profile enabled) + qbittorrent: + image: linuxserver/qbittorrent:latest + container_name: qbittorrent + restart: unless-stopped + environment: + - PUID=1000 + - PGID=1000 + - TZ=Europe/Berlin + - WEBUI_PORT=8080 + volumes: + - ./config/qbittorrent:/config + - ./downloads:/downloads + # When VPN enabled, use gluetun network + # network_mode: "service:gluetun" + networks: + - media-network + - traefik-public + labels: + - "traefik.enable=true" + - "traefik.http.routers.qbittorrent.rule=Host(`downloads.jeffemmett.com`)" + - "traefik.http.routers.qbittorrent.entrypoints=web" + - "traefik.http.services.qbittorrent.loadbalancer.server.port=8080" + - "traefik.docker.network=traefik-public" + + # Legacy Transmission (kept for compatibility, prefer qBittorrent) transmission: image: linuxserver/transmission:latest container_name: transmission @@ -119,6 +233,8 @@ services: - 51413:51413/udp networks: - media-network + profiles: + - legacy networks: media-network: diff --git a/scripts/deploy-to-netcup.sh b/scripts/deploy-to-netcup.sh index a199fc2..b9b2b15 100755 --- a/scripts/deploy-to-netcup.sh +++ b/scripts/deploy-to-netcup.sh @@ -48,9 +48,9 @@ ssh $REMOTE_HOST << 'REMOTE_SCRIPT' cd /opt/media-server # Create required directories -mkdir -p config/{jellyfin,sonarr,radarr,prowlarr,transmission} +mkdir -p config/{jellyfin,jellyseerr,sonarr,radarr,prowlarr,lidarr,qbittorrent,gluetun,navidrome} mkdir -p cache/jellyfin -mkdir -p downloads/{complete/movies,complete/tv} +mkdir -p downloads/{complete/movies,complete/tv,complete/music} mkdir -p media/{movies,shows,music} # Set permissions @@ -77,10 +77,18 @@ echo "" echo "Access your services:" echo " Jellyfin: https://movies.jeffemmett.com" echo " Music: https://music.jeffemmett.com" -echo " Sonarr: http://SERVER_IP:8989" -echo " Radarr: http://SERVER_IP:7878" -echo " Prowlarr: http://SERVER_IP:9696" -echo " Transmission: http://SERVER_IP:9091" +echo " Requests: https://requests.jeffemmett.com" +echo " Sonarr: https://sonarr.jeffemmett.com" +echo " Radarr: https://radarr.jeffemmett.com" +echo " Lidarr: https://lidarr.jeffemmett.com" +echo " Prowlarr: https://prowlarr.jeffemmett.com" +echo " Downloads: https://downloads.jeffemmett.com" +echo "" +echo "Next steps:" +echo " 1. Add hostnames to Cloudflare tunnel config (see README.md)" +echo " 2. Configure Prowlarr with indexers" +echo " 3. Configure Radarr/Sonarr/Lidarr with download client" +echo " 4. Configure Jellyseerr to connect to Jellyfin" echo "" echo "SSH to server: ssh netcup" echo "View logs: ssh netcup 'cd /opt/media-server && docker compose logs -f'"