Stack arr in un unico file Docker Compose
Avvia Prowlarr, Sonarr, Radarr e qBittorrent su un VPS con un solo file Docker Compose, usando PUID, PGID e volumi condivisi per mantenere gli hardlink attivi.
Cosa stai creando
Uno stack arr Docker Compose è composto da quattro container che gestiscono una libreria multimediale: Prowlarr per la configurazione degli indexer, Sonarr per le serie, Radarr per i film e qBittorrent come client di download. I container comunicano tra loro tramite la rete Compose usando il nome del servizio e condividono lo stesso albero di directory sull'host. L'installazione è rapida. Il layout dei volumi determina se lo stack funzionerà per anni o richiederà interventi ogni settimana, quindi gran parte di questa guida è dedicata a questo aspetto.
Lo stack non cerca contenuti al posto tuo. Prowlarr gestisce gli indexer che aggiungi e la scelta degli indexer è una tua decisione e una tua responsabilità legale. Questa guida tratta la configurazione di base: utenti, percorsi, permessi, rete dei container e controlli per verificare che tutto funzioni.
Se non hai mai scritto un file Compose, leggi prima le basi di Docker Compose per un VPS. Questa guida presuppone che docker compose version restituisca già un output sul tuo server.
Perché gli hardlink non funzionano e perché questo è il punto fondamentale
Quando Sonarr termina un download, importa il file nella libreria. Se la directory dei download e quella della libreria si trovano sullo stesso filesystem, l'importazione usa un hardlink: un secondo nome che punta agli stessi dati sul disco. Non richiede spazio aggiuntivo né tempo di copia. Il torrent continua a fare seeding usando il nome originale, mentre il media server legge il nuovo nome.
Se le due directory si trovano su filesystem diversi, il kernel non può creare quel link. Sonarr ricorre quindi a una copia. Una stagione da 40 GB occupa ora 80 GB di spazio e richiede diversi minuti di operazioni di input e output. Inoltre, il log dell'importazione indica che la creazione dell'hardlink è fallita e che il file è stato copiato. Su un VPS con uno spazio disco fisso, è così che si esaurisce lo spazio nel giro di una settimana.
Il problema è questo. All'interno di un container, un bind mount costituisce un confine tra filesystem. Montate /mnt/data/torrents come /downloads e /mnt/data/media come /tv: anche se entrambe le directory risiedono sullo stesso disco dell'host, Sonarr vede due mount separati e rifiuta di creare link tra loro. La documentazione ufficiale dell'immagine LinuxServer.io lo indica chiaramente: usare separatamente i percorsi /downloads e /tv impedisce di creare hardlink.
La soluzione consiste nell'usare un solo mount. Ogni container che accede ai file multimediali riceve lo stesso volume, /mnt/data:/data, e ogni percorso utilizzato è una directory al suo interno. Un punto di mount, un filesystem e hardlink funzionanti.
Crea l'utente, il gruppo e le directory
I container scrivono i file usando un ID utente numerico, impostato da PUID e PGID. Usa il tuo account, così puoi leggere e modificare questi file tramite SSH senza sudo.
id -u
id -gEntrambi restituiscono in genere 1000 su una VPS Ubuntu appena installata. Ora crea la struttura. Posizionala sul disco che contiene i tuoi file multimediali e mantieni l'intera struttura su quel disco.
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/dataVerifica che sia effettivamente un unico filesystem prima di procedere:
df --output=source,target /mnt/data/torrents /mnt/data/mediaEntrambe le righe devono mostrare lo stesso dispositivo di origine. Se i dispositivi sono diversi, gli hard link non funzioneranno mai, indipendentemente dalla configurazione del container.
Le directory delle librerie si chiamano intenzionalmente Movies e Shows. Se esegui già Jellyfin come server multimediale, monta /mnt/data/media in Jellyfin come /media: le librerie verranno quindi create in /media/Movies e /media/Shows, esattamente come previsto da quella guida.
Il file dell’ambiente
Mantieni i valori che cambiano da un server all’altro in .env, accanto al file Compose.
mkdir -p ~/arr && cd ~/arrScrivi ~/arr/.env:
PUID=1000
PGID=1000
TZ=Etc/UTC
DATA_ROOT=/mnt/dataImposta TZ sulla tua zona, ad esempio Europe/Berlin. Le applicazioni arr pianificano le attività e registrano le righe di log in quella zona, quindi un valore errato rende confusi tutti i log in seguito.
Il file Compose
Scrivi ~/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-stoppedIn quel file, quattro elementi svolgono il lavoro effettivo.
${DATA_ROOT}:/data è identico nei tre container che accedono ai contenuti multimediali. Prowlarr non lo utilizza, perché non apre mai file multimediali.
Ogni porta web è associata a 127.0.0.1, quindi Docker la pubblica solo sull'indirizzo di loopback. Un semplice 8989:8989 la pubblicherebbe su tutte le interfacce, e le regole firewall gestite direttamente da Docker farebbero passare quel traffico oltre una regola deny di ufw. Questo comportamento sorprende spesso ed è spiegato in perché Docker pubblica le porte oltre ufw.
La porta 6881 viene pubblicata su tutte le interfacce intenzionalmente. È la porta di ascolto del torrent e deve essere raggiungibile per consentire le connessioni peer in ingresso. Anche in questo caso si applica il comportamento di Docker, quindi non serve una regola ufw e aggiungerne una non cambia nulla. La documentazione di Docker, verificata a ottobre 2026, indica che il traffico diretto a una porta pubblicata viene instradato prima di raggiungere le chain utilizzate da ufw; pertanto sudo ufw allow 6881 non ha alcun effetto su questa porta. La porta è raggiungibile da Internet non appena il container viene avviato, e una regola deny di ufw non la chiuderebbe comunque. Se in futuro devi limitare i client autorizzati a connettersi, la documentazione di Docker indica di usare regole nella chain DOCKER-USER anziché ufw. Usa ufw per i servizi che non vengono eseguiti in Docker, ad esempio SSH, e consulta le basi del firewall ufw per un VPS se non hai mai utilizzato questo strumento.
Le directory di configurazione sono separate per ogni applicazione e solo il volume dei contenuti multimediali è condiviso. Creale prima del primo avvio, in modo che appartengano al tuo utente e non a root:
mkdir -p ~/arr/config/prowlarr ~/arr/config/sonarr ~/arr/config/radarr ~/arr/config/qbittorrent
docker compose up -d
docker compose psTutti e quattro i servizi devono usare running. A ottobre 2026 queste immagini sono pubblicate su lscr.io e il tag latest segue la release stabile corrente; usa quindi un tag di versione specifico se vuoi che gli aggiornamenti siano una scelta consapevole e non una sorpresa.
Accesso sicuro alle interfacce web
Poiché le porte sono associate all’interfaccia loopback, non è ancora esposto nulla. Inoltrale tramite SSH dal tuo computer:
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-serverOra http://127.0.0.1:8989 nel browser consente di raggiungere Sonarr sul server. Per un accesso permanente, pubblica lo stack dietro Traefik con certificati TLS per più applicazioni oppure raggiungi il server tramite una VPN WireGuard gestita autonomamente. Una VPN, però, non consente di raggiungere queste porte da sola. La documentazione di Docker, verificata a ottobre 2026, indica che quando una porta viene pubblicata su un indirizzo localhost, solo l’host Docker può accedervi. Per usare la VPN, continua a eseguire il tunnel SSH attraverso di essa oppure modifica le associazioni nel file Compose da 127.0.0.1 all’indirizzo del server sull’interfaccia VPN. Nessuna di queste applicazioni dovrebbe essere esposta su Internet con davanti soltanto la propria pagina di accesso. Se scegli il reverse proxy e preferisci usare un unico account per tutte e quattro le interfacce invece di gestire quattro account applicativi distinti, Authentik fornisce il single sign-on self-hosted che Traefik può applicare a ogni richiesta tramite forward auth.
qBittorrent genera una password amministrativa casuale al primo avvio e la stampa nel log del container. Leggila, quindi modificala nell’interfaccia web:
docker compose logs qbittorrent | grep -i passwordSe non la modifichi, a ogni riavvio viene generata una nuova password casuale e dovrai consultare nuovamente i log ogni volta.
Impostare i percorsi all'interno di ogni applicazione
In qBittorrent, apri Options, quindi Downloads, e imposta il percorso predefinito di salvataggio su /data/torrents. Mantieni la cartella dei download incompleti all'interno dello stesso albero, ad esempio /data/torrents/incomplete. Un download completato in un percorso esterno a /data non può essere collegato alla libreria tramite hard link.
In Sonarr, apri Settings, quindi Media Management, e aggiungi la cartella radice /data/media/Shows. In Radarr, la cartella radice è /data/media/Movies. Questi sono percorsi interni al container. Il percorso host /mnt/data/media/Shows viene rifiutato perché quella directory non esiste dal punto di vista del container.
In Sonarr e Radarr, apri Settings, quindi Download Clients, e aggiungi qBittorrent. L'host è qbittorrent e la porta è 8080. Il nome del servizio funziona come hostname perché Compose inserisce tutti e quattro i container nella stessa rete, con un servizio DNS (domain name system) interno. Non usare localhost in questo campo: all'interno del container Sonarr, localhost è Sonarr.
Lascia vuoti i Remote Path Mappings. Questa funzione serve a convertire un percorso riportato dal client di download in un percorso visibile all'applicazione arr. Con un unico mount condiviso /data, entrambi i container utilizzano già gli stessi percorsi. Questo è il secondo motivo per cui questa configurazione è utile.
Collega Prowlarr a Sonarr e Radarr
Prowlarr trasferisce le definizioni degli indexer alle altre applicazioni, quindi configuri un indexer una sola volta invece di due. Per funzionare, richiede una chiave API (application programming interface) da ciascuna applicazione.
In Sonarr, apri Settings, quindi General, e copia la chiave API. In Prowlarr, apri Settings, quindi Apps, aggiungi un'applicazione Sonarr e compila tre campi. Prowlarr Server è http://prowlarr:9696. Sonarr Server è http://sonarr:8989. API Key è il valore copiato. Premi Test. Un risultato verde indica che Prowlarr ha raggiunto Sonarr tramite la rete Compose. Ripeti la procedura con Radarr all'indirizzo http://radarr:7878.
Un risultato rosso con l'errore di connessione rifiutata indica quasi sempre un nome del servizio errato o il prefisso http:// mancante. Verifica che il nome venga risolto dall'interno del container:
docker compose exec prowlarr curl -sS -o /dev/null -w '%{http_code}\n' http://sonarr:8989Un codice di stato HTTP dimostra che il percorso di rete funziona. Un errore di risoluzione del nome dimostra che il nome del servizio è errato.
Verifica che i collegamenti fisici vengano effettivamente creati
Non considerare completata la configurazione finché non hai verificato il numero di collegamenti. Dopo l'importazione di un elemento, confronta il file scaricato con il file nella libreria:
stat -c '%i %h %n' /mnt/data/torrents/tv/*/*.mkv
stat -c '%i %h %n' /mnt/data/media/Shows/*/*/*.mkvIl primo numero è l'inode e il secondo è il numero di collegamenti. Un file collegato tramite hardlink mostra lo stesso inode in entrambe le posizioni e un numero di collegamenti pari a 2. Due inode diversi, ciascuno con un numero di collegamenti pari a 1, indicano che Sonarr ha copiato il file. Il log dell'importazione segnalerà che la creazione dell'hardlink non è riuscita.
Controlla anche lo spazio su disco. df -h /mnt/data dovrebbe cambiare appena durante un'importazione, perché un hardlink aggiunge un nome ma non dati.
Che cosa si rompe realmente
Gli errori di autorizzazione durante l'importazione indicano che l'ID utente del container non può scrivere nella directory della libreria. Il messaggio è Access to the path ... is denied. Verifica con ls -ln /mnt/data/media che l'ID del proprietario corrisponda al tuo PUID e ricorda che le directory devono avere il bit di esecuzione prima che il container possa accedervi.
I file che risultano appartenere a root indicano che il container è stato avviato prima che esistesse la directory sull'host, quindi Docker l'ha creata come root. Arresta lo stack, chown la directory e riavvialo.
Se elimini un torrent da qBittorrent e il file della libreria scompare, significa che l'importazione era una copia poi rimossa oppure che hai eliminato i dati anziché la voce del torrent. Con un hard link reale, la rimozione di un nome lascia intatto l'altro, perché i dati vengono liberati solo quando il conteggio dei link raggiunge zero.
Un disco che si riempie più rapidamente dei contenuti multimediali aggiunti indica il problema delle copie nella sua forma più costosa. Esegui il controllo stat precedente prima di acquistare altro spazio di archiviazione.
Cosa richiede questo stack a un VPS
Le tre applicazioni arr sono leggere. Interrogano gli indexer, scrivono in un piccolo database SQLite e rinominano i file. Un server con 2 GB di RAM esegue comodamente tutti e quattro i container. Il carico arriva da altre attività. Un client per i download può saturare l'I/O del disco con torrent di grandi dimensioni, mentre un media server che transcodifica video sullo stesso host utilizza la CPU. Mantieni i file multimediali su un volume con un throughput effettivo adeguato e imposta un limite di banda nel client per i download se sul server esegui anche altre attività importanti. Considera separatamente le risorse necessarie per queste attività, invece di dare per scontato che ci sia margine sufficiente: un workspace AFFiNE self-hosted aggiunge altri quattro container con un database e, su un server con 2 GB, tende a utilizzare la maggior parte della memoria disponibile. Non tutti i servizi aggiuntivi richiedono altrettante risorse: qualcosa di specifico, come un tracker per allenamenti openGym self-hosted, condivide senza problemi il server, purché gli assegni una configurazione TLS propria e sappia dove si trova il file del database prima di affidargli un anno di storico degli allenamenti. Qualsiasi servizio che includa un'applicazione web, un database PostgreSQL e una coda di worker in background si colloca più vicino ad AFFiNE in termini di requisiti. Decidi quindi se un help desk di supporto Chatwoot self-hosted debba risiedere su questo server o su un host dedicato, prima di raggiungere il limite durante un'importazione. I carichi con picchi improvvisi richiedono ancora più cautela, perché durante un'importazione conta il picco, non la media: se stai valutando un OneCLI self-hosted che assegna a ogni persona un agent isolato in una sandbox, confronta i requisiti pubblicati con le risorse realmente libere mentre qBittorrent è in esecuzione a pieno regime, non con il valore mostrato da free -h su un server inattivo.
FAQ
Perché Sonarr copia i file invece di crearne un hard link?
Perché, dal punto di vista del container, origine e destinazione si trovano su filesystem diversi. Due bind mount separati, come /downloads e /tv, sono due filesystem distinti anche quando provengono dallo stesso disco dell'host. Monta una singola directory padre come /data in ogni container e inserisci al suo interno download e libreria: in questo modo il collegamento diventa possibile. Verifica il risultato con stat -c '%i %h %n' su entrambi i file: devono avere lo stesso inode e un numero di link pari a 2.
Quali PUID e PGID devo usare?
Usa l'ID numerico dell'account dell'host proprietario dell'albero dei media. Puoi ricavarlo con id -u e id -g. Su un VPS Ubuntu appena installato, di solito è 1000 per entrambi. Tutti i container dello stack devono usare la stessa coppia. In caso contrario, un'applicazione può creare file che un'altra non riesce a modificare. Dopo aver cambiato i valori, ricrea i container con docker compose up -d --force-recreate e correggi i file esistenti con chown -R.
Devo esporre queste interfacce web su Internet?
No, e non dovresti farlo. Associa ogni porta pubblicata a 127.0.0.1 nel file Compose. Raggiungi quindi le interfacce tramite un tunnel SSH oppure tramite un reverse proxy sul server, che gestisce la terminazione TLS (Transport Layer Security) e aggiunge una propria autenticazione. Una VPN da sola non può raggiungere una porta associata a 127.0.0.1, perché Docker la rende raggiungibile soltanto dall'host stesso. Se vuoi accedere direttamente tramite VPN, pubblica le porte sull'indirizzo VPN del server. Pubblicarle direttamente sull'interfaccia pubblica è più rischioso di quanto sembri, perché Docker inserisce proprie regole firewall e una regola deny di ufw non blocca questo traffico.
Dove trovo la password di qBittorrent?
L'immagine LinuxServer.io stampa una password temporanea per l'utente admin nel log di avvio. Esegui docker compose logs qbittorrent | grep -i password per leggerla, quindi imposta una password permanente in Options e Web UI. A ogni riavvio viene generata una nuova password temporanea finché non ne imposti una personalizzata.
Jellyfin può usare le stesse directory?
Sì, ed è proprio questo lo scopo della struttura. Monta /mnt/data/media nel media server come /media. Le relative librerie si trovano in /media/Movies e /media/Shows, mentre Sonarr e Radarr scrivono nelle stesse directory tramite /data/media. Assegna al media server gli stessi PUID e PGID, in modo che possa leggere ciò che lo stack arr scrive.