Deploying the Arr Stack: Getting Path Mapping Right Before Anything Else

Deploying the Arr Stack: Getting Path Mapping Right Before Anything Else

The most common arr stack failure mode doesn't surface an error. Sonarr reports the download complete. NZBGet has moved the file. The import job finishes without complaint. Nothing appears in your media server. The problem is path mapping — and it was misconfigured before the first container started.

This build covers the arr stack as two distinct groups: a consolidated media stack running NZBGet, Sonarr, Radarr, Lidarr, and Tautulli together; and a Plex stack with direct access to your media shares. The official documentation covers what each app does. This covers what to configure first, why the volume layout is load-bearing, and what to check when a download completes but nothing moves.


The volume layout that determines whether everything works

The arr stack has multiple containers writing to and reading from the same storage. Getting the volume layout wrong means path mapping will fail — and path mapping failures in this stack are silent.

This setup uses three distinct path roots:

  • /working/docker/[container] — config bind mounts, private to each container
  • /working/docker/downloads — NZBGet's download staging root; category subfolders live here
  • /working/shares/tvshows, /working/shares/movies, /working/shares/music — NFS mounts from a Synology NAS, mounted individually on the host

The NFS shares are pre-mounted on the host before any container starts. Containers bind into them directly. There's no Docker-managed NFS volume configuration here — the host owns those mounts, and containers inherit them as standard bind paths.

For those mounts to survive a reboot, they need to be in /etc/fstab. Without an fstab entry, the shares won't mount on startup, Docker will bind into empty directories, and everything will fail silently — no errors, just missing files. Add an entry for each share:

192.168.1.x:/volume1/tvshows   /working/shares/tvshows   nfs   defaults,_netdev,nofail,x-systemd.automount   0   0
192.168.1.x:/volume1/movies    /working/shares/movies    nfs   defaults,_netdev,nofail,x-systemd.automount   0   0
192.168.1.x:/volume1/music     /working/shares/music     nfs   defaults,_netdev,nofail,x-systemd.automount   0   0
192.168.1.x:/volume1/photos    /working/shares/photos    nfs   defaults,_netdev,nofail,x-systemd.automount   0   0

Replace 192.168.1.x with your Synology's IP and adjust the export paths to match your NAS volume configuration. The key options here are _netdev, which tells systemd to wait for the network before attempting the mount, and nofail, which prevents a failed mount from blocking the boot process entirely. The x-systemd.automount option defers the actual mount until something first accesses the path — useful if Docker starts before the NFS server is fully ready.

After editing fstab, test without rebooting:

mount -a

If any mount fails, the error will surface here rather than silently during boot.

Because downloads and media live on different mount points — local disk for downloads, NFS for media — hardlinking between them isn't possible. When Sonarr moves a completed download from /downloads/tvshows/ to /tv/, that's a cross-filesystem copy, not a hardlink. It costs time and disk I/O. Know that going in.

NZBGet manages its own subfolder structure under /working/docker/downloads. The top-level category folders need to exist before NZBGet tries to write to them — NZBGet will create deeper subfolders itself, but the category roots need to be there first.

Before creating directories, it's worth mapping out how paths flow through the stack so the configuration steps that follow make sense.

arr stack — download path to NFS mount

App Download path (container) NFS host path Container media path
Sonarr /downloads/tvshows /working/shares/tvshows /tv
Radarr /downloads/Movies /working/shares/movies /movies
Lidarr /downloads/music /working/shares/music /music

Plex — NFS mount to container path

NFS host path Plex container path
/working/shares/tvshows /data/tvshows
/working/shares/movies /data/movies
/working/shares/music /data/music
/working/shares/photos /data/photos

Create the host directories before any container starts. Docker won't create them, and on Rocky Linux with SELinux enforcing, a missing bind-mount target produces permission errors that look like an SELinux problem when the real cause is the directory doesn't exist.

# NZBGet category roots — NZBGet creates subfolders beneath these
mkdir -p /working/docker/downloads/tvshows
mkdir -p /working/docker/downloads/Movies
mkdir -p /working/docker/downloads/music

# Container config directories
mkdir -p /working/docker/nzbget
mkdir -p /working/docker/sonarr
mkdir -p /working/docker/radarr
mkdir -p /working/docker/lidarr
mkdir -p /working/docker/plex
mkdir -p /working/docker/tautulli

Set ownership on the downloads directory to match the PUID and PGID configured in each container. The NFS share ownership is managed on the Synology side — confirm that UID 1002 / GID 1002 has write access to those shares before proceeding.

chown -R 1002:1002 /working/docker/downloads

The PUID and PGID values must be identical across every container that touches files. If NZBGet runs as 1002:1002 and Sonarr runs as a different UID, Sonarr cannot rename or move files NZBGet downloaded. There's no permission error surfaced in Sonarr's UI — the move silently fails.


Stack 1: The arr stack

