> ## Content Index
> Fetch the complete content index at: https://itsamir.ir/llms.txt
> Use this file to discover other available public pages before exploring further.

# ساخت MediaHub؛ از چندتا Docker Container تا یک مدیا سرور شخصی
- URL: https://itsamir.ir/mediahub-self-hosted-media-server/
- Published: 2026-10-02T18:08:50.000Z
- Updated: 2026-10-02T18:11:55.000Z
- Description: داستان ساخت MediaHub؛ یک Control Plane شخصی برای مدیریت Sonarr، Radarr، Prowlarr، qBittorrent، Plex و نوتیفیکیشن تلگرام روی NAS و Docker.
- Author: ItsAmir

مدتی بود برای فیلم و سریال از سرویس‌های آماده استفاده می‌کردم تا اینکه کم‌کم دردسر استفاده ازشون بیشتر از راحتیش شد.

از اون طرف NAS داشتم و یه سوال ساده برام پیش اومد؛ چرا وقتی میشه کل این جریان رو روی سیستم خودم اجرا کنم، همچنان وابسته به چندتا سرویس مختلف باشم؟

برای همین یه Docker Compose جمع کردم و چندتا ابزار Open Source که هر کدوم یه کار مشخص رو خوب انجام میدن کنار هم گذاشتم:

- Sonarrبرای سریال‌ها
- Radarrبرای فیلم‌ها
- Prowlarr برای مدیریت Indexerها
- Bazarr برای Subtitle
- qBittorrent برای Download Client
- FlareSolverr برای بعضی Sourceهایی که نیازش دارن
- Plex برای Libraryو پخش مدیا

چیزی که برام جالب بود اینه که برای حجم کاری‌ای که انجام میدن Stack خیلی سنگینی نیست. یعنی لازم نیست حتماً یه Server عجیب‌وغریب داشته باشید و روی NAS، Mini PC یا حتی Raspberry Pi ــ اگر Imageهای ARM64 سرویس‌ها موجود باشه ــ قابل اجراست.

البته Plex Transcoding، مخصوصاً برای 4K، داستان خودش رو داره و اونجا قدرت CPU/GPU دستگاه مهم میشه.

همه‌چیز داشت کار می‌کرد ولی یه مشکل وجود داشت؛ برای انجام یه کار ساده باید بین چندتا پنل مختلف می‌چرخیدم.

برای فیلم Radarr، برای سریال Sonarr، برای Downloadها qBittorrent، برای Indexerها Prowlarr و برای دیدن Library همPlex.

اینجا بود که گفتم چرا یه لایه روی همه این‌ها نذارم؟

و این شد شروع **MediaHub**.

## MediaHub قرار نیست Sonarr و Radarrرو دوباره بسازه

از اول هدفم این نبود که برم Sonarr یا Radarr یا Plex رو از صفر پیاده کنم.

هر کدوم از این ابزارها سال‌هاست روی یه مسئله مشخص کار کردن و همون کار رو هم خوب انجام میدن.

MediaHub صرفاً یه Control Planeروی همه این‌هاست.

یعنی یه UI واحد که از داخلش بتونم وضعیت سرویس‌ها رو ببینم، فیلم یا سریال Search کنم، دانلودهای در حال انجام رو چک کنم، Library رو ببینم و Notification بگیرم.

نسخه فعلی تقریباً این ساختار رو داره:

```
MediaHub
├── Dashboard
├── Search & Add
├── Downloads
├── Library
└── Settings
```

Dashboard وضعیت Sonarr، Radarr، Prowlarr، Bazarr، qBittorrent و Plexرو نشون میده.

یه باگ جالب هم همین‌جا داشتم.

Plexجواب:

```
HTTP 401
```

میداد ولی MediaHub با افتخار سبزش کرده بود :)

در صورتی که `401` یعنی سرویس Reachable هست ولی درخواست Authenticate نشده.

پس Health Check درست نباید فقط بگه «سرویس جواب داد یا نه».

باید بین این حالت‌ها تفاوت بذاره:

```
Healthy
Reachable but unauthorized
Unavailable
```

اینم شد یکی از اولین TODOهای پروژه.

## جریان دانلود دقیقاً چطوریه؟

در حالت عادی مسیر یه Request تقریباً این شکلیه:

```
MediaHub
   │
   ▼
Sonarr / Radarr
   │
   ▼
Prowlarr / Indexers
   │
   ▼
Sonarr / Radarr
   │
   ▼
qBittorrent
   │
   ▼
/data/torrents
   │
   ▼
Sonarr / Radarr Import
   │
   ├── /data/media/movies
   └── /data/media/tv
               │
               ▼
              Plex
```

