diff --git a/stacks/media/.env.example b/stacks/media/.env.example index 5d4c4335..ad7b3868 100644 --- a/stacks/media/.env.example +++ b/stacks/media/.env.example @@ -1,7 +1,16 @@ -TZ=Asia/Shanghai -DOMAIN=localhost +# Media Stack Configuration + +# System IDs PUID=1000 PGID=1000 -# Local paths for media and downloads -MEDIA_PATH=/mnt/media -DOWNLOAD_PATH=/mnt/downloads + +# Timezone +TZ=Europe/Paris + +# Base Domain +DOMAIN=example.local + +# TRaSH Guides Hardlink Paths +# Both must ideally be on the same underlying physical mount +MEDIA_ROOT=/data/media +DOWNLOADS_ROOT=/data/torrents diff --git a/stacks/media/README.md b/stacks/media/README.md new file mode 100644 index 00000000..3d8a8163 --- /dev/null +++ b/stacks/media/README.md @@ -0,0 +1,71 @@ +# Media Stack + +This stack provides a complete, automated media management and streaming solution using the [TRaSH Guides](https://trash-guides.info/) methodology for hardlinking. + +## Included Services + +| Service | Function | URL | +|---------|----------|-----| +| **Jellyfin** | Media Server (Streaming) | `https://jellyfin.${DOMAIN}` | +| **Sonarr** | TV Show Management | `https://sonarr.${DOMAIN}` | +| **Radarr** | Movie Management | `https://radarr.${DOMAIN}` | +| **Prowlarr** | Indexer Manager | `https://prowlarr.${DOMAIN}` | +| **qBittorrent** | Torrent Downloader | `https://bt.${DOMAIN}` | +| **Jellyseerr** | Media Request Management | `https://jellyseerr.${DOMAIN}` | + +## Directory Structure (Hardlinks) + +To ensure that hardlinking works (which saves space by not duplicating files between your downloads and media folders), both `MEDIA_ROOT` and `DOWNLOADS_ROOT` **MUST** reside on the same underlying physical mount on the host. + +Example host directory structure: +``` +/data/ +├── torrents/ <-- Set DOWNLOADS_ROOT to this +│ ├── movies/ +│ └── tv/ +└── media/ <-- Set MEDIA_ROOT to this + ├── movies/ + └── tv/ +``` + +Inside the containers, these are mounted as `/data/torrents` and `/data/media`, ensuring atomic moves and hardlinks work seamlessly across all *arr apps. + +## Quick Start + +1. Copy the example environment file: + ```bash + cp .env.example .env + ``` +2. Edit `.env` and set your `DOMAIN`, `PUID`, `PGID`, `MEDIA_ROOT`, and `DOWNLOADS_ROOT`. +3. Start the stack: + ```bash + docker compose up -d + ``` + +## Configuration Steps + +### 1. Connecting qBittorrent to Sonarr/Radarr + +1. Open **Sonarr** / **Radarr** and go to `Settings` > `Download Clients`. +2. Add a new `qBittorrent` client. +3. Configure the connection: + - **Host**: `qbittorrent` (use the internal Docker DNS name) + - **Port**: `8080` + - **Username/Password**: Use your qBittorrent credentials (default is usually `admin` / `adminadmin` unless changed in qBittorrent logs). +4. Save and Test. + +### 2. Adding Media Libraries in Jellyfin + +1. Open **Jellyfin** and go to the Admin Dashboard > `Libraries`. +2. Add a new library (e.g., "Movies"). +3. For the folder path, browse to `/data/media/movies` (this maps to your host's `MEDIA_ROOT/movies`). +4. Add another library (e.g., "TV Shows") and map it to `/data/media/tv`. +5. Run a library scan. + +## FAQ + +**Q: Why are my files taking up double the space?** +A: Hardlinking is not working. Ensure that `MEDIA_ROOT` and `DOWNLOADS_ROOT` are on the exact same disk/partition on your host machine. + +**Q: Why can't Sonarr/Radarr import downloaded files?** +A: Check permissions. The `PUID` and `PGID` set in the `.env` file must own the directories defined in `MEDIA_ROOT` and `DOWNLOADS_ROOT` on the host. diff --git a/stacks/media/docker-compose.yml b/stacks/media/docker-compose.yml index 760ecdbf..8a590fa0 100644 --- a/stacks/media/docker-compose.yml +++ b/stacks/media/docker-compose.yml @@ -1,158 +1,181 @@ networks: proxy: external: true + +volumes: + jellyfin-config: null + jellyfin-cache: null + prowlarr-config: null + qbittorrent-config: null + radarr-config: null + sonarr-config: null + jellyseerr-config: null + services: - jellyfin: - container_name: jellyfin + qbittorrent: + container_name: qbittorrent + image: lscr.io/linuxserver/qbittorrent:4.6.7 environment: - - TZ=${TZ} - - JELLYFIN_PublishedServerUrl=https://media.${DOMAIN} + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + - WEBUI_PORT=8080 + volumes: + - qbittorrent-config:/config + - ${DOWNLOADS_ROOT}:/data/torrents + networks: + - proxy + labels: + - traefik.enable=true + - traefik.http.routers.qbittorrent.rule=Host(`bt.${DOMAIN}`) + - traefik.http.routers.qbittorrent.entrypoints=websecure + - traefik.http.routers.qbittorrent.tls=true + - traefik.http.services.qbittorrent.loadbalancer.server.port=8080 healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:8080"] interval: 30s - retries: 3 - start_period: 30s - test: - - CMD - - curl - - -sf - - http://localhost:8096/health timeout: 10s - image: jellyfin/jellyfin:10.9.11 - labels: - - traefik.enable=true - - traefik.http.routers.jellyfin.rule=Host() - - traefik.http.routers.jellyfin.entrypoints=websecure - - traefik.http.routers.jellyfin.tls=true - - traefik.http.services.jellyfin.loadbalancer.server.port=8096 - networks: - - proxy + retries: 3 + start_period: 60s restart: unless-stopped - volumes: - - jellyfin-config:/config - - jellyfin-cache:/cache - - ${MEDIA_PATH}:/media:ro + prowlarr: container_name: prowlarr + image: lscr.io/linuxserver/prowlarr:1.22.0 environment: - - PUID=1000 - - PGID=1000 - - TZ=${TZ} + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - prowlarr-config:/config + networks: + - proxy + labels: + - traefik.enable=true + - traefik.http.routers.prowlarr.rule=Host(`prowlarr.${DOMAIN}`) + - traefik.http.routers.prowlarr.entrypoints=websecure + - traefik.http.routers.prowlarr.tls=true + - traefik.http.services.prowlarr.loadbalancer.server.port=9696 healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:9696/ping"] interval: 30s + timeout: 10s retries: 3 start_period: 60s - test: - - CMD - - curl - - -sf - - http://localhost:9696/ping - timeout: 10s - image: linuxserver/prowlarr:1.24.3 - labels: - - traefik.enable=true - - traefik.http.routers.prowlarr.rule=Host(`prowlarr.${DOMAIN}`) - - traefik.http.routers.prowlarr.entrypoints=websecure - - traefik.http.routers.prowlarr.tls=true - - traefik.http.services.prowlarr.loadbalancer.server.port=9696 - networks: - - proxy restart: unless-stopped - volumes: - - prowlarr-config:/config - qbittorrent: - container_name: qbittorrent + + sonarr: + container_name: sonarr + image: lscr.io/linuxserver/sonarr:4.0.11 environment: - - PUID=1000 - - PGID=1000 - - TZ=${TZ} - - WEBUI_PORT=8080 + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - sonarr-config:/config + - ${MEDIA_ROOT}:/data/media + - ${DOWNLOADS_ROOT}:/data/torrents + networks: + - proxy + depends_on: + qbittorrent: + condition: service_healthy + labels: + - traefik.enable=true + - traefik.http.routers.sonarr.rule=Host(`sonarr.${DOMAIN}`) + - traefik.http.routers.sonarr.entrypoints=websecure + - traefik.http.routers.sonarr.tls=true + - traefik.http.services.sonarr.loadbalancer.server.port=8989 healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:8989/ping"] interval: 30s + timeout: 10s retries: 3 start_period: 60s - test: - - CMD - - curl - - -sf - - http://localhost:8080 - timeout: 10s - image: linuxserver/qbittorrent:4.6.7 - labels: - - traefik.enable=true - - traefik.http.routers.qbittorrent.rule=Host(`bt.${DOMAIN}`) - - traefik.http.routers.qbittorrent.entrypoints=websecure - - traefik.http.routers.qbittorrent.tls=true - - traefik.http.services.qbittorrent.loadbalancer.server.port=8080 - networks: - - proxy restart: unless-stopped - volumes: - - qbittorrent-config:/config - - ${DOWNLOAD_PATH}:/downloads + radarr: container_name: radarr + image: lscr.io/linuxserver/radarr:5.8.1 environment: - - PUID=1000 - - PGID=1000 - - TZ=${TZ} + - PUID=${PUID} + - PGID=${PGID} + - TZ=${TZ} + volumes: + - radarr-config:/config + - ${MEDIA_ROOT}:/data/media + - ${DOWNLOADS_ROOT}:/data/torrents + networks: + - proxy + depends_on: + qbittorrent: + condition: service_healthy + labels: + - traefik.enable=true + - traefik.http.routers.radarr.rule=Host(`radarr.${DOMAIN}`) + - traefik.http.routers.radarr.entrypoints=websecure + - traefik.http.routers.radarr.tls=true + - traefik.http.services.radarr.loadbalancer.server.port=7878 healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:7878/ping"] interval: 30s + timeout: 10s retries: 3 start_period: 60s - test: - - CMD - - curl - - -sf - - http://localhost:7878/ping - timeout: 10s - image: linuxserver/radarr:5.11.0 - labels: - - traefik.enable=true - - traefik.http.routers.radarr.rule=Host(`radarr.${DOMAIN}`) - - traefik.http.routers.radarr.entrypoints=websecure - - traefik.http.routers.radarr.tls=true - - traefik.http.services.radarr.loadbalancer.server.port=7878 - networks: - - proxy restart: unless-stopped - volumes: - - radarr-config:/config - - ${MEDIA_PATH}:/media - - ${DOWNLOAD_PATH}:/downloads - sonarr: - container_name: sonarr + + jellyfin: + container_name: jellyfin + image: jellyfin/jellyfin:10.9.11 environment: - - PUID=1000 - - PGID=1000 - - TZ=${TZ} + - TZ=${TZ} + - JELLYFIN_PublishedServerUrl=https://jellyfin.${DOMAIN} + volumes: + - jellyfin-config:/config + - jellyfin-cache:/cache + - ${MEDIA_ROOT}:/data/media:ro + networks: + - proxy + labels: + - traefik.enable=true + - traefik.http.routers.jellyfin.rule=Host(`jellyfin.${DOMAIN}`) + - traefik.http.routers.jellyfin.entrypoints=websecure + - traefik.http.routers.jellyfin.tls=true + - traefik.http.services.jellyfin.loadbalancer.server.port=8096 healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:8096/health"] interval: 30s - retries: 3 - start_period: 60s - test: - - CMD - - curl - - -sf - - http://localhost:8989/ping timeout: 10s - image: linuxserver/sonarr:4.0.9 - labels: - - traefik.enable=true - - traefik.http.routers.sonarr.rule=Host(`sonarr.${DOMAIN}`) - - traefik.http.routers.sonarr.entrypoints=websecure - - traefik.http.routers.sonarr.tls=true - - traefik.http.services.sonarr.loadbalancer.server.port=8989 - networks: - - proxy + retries: 3 + start_period: 30s restart: unless-stopped + + jellyseerr: + container_name: jellyseerr + image: fallenbagel/jellyseerr:2.1.1 + environment: + - TZ=${TZ} volumes: - - sonarr-config:/config - - ${MEDIA_PATH}:/media - - ${DOWNLOAD_PATH}:/downloads -volumes: - jellyfin-cache: null - jellyfin-config: null - prowlarr-config: null - qbittorrent-config: null - radarr-config: null - sonarr-config: null + - jellyseerr-config:/app/config + networks: + - proxy + depends_on: + jellyfin: + condition: service_healthy + sonarr: + condition: service_healthy + radarr: + condition: service_healthy + labels: + - traefik.enable=true + - traefik.http.routers.jellyseerr.rule=Host(`jellyseerr.${DOMAIN}`) + - traefik.http.routers.jellyseerr.entrypoints=websecure + - traefik.http.routers.jellyseerr.tls=true + - traefik.http.services.jellyseerr.loadbalancer.server.port=5055 + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:5055/api/v1/status"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + restart: unless-stopped