NZBGet, Sonarr, Radarr, Lidarr, and Tautulli run together in a single compose stack on INTERNAL_BRIDGE. This gives each arr app DNS-based container name resolution to NZBGet without relying on host IPs or localhost — which resolves inside each container to itself, not to any other service.

version: "3.8"

services:
  nzbget:
    container_name: nzbget
    restart: unless-stopped
    image: lscr.io/linuxserver/nzbget:latest
    volumes:
      - /working/docker/nzbget:/config:z
      - /working/docker/downloads:/downloads:z
      - /etc/localtime:/etc/localtime:ro
    environment:
      - PUID=1002
      - PGID=1002
    ports:
      - "6789:6789"
    networks:
      INTERNAL_BRIDGE:

  sonarr:
    container_name: sonarr
    restart: unless-stopped
    image: lscr.io/linuxserver/sonarr:latest
    volumes:
      - /working/docker/sonarr:/config:z
      - /working/docker/downloads:/downloads:z
      - /working/shares/tvshows:/tv
      - /etc/localtime:/etc/localtime:ro
    environment:
      - PUID=1002
      - PGID=1002
    ports:
      - "8989:8989"
    networks:
      INTERNAL_BRIDGE:

  radarr:
    container_name: radarr
    restart: unless-stopped
    image: lscr.io/linuxserver/radarr:latest
    volumes:
      - /working/docker/radarr:/config:z
      - /working/docker/downloads:/downloads:z
      - /working/shares/movies:/movies
      - /etc/localtime:/etc/localtime:ro
    environment:
      - PUID=1002
      - PGID=1002
    ports:
      - "7878:7878"
    networks:
      INTERNAL_BRIDGE:

  lidarr:
    container_name: lidarr
    restart: unless-stopped
    image: lscr.io/linuxserver/lidarr:latest
    volumes:
      - /working/docker/lidarr:/config:z
      - /working/docker/downloads:/downloads:z
      - /working/shares/music:/music
      - /etc/localtime:/etc/localtime:ro
    environment:
      - PUID=1002
      - PGID=1002
    ports:
      - "8686:8686"
    networks:
      INTERNAL_BRIDGE:

  tautulli:
    container_name: tautulli
    restart: unless-stopped
    image: lscr.io/linuxserver/tautulli:latest
    volumes:
      - /working/docker/tautulli:/config:z
      - /etc/localtime:/etc/localtime:ro
    environment:
      - PUID=1002
      - PGID=1002
      - TZ=America/New_York
    ports:
      - "8181:8181"
    networks:
      INTERNAL_BRIDGE:

networks:
  INTERNAL_BRIDGE:
    external:
      name: INTERNAL_BRIDGE

NZBGet configuration

After the container starts, open NZBGet at port 6789. Go to Settings → Paths and set:

MainDir → /downloads

That's the only path that needs to be set explicitly. DestDir, InterDir, and the other path settings default to ${MainDir}-relative values — NZBGet builds its folder structure from there automatically.

Then go to Settings → Categories and create three categories. These must match exactly what Sonarr, Radarr, and Lidarr will send:

tv     → /downloads/tvshows
movies → /downloads/Movies
music  → /downloads/music

The category name is what each arr app passes with the download request. The destination path is where NZBGet places the completed file. If Sonarr sends a download without a matching category, NZBGet drops it in the default destination. Sonarr looks in /downloads/tvshows. The file isn't there. No error is raised.

NZBGet will create its own working subfolders (like _unpack and _failed) beneath these paths as needed — you don't need to pre-create them.

Sonarr, Radarr, and Lidarr configuration

Set the root folder first — before adding any content. Go to Settings → Media Management → Root Folders:

Sonarr → /tv
Radarr → /movies
Lidarr → /music

If a root folder is wrong or missing, imports fail without a meaningful error, or completed files move to an unexpected location.

Then connect NZBGet as the download client in each app. Go to Settings → Download Clients → Add → NZBGet:

Host     → nzbget
Port     → 6789
Category → tv        (Sonarr)
           Movies    (Radarr)
           music     (Lidarr)

The host must be the container name, not localhost or a host IP. Each container has its own network namespace — localhost inside Sonarr resolves to Sonarr, not NZBGet. Because all containers are on INTERNAL_BRIDGE, Docker DNS resolves nzbget to the correct container address. If the connection test fails but you can reach NZBGet's UI directly by IP, the container is not on the shared network.

Tautulli configuration

After the container starts, open Tautulli at port 8181. Connect it to Plex via Settings → Plex Media Server. You'll need a Plex token: in Plex, go to Settings → Account → Privacy → Plex token.

Tautulli doesn't touch the filesystem — it connects to Plex via API and reads playback events only. Deploy it after confirming the core stack works end-to-end: a search, a grab, an import, and a file visible in Plex. Adding monitoring before that verification just adds noise when you're debugging.


Stack 2: Plex

Plex runs in its own stack with bind mounts to its config directory and direct access to each NFS share. Keeping it separate from the arr stack makes sense — Plex has its own hardware transcoding, GPU passthrough, and network port requirements that are cleaner to manage in isolation, and it doesn't need to be restarted when the arr stack is updated.