یعنی Prowlarr خودش چیزی دانلود نمیکنه.

Prowlarr Indexerها رو مدیریت میکنه و Sonarr یا Radarr بعد از پیدا کردن Release مناسب Job رو به qBittorrentمیفرستن.

بعد از تموم شدن دانلود، Sonarr یا Radarr فایل رو Import و مرتب میکنن و Plex هم همون Library رو می‌خونه.

## اجرای پروژه باDocker

برای Development روی سیستمLocal:

```
git clone <repo-url>
cd mediahub
```

بعد:

```
docker compose up -d --build
```

برای دیدن وضعیت سرویس‌ها:

```
docker compose ps
```

برایLog:

```
docker compose logs -f
```

یا مثلاً فقطMediaHub:

```
docker compose logs -f mediahub
```

برای Stop کردنStack:

```
docker compose down
```

و اگر سورس یا Imageها تغییر کرده باشن:

```
docker compose up -d --build
```

اولین بار که Stack رو روی Mac بالا آوردم یه مشکل ساده ولی جالب داشتم.

Compose اولیه برای NAS نوشته شده بود و Pathهایی شبیه این داشت:

```
/volume1/media
/volume1/downloads
```

که طبیعتاً روی macOS وجود ندارن.

برای Development تبدیلش کردم به:

```
MEDIA_ROOT=./data/media
DOWNLOADS_ROOT=./data/torrents
CONFIG_ROOT=./data/config
```

و Directoryها رو ساختم:

```
mkdir -p \
  data/media/movies \
  data/media/tv \
  data/torrents \
  data/config
```

## Volumeها یه نکته مهم دارن

نسخه اول برای qBittorrent یه `/downloads` داشتم، برای Radarr یه `/movies` و برای Sonarr هم `/tv`.

کار میکنه ولی ساختار خوبی برای نسخه نهایی نیست.

ساختاری که الان ترجیح میدم اینه:

```
/data
├── torrents
│   ├── movies
│   └── tv
└── media
    ├── movies
    └── tv
```

و همه Containerهایی که لازم دارن یه `/data` مشترک می‌بینن.

مثلاً:

```
services:

  qbittorrent:
    volumes:
      - /srv/mediahub/data:/data

  radarr:
    volumes:
      - /srv/mediahub/data:/data

  sonarr:
    volumes:
      - /srv/mediahub/data:/data
```

qBittorrentفایل‌ها رو می‌ریزه داخل:

```
/data/torrents
```

Root Folder فیلم توی Radarrمیشه:

```
/data/media/movies
```

و Root Folder سریال تویSonarr:

```
/data/media/tv
```

مزیت این ساختار اینه که qBittorrent و Sonarr/Radarr فایل‌ها رو با Path یکسان می‌بینن و امکان Hardlink و Move درست فایل‌ها حفظ میشه.

وقتی Library چند صد گیگ یا چند ترابایت بشه این موضوع دیگه یه جزئیات کوچیک نیست.

## اگر بخوام روی یه Linux Server نصبش کنم

فرض کنیم یه Home Server یا Mini PC با Ubuntu/Debian داریم.

اول Directoryها:

```
sudo mkdir -p /srv/mediahub/data/torrents
sudo mkdir -p /srv/mediahub/data/media/movies
sudo mkdir -p /srv/mediahub/data/media/tv
sudo mkdir -p /srv/mediahub/config
```

بعدPermission:

```
sudo chown -R $USER:$USER /srv/mediahub
```

داخل `.env`:

```
TZ=Asia/Tehran

MEDIA_ROOT=/srv/mediahub/data/media
DOWNLOADS_ROOT=/srv/mediahub/data/torrents
CONFIG_ROOT=/srv/mediahub/config
```

بعد:

```
docker compose up -d --build
```

و برای چک کردن:

```
docker compose ps
```

برای Server دائمی هم بهتره سرویس‌ها:

```
restart: unless-stopped
```

داشته باشن تا بعد از Reboot دوباره بالا بیان.

## اگر Sonarr و Radarr از قبل نصب باشن چی؟

این حالت برای خودم مهم بود چون ممکنه کسی از قبل Arr Stack خودش رو داشته باشه و فقط بخواد MediaHub رو اضافه کنه.

لازم نیست همه چیز دوباره نصب بشه.

یه Docker Network مشترک می‌سازیم:

```
docker network create media-stack
```

بعد سرویس‌های موجود و MediaHub رو روی همون Network قرار میدیم:

```
networks:
  media-stack:
    external: true
```

مثلاً:

