Stack arr : Docker Compose avec Prowlarr, Sonarr et Radarr
Déployez Prowlarr, Sonarr, Radarr et qBittorrent dans un seul fichier Docker Compose, avec un PUID, un PGID et des volumes partagés pour préserver les hardlinks.
Ce que vous allez construire
Une stack arr Docker Compose se compose de quatre conteneurs qui gèrent une bibliothèque multimédia : Prowlarr pour la configuration des indexeurs, Sonarr pour les séries, Radarr pour les films et qBittorrent comme client de téléchargement. Ils communiquent entre eux sur le réseau Compose en utilisant le nom du service et partagent une même arborescence de répertoires sur l’hôte. L’installation est rapide. La disposition des volumes détermine si la stack fonctionnera pendant des années ou vous posera des problèmes chaque semaine. La majeure partie de ce guide lui est donc consacrée.
La stack ne recherche pas de contenu à votre place. Prowlarr contient les indexeurs que vous y ajoutez. Le choix des indexeurs relève de votre décision et de votre responsabilité juridique. Ce guide traite de la configuration technique : utilisateurs, chemins, permissions, réseau des conteneurs et vérifications qui permettent de confirmer que tout fonctionne.
Si vous n’avez jamais écrit de fichier Compose, commencez par lire les bases de Docker Compose pour un VPS. Ce guide suppose que docker compose version affiche déjà quelque chose sur votre serveur.
Pourquoi les hardlinks échouent et pourquoi tout se joue là
Lorsque Sonarr termine le traitement d’un téléchargement, il importe le fichier dans votre bibliothèque. Si le dossier de téléchargement et le dossier de bibliothèque se trouvent sur le même système de fichiers, l’import utilise un hardlink : un second nom qui pointe vers les mêmes données sur le disque. Il ne consomme pas d’espace supplémentaire et ne prend pas de temps. Le torrent continue de seeder depuis l’ancien nom, tandis que votre serveur multimédia lit le nouveau.
Si les deux dossiers se trouvent sur des systèmes de fichiers différents, le kernel ne peut pas créer ce lien. Sonarr utilise alors une copie. Une saison de 40 GB occupe désormais 80 GB sur le disque et nécessite plusieurs minutes d’entrées-sorties. Le journal d’import indique que le hardlink a échoué et que le fichier a été copié à la place. Sur un VPS avec une capacité disque fixe, c’est ainsi que l’espace vient à manquer en une semaine.
Voici le piège. Dans un conteneur, un bind mount constitue une limite entre systèmes de fichiers. Montez /mnt/data/torrents vers /downloads et /mnt/data/media vers /tv : même si les deux répertoires se trouvent sur le même disque de l’hôte, Sonarr voit deux montages distincts et refuse de créer un lien entre eux. La documentation officielle de l’image LinuxServer.io l’indique clairement : l’utilisation de chemins séparés /downloads et /tv empêche les hardlinks.
La solution consiste à utiliser un seul montage. Chaque conteneur qui manipule des fichiers multimédias reçoit le même volume unique, /mnt/data:/data, et chaque chemin utilisé correspond à un dossier situé à l’intérieur de ce volume. Un seul point de montage, un seul système de fichiers, des hardlinks fonctionnels.
Créer l’utilisateur, le groupe et les répertoires
Les conteneurs écrivent les fichiers avec un identifiant utilisateur numérique, défini par PUID et PGID. Utilisez votre propre compte afin de pouvoir lire et modifier ces fichiers via SSH sans sudo.
id -u
id -gLes deux commandes affichent généralement 1000 sur un VPS Ubuntu neuf. Créez ensuite cette arborescence. Placez-la sur le disque qui contient vos médias et conservez toute l’arborescence sur ce même disque.
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/dataVérifiez qu’il s’agit bien d’un seul système de fichiers avant d’aller plus loin :
df --output=source,target /mnt/data/torrents /mnt/data/mediaLes deux lignes doivent afficher le même périphérique source. Deux périphériques différents signifient que les hard links ne fonctionneront jamais, quels que soient les paramètres définis dans la configuration du conteneur.
Les répertoires de bibliothèque s’appellent volontairement Movies et Shows. Si vous utilisez déjà Jellyfin comme serveur multimédia, montez /mnt/data/media dans Jellyfin sous /media ; ses bibliothèques seront alors accessibles sous /media/Movies et /media/Shows, exactement comme dans ce guide.
Le fichier d’environnement
Conservez dans .env les valeurs qui varient selon le serveur, à côté du fichier Compose.
mkdir -p ~/arr && cd ~/arrÉcrivez ~/arr/.env :
PUID=1000
PGID=1000
TZ=Etc/UTC
DATA_ROOT=/mnt/dataDéfinissez TZ avec votre propre zone, par exemple Europe/Berlin. Les applications arr planifient les tâches et horodatent les lignes de journal dans cette zone. Une valeur incorrecte rend ensuite tous les journaux difficiles à comprendre.
Le fichier Compose
Écrivez ~/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-stoppedQuatre éléments de ce fichier font réellement le travail.
${DATA_ROOT}:/data est identique dans les trois conteneurs qui accèdent aux médias. Prowlarr ne l’utilise pas, car Prowlarr n’ouvre jamais de fichier multimédia.
Chaque port web est lié à 127.0.0.1. Docker ne le publie donc que sur l’adresse loopback. Un simple 8989:8989 le publierait sur toutes les interfaces, et les règles de pare-feu de Docker feraient passer ce trafic directement devant une règle deny de ufw. Ce comportement surprend souvent et est expliqué dans pourquoi Docker publie les ports directement à travers ufw.
Le port 6881 est volontairement publié sur toutes les interfaces. C’est le port d’écoute du torrent, et il doit être accessible pour les connexions entrantes des pairs. Autorisez-le avec sudo ufw allow 6881, et consultez les bases du pare-feu ufw pour un VPS si cette commande ne vous est pas familière.
Les répertoires de configuration sont séparés pour chaque application, et seul le volume des médias est partagé. Créez-les avant le premier démarrage afin qu’ils appartiennent à votre utilisateur plutôt qu’à root :
mkdir -p ~/arr/config/prowlarr ~/arr/config/sonarr ~/arr/config/radarr ~/arr/config/qbittorrent
docker compose up -d
docker compose psLes quatre services doivent lire running. En juillet 2026, ces images sont publiées sur lscr.io et la balise latest suit la version stable actuelle. Utilisez plutôt une balise de version si vous voulez que les mises à niveau soient une décision et non une surprise.
Accéder aux interfaces web en toute sécurité
Les ports étant liés à loopback, rien n’est encore exposé. Transférez-les via SSH depuis votre propre machine :
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À présent, http://127.0.0.1:8989 dans votre navigateur ouvre Sonarr sur le serveur. Pour un accès permanent, placez la stack derrière Traefik avec des certificats TLS pour plusieurs applications, ou accédez au serveur via un VPN WireGuard que vous hébergez vous-même. Aucune de ces applications ne doit être directement exposée sur Internet avec sa seule page de connexion. Si vous choisissez le reverse proxy et préférez utiliser un seul compte pour les quatre interfaces plutôt que de gérer quatre comptes d’application distincts, Authentik fournit un SSO auto-hébergé que Traefik peut appliquer à chaque requête avec l’authentification forward.
qBittorrent génère un mot de passe administrateur aléatoire lors du premier démarrage et l’inscrit dans les logs du conteneur. Lisez-le, puis modifiez-le dans l’interface web :
docker compose logs qbittorrent | grep -i passwordSi vous ne le modifiez pas, un nouveau mot de passe aléatoire est généré à chaque redémarrage et vous devrez consulter les logs à chaque fois.
Définir les chemins dans chaque application
Dans qBittorrent, ouvrez Options, puis Downloads, et définissez /data/torrents comme chemin d’enregistrement par défaut. Conservez le dossier des téléchargements incomplets dans la même arborescence, par exemple /data/torrents/incomplete. Un téléchargement qui se termine ailleurs que dans /data ne peut pas être lié physiquement à la bibliothèque.
Dans Sonarr, ouvrez Settings, puis Media Management, et ajoutez le dossier racine /data/media/Shows. Dans Radarr, le dossier racine est /data/media/Movies. Il s’agit de chemins à l’intérieur du conteneur. Le chemin de l’hôte /mnt/data/media/Shows est refusé, car ce répertoire n’existe pas du point de vue du conteneur.
Dans Sonarr et Radarr, ouvrez Settings, puis Download Clients, et ajoutez qBittorrent. L’hôte est qbittorrent et le port est 8080. Le nom du service fonctionne comme nom d’hôte, car Compose place les quatre conteneurs sur un même réseau doté d’un service DNS (domain name system) interne. N’utilisez pas localhost ici : dans le conteneur Sonarr, localhost correspond à Sonarr.
Laissez Remote Path Mappings vide. Cette fonction sert à traduire un chemin signalé par le client de téléchargement en un chemin accessible par l’application arr. Avec un seul montage /data partagé, les deux conteneurs utilisent déjà les mêmes chemins. C’est la deuxième raison pour laquelle cette organisation est utile.
Connecter Prowlarr à Sonarr et Radarr
Prowlarr transmet les définitions des indexers aux autres applications. Vous configurez ainsi un indexer une seule fois au lieu de deux. Il lui faut une clé API (application programming interface) pour chaque application.
Dans Sonarr, ouvrez Settings, puis General, et copiez la clé API. Dans Prowlarr, ouvrez Settings, puis Apps, ajoutez une application Sonarr et renseignez trois champs. Prowlarr Server est http://prowlarr:9696. Sonarr Server est http://sonarr:8989. API Key correspond à la valeur copiée. Cliquez sur Test. Un résultat vert signifie que Prowlarr a joint Sonarr sur le réseau Compose. Répétez l’opération avec Radarr à http://radarr:7878.
Un résultat rouge indiquant que la connexion a été refusée signifie presque toujours que le nom du service est incorrect ou que le préfixe http:// est absent. Vérifiez que le nom est résolu depuis le conteneur :
docker compose exec prowlarr curl -sS -o /dev/null -w '%{http_code}\n' http://sonarr:8989Un code d’état HTTP prouve que le chemin réseau fonctionne. Une erreur de résolution de nom prouve que le nom du service est incorrect.
Vérifier que les hardlinks sont bien utilisés
Ne faites pas confiance à la configuration tant que vous n’avez pas vérifié le nombre de liens. Après l’import d’un élément, comparez le fichier téléchargé avec le fichier de la bibliothèque :
stat -c '%i %h %n' /mnt/data/torrents/tv/*/*.mkv
stat -c '%i %h %n' /mnt/data/media/Shows/*/*/*.mkvLe premier nombre est l’inode et le second est le nombre de liens. Un fichier associé par hardlink affiche le même inode aux deux emplacements et un nombre de liens égal à 2. Deux inodes différents, chacun avec un nombre de liens égal à 1, indiquent que Sonarr a copié le fichier. Le journal d’import indique alors que le hardlink a échoué.
Surveillez également l’espace disque. df -h /mnt/data devrait à peine varier lors d’un import, car un hardlink ajoute un nom, sans ajouter de données.
Ce qui provoque réellement les erreurs
Les erreurs de permission lors de l’import signifient que l’identifiant utilisateur du conteneur ne peut pas écrire dans le dossier de bibliothèque. Le message est Access to the path ... is denied. Vérifiez avec ls -ln /mnt/data/media que l’identifiant du propriétaire correspond à votre PUID, et n’oubliez pas que les répertoires doivent disposer du bit d’exécution pour que le conteneur puisse y entrer.
Des fichiers qui semblent appartenir à root indiquent que le conteneur a démarré avant que le répertoire de l’hôte existe. Docker l’a donc créé en tant que root. Arrêtez la stack, chown le répertoire, puis redémarrez-la.
Si vous supprimez un torrent dans qBittorrent et constatez que le fichier de la bibliothèque a disparu, l’import était une copie qui a ensuite été supprimée, ou vous avez supprimé les données au lieu de l’entrée du torrent. Avec un véritable lien physique, la suppression d’un nom conserve l’autre, car les données ne sont libérées que lorsque le nombre de liens atteint zéro.
Un disque qui se remplit plus vite que le volume de médias ajouté indique la forme la plus coûteuse du problème de copie. Exécutez la vérification stat indiquée plus haut avant d’acheter davantage de stockage.
Ce que cette stack nécessite sur un VPS
Les trois applications arr sont légères. Elles interrogent les indexers, écrivent dans une petite base SQLite et renomment les fichiers. Un serveur avec 2 GB de RAM exécute confortablement les quatre conteneurs. La charge vient d’ailleurs. Un client de téléchargement sature les entrées-sorties du disque avec les gros torrents, et un media server qui transcode une vidéo sur le même serveur mobilisera le CPU. Stockez les médias sur un volume offrant un débit réel et définissez une limite de bande passante sur le client de téléchargement si le serveur doit aussi exécuter d’autres services importants. Prévoyez séparément les ressources nécessaires pour ces autres services au lieu de supposer qu’il reste suffisamment de marge : un espace de travail AFFiNE auto-hébergé ajoute quatre conteneurs avec une base de données, et sur un serveur de 2 GB il veut utiliser la majeure partie de la mémoire à lui seul. Tous les services supplémentaires ne consomment pas autant : un service à usage unique comme un suivi d’entraînement openGym auto-hébergé partage facilement le serveur, à condition de lui fournir sa propre terminaison TLS et de savoir où se trouve son fichier de base de données avant de lui confier un an d’historique d’entraînement. Tout ce qui regroupe une application web, une base PostgreSQL et une file de tâches traitées en arrière-plan se situe plutôt vers le niveau de consommation d’AFFiNE. Décidez donc si un service d’assistance Chatwoot auto-hébergé doit rester sur ce serveur ou disposer de son propre serveur, avant de découvrir la limite au milieu d’un import. Les charges variables nécessitent encore plus de prudence, car c’est leur pic, et non leur moyenne, qui entre en conflit avec un import : si vous envisagez un OneCLI auto-hébergé qui fournit à chaque personne un agent isolé dans son propre sandbox, comparez ses besoins publiés avec les ressources réellement disponibles lorsque qBittorrent fonctionne à pleine vitesse, et non avec ce que free -h affiche sur un serveur inactif.
FAQ
Pourquoi Sonarr copie-t-il les fichiers au lieu de créer des liens physiques ?
La source et la destination se trouvent sur des systèmes de fichiers différents du point de vue du conteneur. Deux bind mounts distincts, comme /downloads et /tv, correspondent à deux systèmes de fichiers, même s’ils proviennent du même disque de l’hôte. Montez un répertoire parent unique sous /data dans chaque conteneur, puis placez les téléchargements et la bibliothèque à l’intérieur. Le lien physique devient alors possible. Vérifiez le résultat avec stat -c '%i %h %n' sur les deux fichiers : ils doivent avoir le même inode et un compteur de liens égal à 2.
Quels PUID et PGID dois-je utiliser ?
Utilisez l’identifiant numérique du compte de l’hôte qui possède l’arborescence des médias. Vous pouvez l’obtenir avec id -u et id -g. Sur un VPS Ubuntu fraîchement installé, il s’agit généralement de 1000 pour les deux. Tous les conteneurs de la stack doivent utiliser la même paire. Sinon, une application peut créer des fichiers qu’une autre ne pourra pas modifier. Après avoir changé ces valeurs, recréez les conteneurs avec docker compose up -d --force-recreate, puis corrigez les fichiers existants avec chown -R.
Dois-je exposer ces interfaces web sur Internet ?
Non, et vous ne devriez pas le faire. Liez chaque port publié à 127.0.0.1 dans le fichier Compose, puis accédez aux interfaces via un tunnel SSH, un VPN ou un reverse proxy avec terminaison TLS (transport layer security) et son propre mécanisme d’authentification. Les publier directement est plus risqué qu’il n’y paraît, car Docker ajoute ses propres règles de pare-feu et une règle deny de ufw n’empêchera pas ce trafic.
Où trouver le mot de passe qBittorrent ?
L’image LinuxServer.io affiche un mot de passe temporaire pour l’utilisateur admin dans son journal de démarrage. Exécutez docker compose logs qbittorrent | grep -i password pour le lire, puis définissez un mot de passe permanent dans Options et Web UI. Un nouveau mot de passe temporaire est généré à chaque redémarrage tant que vous n’avez pas défini le vôtre.
Jellyfin peut-il utiliser les mêmes dossiers ?
Oui, c’est précisément l’objectif de cette organisation. Montez /mnt/data/media dans votre serveur multimédia sous /media. Ses bibliothèques se trouvent alors dans /media/Movies et /media/Shows, tandis que Sonarr et Radarr écrivent dans ces mêmes répertoires via /data/media. Attribuez au serveur multimédia les mêmes PUID et PGID afin qu’il puisse lire les fichiers créés par la stack arr.