version: "3.8"

services:
  plex:
    container_name: plex
    restart: unless-stopped
    image: lscr.io/linuxserver/plex:latest
    volumes:
      - /working/docker/plex:/config:z
      - /working/shares/movies:/data/movies:ro
      - /working/shares/tvshows:/data/tvshows:ro
      - /working/shares/photos:/data/photos:ro
      - /working/shares/music:/data/music:ro
      - /etc/localtime:/etc/localtime:ro
    environment:
      - PUID=1002
      - PGID=1002
      - VERSION=docker
      - TZ=America/New_York
    ports:
      - "32400:32400"
    networks:
      INTERNAL_BRIDGE:

networks:
  INTERNAL_BRIDGE:
    external:
      name: INTERNAL_BRIDGE

Plex mounts all media shares read-only under /data/[sharename] — it has no reason to write to the NFS shares, and :ro makes that explicit. The arr apps write to those same shares via their own mounts on the host side. Plex just reads what they put there.

Adding libraries in Plex

After the container starts, open Plex at port 32400 and walk through the setup wizard. When prompted to add a library, select the library type, give it a name, then click Browse for media folder. Plex's file browser shows the container filesystem — not the host, not the Synology. Navigate to the /data/ directory and select the appropriate subfolder:

TV Shows → /data/tvshows
Movies   → /data/movies
Music    → /data/music
Photos   → /data/photos

The key thing to understand here is that Plex only ever sees the paths inside its own container. /data/tvshows is what the volume mount presents to Plex — the fact that it's backed by /working/shares/tvshows on the host, which is itself an NFS mount from the Synology, is invisible to Plex. If the path isn't showing up in the file browser, the volume mount isn't working — go back and confirm the NFS share is mounted on the host before troubleshooting anything inside Plex.

To add additional libraries later — or verify paths on an existing one — go to Settings → Libraries, click the three-dot menu on a library, and select Edit. The folder path shown there is always the container-side path. Use the Plex reference table above to trace it back to the corresponding NFS host path if you need to verify what's actually mounted.


When a download completes but nothing appears

Check Sonarr's Activity → Queue first. If an import is sitting in a warning state, the reason is usually there.

"Import failed, path does not exist" means the path the arr app expects doesn't match where NZBGet placed the file. Cross-reference NZBGet's category destination with Sonarr's download client path configuration — one of them is wrong. Both containers mount /working/docker/downloads as /downloads, so the paths have to agree on what lives beneath that. If the category says /downloads/tvshows and Sonarr is looking somewhere else, nothing will match.

"Already imported" or "No files found" — navigate to the source path in a shell and confirm the file exists and is readable by UID 1002. If the file is owned by root or a different UID, the rename fails without a visible error in the UI. Fix ownership with chown and trigger a manual import from Sonarr's Activity tab.

Sonarr reports a successful import but nothing appears in Plex — the issue is downstream. Trigger a library scan manually in Plex. If the file still doesn't appear after a scan, confirm the correct NFS share is mounted read-only and that the library is pointed at /data/tvshows or /data/movies as appropriate. Sonarr writes to /tv/ on its container mount — Plex reads from /data/tvshows/ on its own — both resolve to the same Synology share, but if either mount is missing or stale the chain breaks.

One thing worth noting with NFS mounts specifically: if the Synology goes offline or the NFS mount drops and remounts with different permissions, the arr apps will start failing silently again. If imports suddenly stop working after a period of stability, check that the NFS mounts are healthy on the host before chasing anything inside the containers.


Commands reference

# NZBGet category roots
mkdir -p /working/docker/downloads/tvshows
mkdir -p /working/docker/downloads/Movies
mkdir -p /working/docker/downloads/music

# arr stack config directories
mkdir -p /working/docker/nzbget
mkdir -p /working/docker/sonarr
mkdir -p /working/docker/radarr
mkdir -p /working/docker/lidarr

# Plex config directory
mkdir -p /working/docker/plex

# Tautulli config directory
mkdir -p /working/docker/tautulli

# Set ownership on downloads directory
chown -R 1002:1002 /working/docker/downloads

# Confirm INTERNAL_BRIDGE network exists
docker network ls | grep INTERNAL_BRIDGE

# Start the arr stack
cd /path/to/arr-stack
docker compose up -d

# Start the Plex stack
cd /path/to/plex-stack
docker compose up -d

# Start the Tautulli stack
cd /path/to/tautulli-stack
docker compose up -d

# Check container status
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

# Tail logs for a specific container
docker logs -f sonarr
docker logs -f nzbget

# Fix ownership if imports stop working
chown -R 1002:1002 /working/docker/downloads
Ryan Mckernan
Author

Ryan Mckernan

IT service manager & infrastructure architect turning real-world IT messes into practical, documented fixes. I build systems, streamline ops, and share field notes at Painfully Useful—tested, refined, and repeatable.