```
services:
  sonarr:
    networks:
      - media-stack

  radarr:
    networks:
      - media-stack
```

وMediaHub:

```
services:
  mediahub:
    networks:
      - media-stack

networks:
  media-stack:
    external: true
```

بعد داخل MediaHub نیازی به IP ندارم.

مستقیم میشه نوشت:

```
http://sonarr:8989
http://radarr:7878
http://prowlarr:9696
http://qbittorrent:8080
```

این یکی از چیزهایی بود که اول خودمم اشتباه کردم و گذاشته بودم:

```
localhost:7878
```

در حالی که داخل Docker هر Container یه `localhost` مخصوص خودش داره.

Docker DNS اسم Serviceها رو Resolve میکنه و همین خیلی از دردسرهای IPثابت رو حذف میکنه.

## API Keyها

Sonarr، Radarr، Prowlarr و Bazarr از داخل Settings خودشون API Keyمیدن.

MediaHub هم تقریباً با همچین Configای باهاشون حرف میزنه:

```
SONARR_URL=http://sonarr:8989
SONARR_API_KEY=...

RADARR_URL=http://radarr:7878
RADARR_API_KEY=...

PROWLARR_URL=http://prowlarr:9696
PROWLARR_API_KEY=...

BAZARR_URL=http://bazarr:6767
BAZARR_API_KEY=...
```

qBittorrent هم در تنظیم معمول از Username و Password مربوط به Web UIاستفاده میکنه:

```
QBITTORRENT_URL=http://qbittorrent:8080
QBITTORRENT_USERNAME=admin
QBITTORRENT_PASSWORD=...
```

و طبیعتاً `.env` نباید بره داخلGit:

```
.env
```

## Prowlarr و FlareSolverr

بعد Prowlarr رو به Sonarr و Radarr وصل کردم تا Indexerها از یه جا مدیریت بشن.

داخل Prowlarr برایSonarr:

```
Settings
→ Apps
→ Sonarr
```

وURL:

```
http://sonarr:8989
```

برای Radarr هم:

```
http://radarr:7878
```

بعد Indexerهایی که اجازه استفاده ازشون رو دارم توی Prowlarr تعریف میشن و تنظیمات مرتبط با Sonarr و Radarr Sync میشه.

بعضی Sourceها هم پشت Cloudflare هستن.

برای همین FlareSolverr رو هم داخل Stack گذاشتم.

مسیرش تقریباً این شکلی میشه:

```
Prowlarr
    │
    ▼
FlareSolverr
    │
    ▼
Source
```

البته FlareSolverr جادو نیست و قرار نیست هر Cloudflare Challenge جدیدی رو حل کنه.

برای بعضی Sourceها کار میکنه، برای بعضی‌ها نه.

## Telegram Notification

یه چیزی که خیلی دوست داشتم این بود که بعد از Request کردن یه فیلم یا سریال دیگه لازم نباشه برگردم Dashboard و وضعیتش رو چک کنم.

برای همین Telegram رو هم بهش وصل کردم.

از BotFather یه Bot ساختم و یه **Bot Token** گرفتم.

Config:

```
TELEGRAM_BOT_TOKEN=123456789:AAExampleToken
TELEGRAM_CHAT_ID=123456789
WEBHOOK_SECRET=some-random-secret
```

بعد Sonarr و Radarr موقع Download/Import شدن فایل یه Webhook به MediaHub میزنن.

برای مثال نسخه خیلی ساده ارسال پیام باPython:

```
import os
import requests

TELEGRAM_BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
TELEGRAM_CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]

def send_telegram(message: str):
    response = requests.post(
        f"https://api.telegram.org/"
        f"bot{TELEGRAM_BOT_TOKEN}/sendMessage",
        json={
            "chat_id": TELEGRAM_CHAT_ID,
            "text": message,
        },
        timeout=10,
    )

    response.raise_for_status()
```

و برای گرفتن Webhookها میشه خیلی ساده FastAPI گذاشت:

