Arr stack in Docker Compose: handleiding voor VPS
Bouw een stabiele stack met Prowlarr, Sonarr, Radarr en qBittorrent. Leer hoe u PUID, PGID en volumes correct configureert zodat hardlinks werken en uw media perfect worden beheerd.
Wat u gaat bouwen
Een Docker Compose arr-stack bestaat uit vier containers die een mediabibliotheek beheren: Prowlarr voor indexer-instellingen, Sonarr voor series, Radarr voor films en qBittorrent als downloadclient. Ze communiceren met elkaar via het Compose-netwerk op basis van hun servicenaam en delen één mappenstructuur op de host. De installatie is kort. Het onderdeel dat bepaalt of de stack jarenlang probleemloos werkt of wekelijks voor problemen zorgt, is de volume-indeling; daarom richt deze handleiding zich grotendeels daarop.
De stack zoekt niet uit zichzelf naar content. Prowlarr bevat de indexers die u zelf toevoegt; welke indexers u gebruikt, is uw eigen beslissing en uw juridische verantwoordelijkheid. Deze handleiding behandelt het leidingwerk: gebruikers, paden, rechten, container-netwerken en de controles die aantonen dat alles werkt.
Als u nog nooit een Compose-bestand heeft geschreven, lees dan eerst de Docker Compose basisprincipes voor een VPS. Dit artikel gaat ervan uit dat docker compose version al output geeft op uw server.
Waarom hardlinks niet werken en waarom dat cruciaal is
Wanneer Sonarr een download voltooit, importeert het programma het bestand in uw bibliotheek. Als de downloadmap en de bibliotheekmap zich op hetzelfde bestandssysteem bevinden, wordt de import uitgevoerd als een hardlink: een tweede naam die naar dezelfde gegevens op de schijf verwijst. Dit kost geen extra ruimte en geen tijd. De torrent blijft seeden vanaf de oude naam, terwijl uw mediaserver het bestand vanaf de nieuwe naam leest.
Als de twee mappen op verschillende bestandssystemen staan, kan de kernel die link niet aanmaken. Sonarr valt dan terug op een kopieeractie. Een seizoen van 40 GB neemt dan 80 GB schijfruimte in beslag en kost enkele minuten aan lees- en schrijfbewerkingen. In het importlogboek wordt vastgelegd dat de hardlink is mislukt en dat het bestand in plaats daarvan is gekopieerd. Op een VPS met een beperkte schijfcapaciteit is dit de reden waarom gebruikers binnen een week zonder schijfruimte komen te zitten.
Dit is de valstrik. Binnen een container vormt een bind mount een grens voor het bestandssysteem. Mount /mnt/data/torrents als /downloads en /mnt/data/media als /tv; hoewel beide op dezelfde hostschijf staan, ziet Sonarr twee afzonderlijke mounts en weigert het om een link tussen beide te maken. De officiële documentatie van de LinuxServer.io-image vermeldt dit expliciet: het gebruik van de afzonderlijke paden /downloads en /tv maakt hardlinks onmogelijk.
De oplossing is één mount. Elke container die media verwerkt, krijgt hetzelfde enkele volume, /mnt/data:/data, en elk pad dat ze gebruiken is een map daarbinnen. Eén mountpoint, één bestandssysteem, werkende hardlinks.
De gebruiker, de groep en de mappen aanmaken
De containers schrijven bestanden als een numeriek user id, ingesteld door PUID en PGID. Gebruik uw eigen account zodat u deze bestanden via SSH kunt lezen en bewerken zonder sudo.
id -u
id -gBeide commando's tonen doorgaans 1000 op een nieuwe Ubuntu VPS. Bouw nu de mappenstructuur op. Plaats deze op de schijf waar uw media staat en houd de volledige structuur op die ene schijf.
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/dataControleer of het daadwerkelijk één bestandssysteem is voordat u verdergaat:
df --output=source,target /mnt/data/torrents /mnt/data/mediaBeide regels moeten hetzelfde bronapparaat tonen. Twee verschillende apparaten betekenen dat hardlinks nooit zullen werken, ongeacht wat u instelt in de containerconfiguratie.
De bibliotheekmappen zijn bewust Movies en Shows genoemd. Als u al Jellyfin als uw mediaserver gebruikt, mount dan /mnt/data/media in Jellyfin als /media; de bibliotheken komen dan terecht in /media/Movies en /media/Shows, precies op de locatie die in die handleiding wordt beschreven.
Het omgevingsbestand
Houd de waarden die per server verschillen in .env, naast het Compose-bestand.
mkdir -p ~/arr && cd ~/arrSchrijf ~/arr/.env:
PUID=1000
PGID=1000
TZ=Etc/UTC
DATA_ROOT=/mnt/dataStel TZ in op uw eigen zone, zoals Europe/Berlin. De arr-applicaties plannen taken en voorzien logregels van een tijdstempel in die zone; een onjuiste waarde maakt elk logboek achteraf onduidelijk.
Het Compose-bestand
Schrijf ~/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-stoppedVier zaken in dat bestand verrichten het eigenlijke werk.
${DATA_ROOT}:/data is identiek in de drie containers die media benaderen. Prowlarr krijgt dit niet, omdat Prowlarr nooit een mediabestand opent.
Elke webpoort is gebonden aan 127.0.0.1, waardoor Docker deze alleen op het loopback-adres publiceert. Een simpele 8989:8989 zou deze op elke interface publiceren, en de eigen firewallregels van Docker zouden dat verkeer direct langs een ufw deny-regel leiden. Dat gedrag verrast mensen constant, en het wordt uitgelegd in waarom Docker poorten direct door ufw heen publiceert.
Poort 6881 wordt bewust op alle interfaces gepubliceerd. Dit is de luisterpoort voor torrents, en deze moet bereikbaar zijn voor inkomende peer-verbindingen. Sta deze toe met sudo ufw allow 6881, en lees de basisprincipes van de ufw-firewall voor een VPS als dat commando nieuw voor u is.
De configuratiemappen zijn per applicatie gescheiden en alleen het media-volume wordt gedeeld. Maak deze aan vóór de eerste start, zodat ze eigendom zijn van uw gebruiker in plaats van root:
mkdir -p ~/arr/config/prowlarr ~/arr/config/sonarr ~/arr/config/radarr ~/arr/config/qbittorrent
docker compose up -d
docker compose psAlle vier de services zouden running moeten lezen. Sinds juli 2026 worden deze images gepubliceerd op lscr.io en de latest-tag volgt de huidige stabiele release. Pin daarom een versie-tag als u wilt dat upgrades een bewuste keuze zijn in plaats van een verrassing.
Veilige toegang tot de webinterfaces
Omdat de poorten op loopback staan, is er nog niets blootgesteld. Forward deze via SSH vanaf uw eigen 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-serverNu bereikt http://127.0.0.1:8989 in uw browser Sonarr op de server. Voor permanente toegang plaatst u de stack achter Traefik met TLS-certificaten voor meerdere applicaties, of bereikt u de server via een zelfgehoste WireGuard VPN. Geen van deze applicaties hoort direct op het openbare internet te staan met enkel een eigen inlogpagina als beveiliging. Als u kiest voor een reverse proxy en de voorkeur geeft aan één account voor alle vier de interfaces in plaats van vier afzonderlijke inloggegevens, biedt Authentik zelfgehoste single sign-on die Traefik op elk verzoek kan afdwingen via forward auth.
qBittorrent genereert bij de eerste start een willekeurig administratorwachtwoord en toont dit in het containerlogboek. Lees dit uit en wijzig het vervolgens in de webinterface:
docker compose logs qbittorrent | grep -i passwordAls u deze wijziging overslaat, wordt er bij elke herstart een nieuw willekeurig wachtwoord gegenereerd en zult u telkens opnieuw de logboeken moeten raadplegen.
Set the paths inside each application
In qBittorrent, open Options, then Downloads, and set the default save path to /data/torrents. Keep the incomplete-downloads folder inside the same tree, such as /data/torrents/incomplete. A download that finishes anywhere outside /data cannot be hardlinked into the library.
In Sonarr, open Settings, then Media Management, and add the root folder /data/media/Shows. In Radarr the root folder is /data/media/Movies. These are paths inside the container. The host path /mnt/data/media/Shows is rejected, because that directory does not exist from the container's point of view.
In both Sonarr and Radarr, open Settings, then Download Clients, and add qBittorrent. The host is qbittorrent and the port is 8080. The service name works as a hostname because Compose puts all four containers on one network with an internal DNS (domain name system) service. Do not use localhost here: inside the Sonarr container, localhost is Sonarr.
Leave Remote Path Mappings empty. That feature exists to translate a path the download client reports into a path the arr application can see. With one shared /data mount, both containers already agree on every path, which is the second reason this layout is worth the effort.
Prowlarr koppelen aan Sonarr en Radarr
Prowlarr pusht indexer-definities naar de andere applicaties, waardoor u een indexer slechts eenmaal hoeft te configureren in plaats van tweemaal. Hiervoor is een API (application programming interface) key van elke applicatie vereist.
Open in Sonarr de instellingen (Settings), ga naar General en kopieer de API key. Open in Prowlarr de instellingen (Settings), ga naar Apps, voeg een Sonarr-applicatie toe en vul de drie velden in. Prowlarr Server is http://prowlarr:9696. Sonarr Server is http://sonarr:8989. API Key is de waarde die u zojuist heeft gekopieerd. Druk op Test. Een groen resultaat betekent dat Prowlarr Sonarr heeft bereikt via het Compose-netwerk. Herhaal dit voor Radarr op http://radarr:7878.
Een rood resultaat met de melding dat de verbinding is geweigerd (connection refused), betekent bijna altijd dat de servicenaam onjuist is of dat het http://-voorvoegsel ontbreekt. Controleer of de naam wordt omgezet vanuit de container:
docker compose exec prowlarr curl -sS -o /dev/null -w '%{http_code}\n' http://sonarr:8989Een HTTP-statuscode bewijst dat het netwerkpad correct is. Een fout bij de naamomzetting bewijst dat de servicenaam onjuist is.
Controleer of hardlinks daadwerkelijk worden toegepast
Vertrouw de configuratie pas als u het link-aantal heeft gecontroleerd. Vergelijk na het importeren van één item het gedownloade bestand met het bestand in de bibliotheek:
stat -c '%i %h %n' /mnt/data/torrents/tv/*/*.mkv
stat -c '%i %h %n' /mnt/data/media/Shows/*/*/*.mkvHet eerste getal is de inode en het tweede is het link-aantal. Een bestand dat via een hardlink is gekoppeld, toont in beide gevallen dezelfde inode en een link-aantal van 2. Twee verschillende inodes, elk met een link-aantal van 1, betekenen dat Sonarr het bestand heeft gekopieerd; het importlogboek zal in dat geval vermelden dat de hardlink is mislukt.
Houd ook de schijfactiviteit in de gaten. df -h /mnt/data zou nauwelijks moeten veranderen wanneer een import plaatsvindt, omdat een hardlink enkel een naam toevoegt en geen data kopieert.
Wat er daadwerkelijk misgaat
Permissiefouten bij het importeren betekenen dat de user id van de container niet naar de bibliotheekmap kan schrijven. De melding is Access to the path ... is denied. Controleer met ls -ln /mnt/data/media of de eigenaar-id overeenkomt met uw PUID, en onthoud dat mappen het execute-bit nodig hebben voordat de container ze kan openen.
Bestanden die eigendom lijken te zijn van root betekenen dat de container is gestart voordat de hostmap bestond, waardoor Docker deze als root heeft aangemaakt. Stop de stack, chown de map en start deze opnieuw.
Wanneer u een torrent uit qBittorrent verwijdert en het bibliotheekbestand is verdwenen, betekent dit dat de import een kopie was die later is verwijderd, of dat u de data in plaats van het torrent-item heeft verwijderd. Bij een echte hardlink blijft bij het verwijderen van de ene naam de andere intact, omdat de data pas wordt vrijgegeven wanneer het link-aantal nul bereikt.
Een schijf die sneller volloopt dan de media die u heeft toegevoegd, is het kopieerprobleem in zijn meest kostbare vorm. Voer de stat-controle hierboven uit voordat u meer opslagruimte aanschaft.
Wat deze stack vereist van een VPS
De drie arr-applicaties zijn licht. Ze pollen indexers, schrijven naar een kleine SQLite-database en hernoemen bestanden. Een server met 2 GB RAM kan alle vier containers probleemloos uitvoeren. De belasting ontstaat elders. Een downloadclient kan bij grote torrents de schijf volledig belasten met lees- en schrijfbewerkingen, en een mediaserver die video transcodeert op dezelfde machine zal de CPU belasten. Sla media op een volume met een hoge werkelijke doorvoersnelheid op en stel een bandbreedtelimiet in op de downloadclient als de server ook andere taken uitvoert die voor u belangrijk zijn.
Reserveer afzonderlijk capaciteit voor die andere taken. Ga er niet van uit dat deze ruimte beschikbaar is: een zelfgehoste AFFiNE-werkruimte bestaat uit nog vier containers met daarachter een database, en op een machine met 2 GB RAM gebruikt deze werkruimte het grootste deel van het geheugen zelf. Niet elke extra service vereist zoveel: iets met één doel, zoals een zelfgehoste openGym-workouttracker, werkt probleemloos naast de andere services, zolang u hiervoor een eigen TLS-configuratie gebruikt en weet waar het databasebestand staat voordat u de service vertrouwt met een jaar aan trainingsgeschiedenis. Alles met een webapplicatie, een PostgreSQL-database en een achtergrondwachtrij voor workers bevindt zich dichter bij het AFFiNE-uiteinde van dat bereik. Bepaal daarom voordat u de limiet halverwege een import bereikt of een zelfgehoste Chatwoot-supportdesk op deze server hoort te draaien of een eigen server nodig heeft. Bij piekbelastingen is nog meer voorzichtigheid nodig, omdat de piekbelasting en niet het gemiddelde de import kan hinderen: als u overweegt een zelfgehoste OneCLI waarbij elke gebruiker een eigen geïsoleerde agent krijgt, vergelijk dan de gepubliceerde sizinggegevens met de werkelijk beschikbare capaciteit terwijl qBittorrent maximaal belast wordt, en niet met wat free -h toont op een niet-belaste machine.
FAQ
Waarom kopieert Sonarr bestanden in plaats van hardlinks te gebruiken?
Omdat de bron en de bestemming zich vanuit het perspectief van de container op verschillende bestandssystemen bevinden. Twee afzonderlijke bind mounts, zoals /downloads en /tv, worden gezien als twee bestandssystemen, zelfs als ze beide van dezelfde hostschijf komen. Mount één bovenliggende map als /data in elke container en plaats downloads en de bibliotheek daarbinnen; dan wordt de link mogelijk. Bevestig het resultaat met stat -c '%i %h %n' op beide bestanden: ze moeten hetzelfde inode-nummer en een link-count van 2 hebben.
Welke PUID en PGID moet ik gebruiken?
Gebruik de numerieke ID van het host-account dat eigenaar is van de mediaboom; deze vindt u met id -u en id -g. Op een nieuwe Ubuntu VPS is dit meestal 1000 voor beide. Elke container in de stack moet hetzelfde paar gebruiken, anders schrijft de ene applicatie bestanden die de andere niet kan wijzigen. Nadat u de waarden heeft gewijzigd, maakt u de containers opnieuw aan met docker compose up -d --force-recreate en herstelt u de bestaande bestanden met chown -R.
Moet ik deze webinterfaces blootstellen aan het internet?
Nee, en dat moet u ook niet doen. Bind elke gepubliceerde poort aan 127.0.0.1 in het Compose-bestand en benader de interfaces vervolgens via een SSH-tunnel, een VPN of een reverse proxy die TLS (transport layer security) afhandelt en eigen authenticatie toevoegt. Ze direct publiceren is riskanter dan het lijkt, omdat Docker eigen firewallregels invoegt en een ufw deny-regel dat verkeer niet blokkeert.
Waar vind ik het wachtwoord voor qBittorrent?
De LinuxServer.io-image print een tijdelijk wachtwoord voor de gebruiker admin in het opstartlogboek. Voer docker compose logs qbittorrent | grep -i password uit om dit te lezen en stel vervolgens een permanent wachtwoord in onder Options en Web UI. Bij elke herstart wordt een nieuw tijdelijk wachtwoord gegenereerd totdat u uw eigen wachtwoord instelt.
Kan Jellyfin dezelfde mappen gebruiken?
Ja, en dat is het doel van deze indeling. Mount /mnt/data/media in uw mediaserver als /media; de bibliotheken bevinden zich dan op /media/Movies en /media/Shows, terwijl Sonarr en Radarr via /data/media naar diezelfde mappen schrijven. Geef de mediaserver dezelfde PUID en PGID zodat deze kan lezen wat de arr-stack schrijft.