Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions stacks/media/.env.example
Original file line number Diff line number Diff line change
@@ -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
71 changes: 71 additions & 0 deletions stacks/media/README.md
Original file line number Diff line number Diff line change
@@ -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.
265 changes: 144 additions & 121 deletions stacks/media/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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