```
import os

import requests
from fastapi import FastAPI, HTTPException, Request

app = FastAPI()

TELEGRAM_BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
TELEGRAM_CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]
WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"]

def send_telegram(message: str):
    response = requests.post(
        f"https://api.telegram.org/"
        f"bot{TELEGRAM_BOT_TOKEN}/sendMessage",
        json={
            "chat_id": TELEGRAM_CHAT_ID,
            "text": message,
        },
        timeout=10,
    )

    response.raise_for_status()

def check_secret(token: str | None):
    if token != WEBHOOK_SECRET:
        raise HTTPException(status_code=401)

@app.post("/webhooks/radarr")
async def radarr_webhook(
    request: Request,
    token: str | None = None,
):
    check_secret(token)

    payload = await request.json()

    if payload.get("eventType") == "Download":
        title = payload.get("movie", {}).get(
            "title",
            "Unknown movie",
        )

        send_telegram(
            f"🎬 {title} downloaded and imported."
        )

    return {"ok": True}

@app.post("/webhooks/sonarr")
async def sonarr_webhook(
    request: Request,
    token: str | None = None,
):
    check_secret(token)

    payload = await request.json()

    if payload.get("eventType") == "Download":
        title = payload.get("series", {}).get(
            "title",
            "Unknown series",
        )

        send_telegram(
            f"📺 {title} downloaded and imported."
        )

    return {"ok": True}
```

برای اجرا:

```
pip install fastapi uvicorn requests
```

بعد:

```
uvicorn main:app \
  --host 0.0.0.0 \
  --port 8000
```

Webhookها میشن مثلاً:

```
http://mediahub:8000/webhooks/radarr?token=SECRET

http://mediahub:8000/webhooks/sonarr?token=SECRET
```

و همین URLها داخل Sonarr و Radarr قرار می‌گیرن.

در نهایت مسیر Notification میشه:

```
Download finished
      │
      ▼
Sonarr / Radarr
      │
      ▼
MediaHub Webhook
      │
      ▼
Telegram Bot API
      │
      ▼
📱 Notification
```

یعنی یه فیلم رو Request میکنم، لپ‌تاپ رو می‌بندم و وقتی Import شد Telegram بهم خبر میده.

برای MVP همین مدل Token توی URL جواب میده ولی برای نسخه Production ترجیح میدم Secret با Header فرستاده بشه و Webhook هم پشت شبکه داخلی یا Reverse Proxy قرار بگیره.

## Plex

بعد از Import شدن فایل، Plex از:

```
/data/media/movies
/data/media/tv
```

Libraryرو می‌خونه.

برای دسترسی MediaHub به API مربوط به Plex هم Token دارم:

```
PLEX_URL=http://plex:32400
PLEX_TOKEN=...
```

و قسمت Recently Added هم از همین‌جا پر میشه.

## نسخه نهایی MediaHub قراره چه شکلی باشه؟

نسخه فعلی بیشتر Admin Panel هست.

ولی چیزی که دوست دارم در نهایت داشته باشم یه Media App واقعی‌تره.

یعنی کاربر وارد بشه، Posterها رو ببینه، Search فارسی و انگلیسی داشته باشه، صفحه Details فیلم و سریال داشته باشه و در نهایت فقط:

```
Request
```

رو بزنه.

اگر لازم داشت Quality رو هم انتخاب کنه:

```
1080p
2160p
HEVC
```

و تمام.

کاربر لازم نیست بدونه پشت صحنه Sonarr، Radarr، Prowlarr یا qBittorrent وجود دارن.

کل پیچیدگی باید پشت MediaHub مخفی بشه.

## بحثSecurity

یه موضوع دیگه هم اینه که اگر Stack روی Server اجرا بشه من شخصاً این Portها رو مستقیم Public نمیکنم:

```
8989
7878
9696
8080
6767
```

Sonarr، Radarr، Prowlarr و qBittorrent پنل‌های Adminهستن.

برای دسترسی از بیرون ترجیحم LAN، VPN مثل WireGuard/Tailscale یا Reverse Proxy با Authentication هست.

در حالت ایده‌آل MediaHub تنها UIای میشه که کاربر عادی می‌بینه و بقیه سرویس‌ها فقط داخل Network داخلی با هم حرف میزنن.

## آخرش

MediaHub هنوز MVPهست.

Authentication، User Management، مدیریت بهتر Quality Profile، Watchlist، Plex Integration بهتر و UIکامل‌تر هنوز مونده.

ولی چیزی که برای خودم توی این پروژه جذابه اینه که قرار نیست دوباره چرخ رو اختراع کنم.

چندتا ابزار Open Source که هر کدوم یه کار رو خیلی خوب انجام میدن کنار هم قرار گرفتن و MediaHub فقط اون‌ها رو به یه تجربه واحد تبدیل میکنه.

رایگان، Self-hosted، قابل اجرا روی NAS یا Home Server و مهم‌تر از همه چیزی که خودم واقعاً ازش استفاده میکنم.

فعلاً نسخه Development روی Mac بالا اومده و کل Stack داره کار میکنه.

مرحله بعد انتقال کاملش روی NAS، بهتر کردن UI و تبدیل کردنش از یه Control Panel ساده به یه Media App واقعیه.