Arr stack у Docker Compose: Prowlarr, Sonarr, Radarr
Запустіть Prowlarr, Sonarr, Radarr і qBittorrent одним Docker Compose на VPS: спільні PUID, PGID і томи збережуть hardlink під час імпорту.
Що ви налаштовуєте
Docker Compose arr stack складається з чотирьох контейнерів, які керують медіатекою: Prowlarr відповідає за налаштування indexer, Sonarr — за серіали, Radarr — за фільми, а qBittorrent — за клієнт завантаження. Вони взаємодіють через мережу Compose за іменами сервісів і спільно використовують одне дерево каталогів на хості. Встановлення займає небагато часу. Від структури томів залежить, чи працюватиме stack роками, чи щотижня створюватиме проблеми, тому більша частина цього посібника присвячена саме їй.
Stack не шукає контент самостійно. Prowlarr використовує indexer, які ви до нього додали, а вибір indexer є вашим рішенням і вашою юридичною відповідальністю. У цьому посібнику розглянуто інфраструктурну частину: користувачів, шляхи, права доступу, мережу контейнерів і перевірки, які підтверджують, що все працює.
Якщо ви ніколи не створювали файл Compose, спочатку прочитайте основи Docker Compose для VPS. У цьому матеріалі передбачається, що docker compose version уже виводить результат на вашому сервері.
Чому hardlink не працює і чому це визначає всю схему
Коли Sonarr завершує завантаження, він імпортує файл до медіатеки. Якщо каталог завантажень і каталог медіатеки розташовані в тій самій файловій системі, імпорт виконується через hardlink: це друге ім’я, що вказує на ті самі дані на диску. Додатковий простір і час не потрібні. Торент продовжує роздавати файл за старим ім’ям, а медіасервер читає його за новим.
Якщо два каталоги розташовані в різних файлових системах, ядро не може створити таке посилання. Sonarr переходить до копіювання. Сезон розміром 40 GB тепер займає 80 GB на диску й потребує кількох хвилин операцій введення-виведення, а журнал імпорту фіксує, що створення hardlink не вдалося і файл було скопійовано. На VPS із фіксованим обсягом диска саме так можна залишитися без вільного місця за тиждень.
Ось де виникає проблема. Усередині контейнера bind mount є межею файлової системи. Якщо змонтувати /mnt/data/torrents як /downloads, а /mnt/data/media як /tv, Sonarr бачитиме два окремі монтування, навіть якщо обидва розташовані на одному диску хоста, і відмовиться створювати посилання між ними. В офіційній документації образу LinuxServer.io це зазначено безпосередньо: використання окремих шляхів /downloads і /tv позбавляє можливості створювати hardlink.
Потрібно використовувати одне монтування. Кожен контейнер, який працює з медіафайлами, отримує той самий єдиний том, /mnt/data:/data, а всі шляхи в контейнері мають бути каталогами всередині нього. Одна точка монтування, одна файлова система — hardlink працює.
Створіть користувача, групу та каталоги
Контейнери записують файли від імені користувача з числовим ідентифікатором, заданим параметрами PUID і PGID. Використовуйте власний обліковий запис, щоб читати й редагувати ці файли через SSH без sudo.
id -u
id -gВ обох випадках на щойно створеному Ubuntu VPS зазвичай буде виведено 1000. Тепер створіть цю структуру каталогів. Розмістіть її на диску, де зберігаються ваші медіафайли, і зберігайте всю структуру на цьому одному диску.
sudo mkdir -p /mnt/data/torrents/movies /mnt/data/torrents/tv
sudo mkdir -p /mnt/data/media/Movies /mnt/data/media/Shows
sudo chown -R 1000:1000 /mnt/data
sudo chmod -R 775 /mnt/dataПерш ніж продовжити, перевірте, що це справді одна файлова система:
df --output=source,target /mnt/data/torrents /mnt/data/mediaВ обох рядках має бути вказано той самий вихідний пристрій. Якщо пристрої різні, hard link ніколи не працюватиме, незалежно від налаштувань контейнера.
Каталоги бібліотек навмисно названо Movies і Shows. Якщо ви вже використовуєте Jellyfin як медіасервер, змонтуйте /mnt/data/media у Jellyfin як /media, і його бібліотеки буде розміщено в /media/Movies та /media/Shows — саме там, де їх розміщує той посібник.
Файл середовища
Зберігайте значення, які відрізняються для кожного сервера, у .env поруч із файлом Compose.
mkdir -p ~/arr && cd ~/arrСтворіть ~/arr/.env:
PUID=1000
PGID=1000
TZ=Etc/UTC
DATA_ROOT=/mnt/dataУстановіть для TZ власний часовий пояс, наприклад Europe/Berlin. Застосунки arr планують завдання та додають часові мітки до рядків журналу в цьому часовому поясі, тому неправильне значення згодом ускладнить аналіз усіх журналів.
Файл Compose
Запишіть ~/arr/docker-compose.yml:
services:
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
container_name: prowlarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
volumes:
- ./config/prowlarr:/config
ports:
- 127.0.0.1:9696:9696
restart: unless-stopped
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
volumes:
- ./config/sonarr:/config
- ${DATA_ROOT}:/data
ports:
- 127.0.0.1:8989:8989
restart: unless-stopped
radarr:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
volumes:
- ./config/radarr:/config
- ${DATA_ROOT}:/data
ports:
- 127.0.0.1:7878:7878
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
- WEBUI_PORT=8080
- TORRENTING_PORT=6881
volumes:
- ./config/qbittorrent:/config
- ${DATA_ROOT}:/data
ports:
- 127.0.0.1:8080:8080
- 6881:6881
- 6881:6881/udp
stop_grace_period: "10s"
restart: unless-stoppedУ цьому файлі чотири налаштування виконують основну роботу.
${DATA_ROOT}:/data однаковий у трьох контейнерах, які працюють із медіафайлами. Prowlarr не отримує цей параметр, оскільки Prowlarr ніколи не відкриває медіафайл.
Кожен вебпорт прив’язано до 127.0.0.1, тому Docker публікує його лише на loopback-адресі. Звичайний 8989:8989 опублікував би порт на всіх інтерфейсах, а власні правила firewall Docker передали б цей трафік безпосередньо повз правило deny у ufw. Така поведінка постійно дивує користувачів. Її пояснено в матеріалі чому Docker публікує порти безпосередньо повз ufw.
Порт 6881 навмисно опубліковано на всіх інтерфейсах. Це порт для прослуховування torrent-з’єднань, і він має бути доступним для вхідних підключень від peer-вузлів. Тут діє та сама поведінка Docker, тому правило ufw для цього порту не потрібне, а його додавання нічого не змінить. У документації Docker, перевіреній у жовтні 2026 року, зазначено, що трафік до опублікованого порту маршрутизується до того, як потрапляє в chains, які використовує ufw, тому sudo ufw allow 6881 не впливає на цей порт. Порт стає доступним з інтернету одразу після запуску контейнера, і правило deny у ufw також не закриє його. Якщо потрібно обмежити список вузлів, які можуть підключатися до цього порту, документація Docker рекомендує правила в chain DOCKER-USER, а не в ufw. Використовуйте ufw для сервісів, які не працюють у Docker, наприклад SSH. Якщо цей інструмент для вас новий, ознайомтеся з матеріалом основи firewall ufw для VPS.
Каталоги конфігурації окремі для кожного застосунку, а спільним є лише том із медіафайлами. Створіть ці каталоги до першого запуску, щоб їхнім власником був ваш користувач, а не root:
mkdir -p ~/arr/config/prowlarr ~/arr/config/sonarr ~/arr/config/radarr ~/arr/config/qbittorrent
docker compose up -d
docker compose psУсі чотири сервіси мають використовувати running. Станом на жовтень 2026 року ці образи опубліковано на lscr.io, а тег latest відповідає поточному стабільному випуску. Тому замість нього можна зафіксувати тег версії, якщо ви хочете, щоб оновлення були свідомим рішенням, а не несподіванкою.
Безпечний доступ до вебінтерфейсів
Оскільки порти прив’язані до loopback, наразі нічого не опубліковано назовні. Перенаправте їх через SSH зі свого комп’ютера:
ssh -L 9696:127.0.0.1:9696 -L 8989:127.0.0.1:8989 \
-L 7878:127.0.0.1:7878 -L 8080:127.0.0.1:8080 you@your-serverТепер http://127.0.0.1:8989 у браузері відкриває Sonarr на сервері. Для постійного доступу розмістіть стек за Traefik із TLS-сертифікатами для кількох застосунків або підключайтеся до сервера через WireGuard VPN, розгорнутий власноруч. Однак VPN сам по собі не надає доступу до цих портів. У документації Docker, перевіреній у жовтні 2026 року, зазначено: якщо порт опубліковано на localhost-адресі, доступ до нього має лише Docker host. Щоб використовувати VPN, продовжуйте запускати SSH-тунель через нього або змініть прив’язки у файлі Compose з 127.0.0.1 на власну адресу сервера у VPN-інтерфейсі. Жоден із цих застосунків не повинен бути доступним із публічного інтернету лише через власну сторінку входу. Якщо ви оберете reverse proxy і хочете використовувати один обліковий запис для всіх чотирьох інтерфейсів замість чотирьох окремих облікових записів застосунків, Authentik надає self-hosted single sign-on, який Traefik може застосовувати до кожного запиту за допомогою forward auth.
qBittorrent під час першого запуску генерує випадковий пароль адміністратора та виводить його в журнал контейнера. Перегляньте його, а потім змініть у вебінтерфейсі:
docker compose logs qbittorrent | grep -i passwordЯкщо не змінити пароль, під час кожного перезапуску буде згенеровано новий випадковий пароль, і щоразу доведеться знову шукати його в журналах.
Налаштуйте шляхи всередині кожного застосунку
У qBittorrent відкрийте Options, потім Downloads і вкажіть /data/torrents як стандартний шлях збереження. Папка для незавершених завантажень також має бути в цьому дереві, наприклад /data/torrents/incomplete. Завантаження, яке завершилося за межами /data, неможливо додати до бібліотеки за допомогою hardlink.
У Sonarr відкрийте Settings, потім Media Management і додайте кореневу папку /data/media/Shows. У Radarr коренева папка має бути /data/media/Movies. Це шляхи всередині контейнера. Шлях хоста /mnt/data/media/Shows буде відхилено, оскільки з погляду контейнера цей каталог не існує.
У Sonarr і Radarr відкрийте Settings, потім Download Clients і додайте qBittorrent. Вкажіть хост qbittorrent і порт 8080. Ім’я сервісу працює як ім’я хоста, оскільки Compose розміщує всі чотири контейнери в одній мережі з внутрішньою службою DNS (domain name system). Не використовуйте тут localhost: усередині контейнера Sonarr localhost — це Sonarr.
Залиште Remote Path Mappings порожнім. Ця функція призначена для перетворення шляху, який повідомляє клієнт завантажень, у шлях, доступний застосунку arr. За наявності спільного монтування /data обидва контейнери вже використовують однакові шляхи. Це друга причина, чому така структура варта докладених зусиль.
Підключіть Prowlarr до Sonarr і Radarr
Prowlarr передає визначення індексаторів до інших застосунків, тому індексатор потрібно налаштувати лише один раз, а не двічі. Для цього потрібен ключ API (інтерфейсу програмування застосунків) від кожного з них.
У Sonarr відкрийте Settings, потім General і скопіюйте ключ API. У Prowlarr відкрийте Settings, потім Apps, додайте застосунок Sonarr і заповніть три поля. У полі Prowlarr Server вкажіть http://prowlarr:9696. У полі Sonarr Server вкажіть http://sonarr:8989. У полі API Key вкажіть скопійоване значення. Натисніть Test. Зелений результат означає, що Prowlarr підключився до Sonarr через мережу Compose. Повторіть ці дії для Radarr, використавши http://radarr:7878.
Червоний результат із повідомленням про відмову в підключенні майже завжди означає неправильне ім’я сервісу або відсутній префікс http://. Переконайтеся, що ім’я дозволяється з контейнера:
docker compose exec prowlarr curl -sS -o /dev/null -w '%{http_code}\n' http://sonarr:8989Код стану HTTP підтверджує, що мережевий маршрут працює. Помилка дозволення імені означає, що ім’я сервісу неправильне.
Перевірте, що жорсткі посилання справді створюються
Не вважайте налаштування правильним, доки не перевірите кількість посилань. Після імпорту одного елемента порівняйте завантажений файл із файлом у бібліотеці:
stat -c '%i %h %n' /mnt/data/torrents/tv/*/*.mkv
stat -c '%i %h %n' /mnt/data/media/Shows/*/*/*.mkvПерше число — це inode, а друге — кількість посилань. Для файлу, до якого створено жорстке посилання, в обох місцях має бути той самий inode, а кількість посилань має дорівнювати 2. Два різні inode, кожен із кількістю посилань 1, означають, що Sonarr скопіював файл. У журналі імпорту буде зазначено, що створити жорстке посилання не вдалося.
Також стежте за використанням диска. Показник df -h /mnt/data під час імпорту має майже не змінюватися, оскільки жорстке посилання додає ім’я, але не дані.
Що саме ламається
Помилки доступу під час імпорту означають, що ідентифікатор користувача контейнера не може записувати дані до папки бібліотеки. Повідомлення має вигляд Access to the path ... is denied. Перевірте за допомогою ls -ln /mnt/data/media, чи відповідає ідентифікатор власника вашому PUID. Пам’ятайте, що каталог має мати біт виконання, перш ніж контейнер зможе до нього увійти.
Файли, власником яких відображається root, означають, що контейнер запустився до створення каталогу на хості, тому Docker створив його від імені root. Зупиніть stack, chown каталог і запустіть stack знову.
Якщо після видалення торента з qBittorrent файл у бібліотеці також зник, це означає, що під час імпорту було створено копію, яку згодом видалили, або ви видалили дані, а не запис торента. У разі справжнього hardlink видалення одного імені не видаляє інше, оскільки дані звільняються лише після того, як лічильник посилань досягне нуля.
Якщо диск заповнюється швидше, ніж додаються медіафайли, це найдорожчий прояв проблеми з копіюванням. Виконайте перевірку stat вище, перш ніж купувати додаткове сховище.
Що цьому стеку потрібно від VPS
Три arr-застосунки мають невисокі вимоги. Вони опитують індексатори, записують дані в невелику базу SQLite та перейменовують файли. Сервер із 2 GB RAM без проблем запускає всі чотири контейнери. Основне навантаження створюють інші компоненти. Клієнт завантажень інтенсивно використовує дискові операції читання й запису під час роботи з великими torrent, а media server, який транскодує відео на тому самому сервері, навантажує CPU. Зберігайте медіафайли на томі з реальною пропускною здатністю та встановіть обмеження швидкості для клієнта завантажень, якщо сервер виконує інші важливі завдання. Оцінюйте потреби цих компонентів окремо, а не припускайте, що в системі достатньо вільних ресурсів: self-hosted робочий простір AFFiNE — це ще чотири контейнери з базою даних, і на сервері з 2 GB він використовує більшу частину пам’яті. Не кожен додатковий сервіс потребує стільки ресурсів: однопрофільний сервіс на кшталт self-hosted трекера тренувань openGym без проблем працює на тому самому сервері, якщо для нього налаштовано власний TLS і ви заздалегідь знаєте розташування його файлу бази даних, перш ніж доручати йому зберігання історії тренувань за рік. Будь-який стек із web-застосунком, базою даних Postgres і чергою фонових завдань за вимогами ближчий до AFFiNE. Тому вирішіть, чи має self-hosted служба підтримки Chatwoot працювати на цьому сервері, чи на окремому, перш ніж обмеження ресурсів проявиться посеред імпорту. Для стрибкоподібних навантажень потрібна ще більша обережність, оскільки під час імпорту конфлікт створює пікове, а не середнє навантаження: якщо ви розглядаєте self-hosted OneCLI, який надає кожному користувачу власний ізольований sandbox-агент, порівняйте опубліковані вимоги до ресурсів із фактично доступними ресурсами під час роботи qBittorrent на максимальній швидкості, а не зі значенням, яке free -h показує на сервері без навантаження.
FAQ
Чому Sonarr копіює файли замість створення hardlink?
Тому що з погляду контейнера джерело та призначення розташовані в різних файлових системах. Два окремі bind mount, наприклад /downloads і /tv, вважаються різними файловими системами, навіть якщо обидва походять з одного диска хоста. Змонтуйте один батьківський каталог як /data у кожному контейнері та розмістіть у ньому завантаження й медіатеку. Після цього створення посилання стане можливим. Перевірте результат за допомогою stat -c '%i %h %n' для обох файлів: вони мають мати однаковий inode і кількість посилань 2.
Які PUID і PGID слід використовувати?
Використовуйте числові id облікового запису хоста, якому належить дерево медіафайлів. Отримати їх можна за допомогою id -u і id -g. У щойно створеному Ubuntu VPS зазвичай використовується 1000 для обох значень. Усі контейнери стека мають використовувати одну й ту саму пару. Інакше один застосунок не зможе змінювати файли, створені іншим. Після зміни значень пересоздайте контейнери за допомогою docker compose up -d --force-recreate і виправте наявні файли за допомогою chown -R.
Чи потрібно відкривати ці вебінтерфейси в інтернет?
Ні, і робити цього не слід. Прив’яжіть кожен опублікований порт до 127.0.0.1 у Compose-файлі. Після цього підключайтеся до інтерфейсів через SSH-тунель або reverse proxy на сервері, який завершує TLS (transport layer security) і додає власну автентифікацію. Самої VPN недостатньо, щоб отримати доступ до порту, прив’язаного до 127.0.0.1, оскільки Docker робить його доступним лише з самого хоста. Якщо потрібен прямий доступ через VPN, опублікуйте порти на VPN-адресі сервера. Публікувати їх безпосередньо на публічному інтерфейсі гірше, ніж здається, оскільки Docker додає власні правила firewall, а правило ufw deny не зупинить такий трафік.
Де знайти пароль qBittorrent?
Образ LinuxServer.io виводить тимчасовий пароль для користувача admin у журналі запуску. Виконайте docker compose logs qbittorrent | grep -i password, щоб прочитати його, а потім задайте постійний пароль у розділі Options і Web UI. Під час кожного перезапуску генерується новий тимчасовий пароль, доки ви не задасте власний.
Чи може Jellyfin використовувати ті самі каталоги?
Так, саме для цього призначена така структура. Змонтуйте /mnt/data/media у медіасервер як /media. Тоді його бібліотеки розташовуватимуться в /media/Movies і /media/Shows, а Sonarr та Radarr записуватимуть у ці самі каталоги через /data/media. Надайте медіасерверу такі самі PUID і PGID, щоб він міг читати файли, створені стеком arr.