Build and Test a Plex, Prowlarr, Sonarr, Radarr, SABnzbd, and Tdarr Stack
Rights, lawful use, and scope: This article is for administering a private media server, organizing media you own or are authorized to use, preserving backups, and testing your own infrastructure. It is not legal advice and it is not a guide to finding, acquiring, sharing, or bypassing access controls for copyrighted works. Do not use Plex, Sonarr, Radarr, Prowlarr, SABnzbd, Tdarr, lists, indexers, or download clients to download, store, process, or distribute content unless you have the legal right and service permission to do so.
Quick Answer
Build the stack in gates: validate Ubuntu, storage, identity, and Compose; make Prowlarr, SABnzbd, Sonarr, and Radarr ready; prove one authorized TV and movie handoff; add Plex playback; then run a reversible Tdarr canary. Use one consistent /data tree for downloads and Arr imports, keep admin UIs on localhost or a VPN, and record backups plus image references. A running container is only the first gate, not end-to-end proof.
Safe default: Build this LAN-only or VPN-only first. Do not port-forward Sonarr, Radarr, Prowlarr, SABnzbd, Tdarr, NAS admin pages, or reverse-proxy admin pages. These are control-plane apps that can reveal secrets, change libraries, trigger jobs, delete files, or expose host paths.
Evidence status: This revision was checked against current Docker, Servarr, LinuxServer.io, Tdarr, and Plex documentation on August 24, 2026. TechGeeks did not build a fresh Ubuntu host, pull these images, run an authorized item through the workflow, measure performance, reboot the stack, or restore a database. Every output and success state below is explicitly an unperformed example or acceptance criterion.
What You Are Building
Prowlarr manages and tests lawful indexer definitions. Sonarr and Radarr decide what belongs in TV and movie libraries. SABnzbd handles authorized jobs and completion. Sonarr and Radarr import completed files into final folders. Plex serves those folders. Tdarr changes final files only after the basic workflow is stable. The reader's intent is not merely to launch six containers; it is to prove every handoff and know where to stop when one fails.
Click each step to see what must be true before moving on. The goal is one clean authorized import before broad automation.
1. FoundationUbuntu + Docker
Patch the host, install Docker or native packages, and confirm the server can reboot cleanly.
2. Paths/data Layout
Create shared download, media, and cache paths before apps start creating files with mismatched ownership.
3. SourcesProwlarr
Add only lawful sources and sync indexers outward. Prowlarr finds candidates; it does not decide what belongs in the library.
4. DownloadsSABnzbd
Use category folders for movies and TV. Keep incomplete work away from final Plex library roots.
5. DecisionsSonarr / Radarr
Set roots, profiles, categories, and one controlled test item before enabling lists or broad searching.
6. PlaybackPlex
Point Plex at final media only, then confirm it can see the imported test item without permission fixes.
7. Optimize LaterTdarr
Add Tdarr only after imports work. Start with one test file, validate output, then scale carefully.
Use Five Acceptance Gates
| Gate | Question | Evidence |
|---|---|---|
| Running | Did the container process start? | docker compose ps and startup logs |
| Ready | Can the app answer its UI/API and finish startup? | HTTP check, application health page, no migration or permission error |
| Configured | Are paths, credentials, profiles, categories, and app URLs correct? | Sanitized settings ledger and each app's own Test action |
| Functional | Does one authorized item cross the handoff? | Correlated queue, import, scan, and playback records |
| Recoverable | Can the state survive restart, update, and restore? | Reboot check, pinned image record, backup and isolated restore artifact |
Docker Compose starts containers in dependency order when dependencies are declared, but the current Docker documentation is explicit that "running" is not the same as "ready." This stack therefore uses manual readiness gates. Do not add a copied healthcheck until you verify that the selected image contains the probe tool and that the endpoint represents application readiness for that version.
Prerequisites and Private Change Ledger
- A currently supported 64-bit Ubuntu release for Docker Engine, with security updates applied.
- A wired host with enough CPU, memory, and storage for the chosen workloads. No performance figure is assumed here.
- Local storage or a mounted NAS for final media, plus local SSD/NVMe where practical for incomplete downloads and transcode cache.
- A router reservation or stable address and working Secure Shell (SSH) access.
- A non-public backup target with room for app configuration and database versions.
- A small personal, public-domain, open-licensed, or otherwise authorized movie and series sample.
- Legal accounts for any services you connect. This guide does not select providers or bypass terms.
Record the environment before installation. These commands are examples and were not run for this revision.
date -u
. /etc/os-release
printf 'OS=%s %s\n' "$NAME" "$VERSION_ID"
uname -r
id
findmnt -T /data 2>/dev/null || true
df -hT /opt /data 2>/dev/null || true
Example expected output shape (not observed): a UTC timestamp, Ubuntu name/version, kernel release, UID/GID/group list, and the filesystem/device backing each intended path. Record real output privately; do not substitute the example for your host.
Install Docker Engine from the Current Ubuntu Repository
The repository and package syntax below matches Docker's official Ubuntu instructions checked on August 24, 2026. Reopen that source before implementation because supported Ubuntu releases and package versions change.
sudo apt update
sudo apt install -y ca-certificates curl acl jq tar sqlite3 \
mediainfo cifs-utils nfs-common ufw
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo docker run --rm hello-world
sudo docker compose version
Example expected state (not observed): both final commands exit with status 0; the first prints Docker's test confirmation and the second prints an installed Compose version. Do not copy a version from this article into a lab record. If you add an administrator to the docker group, treat that membership as root-equivalent access and limit it to trusted users.
Create One Storage and Identity Model
Downloads and final Arr libraries should live under one logical /data tree. Sonarr and Radarr receive that tree as one bind mount, which preserves a single in-container filesystem view for hardlinks or atomic moves when the host paths are also on one filesystem. Prowlarr needs no media mount. Plex and Tdarr need only final media plus their cache.
/opt/media-stack
.env
compose.yml
plex/config
prowlarr/config
sabnzbd/config
sonarr/config
radarr/config
tdarr/server
tdarr/configs
tdarr/logs
/data
downloads/incomplete
downloads/usenet/movies
downloads/usenet/tv
media/movies
media/tv
media/tdarr-canary
cache/plex
cache/tdarr
sudo groupadd -f media
sudo usermod -aG media "$USER"
sudo install -d -o "$USER" -g media -m 2775 /opt/media-stack
sudo install -d -o "$USER" -g media -m 2775 \
/data/downloads/incomplete \
/data/downloads/usenet/movies \
/data/downloads/usenet/tv \
/data/media/movies \
/data/media/tv \
/data/media/tdarr-canary \
/data/cache/plex \
/data/cache/tdarr
sudo chgrp -R media /data
sudo chmod -R g+rwX /data
sudo find /data -type d -exec chmod 2775 {} +
touch /data/media/.media_mount_marker
ls -ld /data /data/downloads /data/media /data/cache
Example expected state (not observed): the listed directories use the media group and have group read/write/execute access; new subdirectories inherit the group. The marker represents valid local storage. For NAS storage, the next block removes that underlying local marker before mounting and creates a replacement only on the verified remote filesystem. Do not run recursive ownership changes against an existing library until you understand its current NAS ACLs, UID/GID mapping, snapshots, and backup state.
NAS Mount Gate
If final media is on a NAS, mount it at /data/media and prove the mount before containers start. Use either CIFS/SMB or NFS according to the NAS design. Keep credentials outside the Compose file, restrict their mode, and do not publish them.
# CIFS example: replace every YOUR_* placeholder.
sudo install -m 600 /dev/null /root/.smb-media
sudoedit /root/.smb-media
# File contents:
# username=YOUR_NAS_USER
# password=YOUR_NAS_PASSWORD
# domain=WORKGROUP
mount_media() {
local media_gid
media_gid="$(getent group media | cut -d: -f3)"
sudo rm -f /data/media/.media_mount_marker
sudo mount -t cifs //YOUR_NAS/media /data/media \
-o "credentials=/root/.smb-media,uid=$(id -u),gid=$media_gid,dir_mode=0775,file_mode=0664,vers=3.1.1" \
|| return 1
test "$(findmnt -rn -T /data/media -o SOURCE)" = '//YOUR_NAS/media' \
|| return 1
test "$(findmnt -rn -T /data/media -o FSTYPE)" = 'cifs' \
|| return 1
touch /data/media/.media_mount_marker
test -f /data/media/.media_mount_marker
}
mount_media
findmnt --target /data/media
Example expected state (not observed): findmnt identifies the intended remote source and filesystem, and test exits 0. Confirm the NAS supports the chosen SMB dialect. Use a systemd mount unit or reviewed /etc/fstab entry after the manual test, and require the marker before the stack starts. An empty local mountpoint is not a valid library.
Create the Environment and Compose Files
cd /opt/media-stack
MEDIA_UID="$(id -u)"
MEDIA_GID="$(getent group media | cut -d: -f3)"
cat > .env <<EOF
PUID=$MEDIA_UID
PGID=$MEDIA_GID
TZ=America/Chicago
UMASK=002
PLEX_CLAIM=
EOF
chmod 600 .env
Replace the timezone. A Plex claim token is temporary; obtain it only when needed, remove it after the server is claimed, and never publish it. Arr API keys, SABnzbd keys, NAS credentials, provider settings, and app passwords belong in protected configuration, not this file or a public repository.
cd /opt/media-stack
cat > compose.yml <<'EOF'
services:
plex:
image: lscr.io/linuxserver/plex:latest
container_name: plex
network_mode: host
env_file: .env
environment:
VERSION: docker
PLEX_CLAIM: ${PLEX_CLAIM:-}
volumes:
- /opt/media-stack/plex/config:/config
- /data/media/movies:/movies
- /data/media/tv:/tv
- /data/cache/plex:/transcode
restart: unless-stopped
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
container_name: prowlarr
env_file: .env
volumes:
- /opt/media-stack/prowlarr/config:/config
ports:
- "127.0.0.1:9696:9696"
networks: [media]
restart: unless-stopped
sabnzbd:
image: lscr.io/linuxserver/sabnzbd:latest
container_name: sabnzbd
env_file: .env
volumes:
- /opt/media-stack/sabnzbd/config:/config
- /data/downloads:/data/downloads
ports:
- "127.0.0.1:8080:8080"
networks: [media]
restart: unless-stopped
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
env_file: .env
volumes:
- /opt/media-stack/sonarr/config:/config
- /data:/data
ports:
- "127.0.0.1:8989:8989"
networks: [media]
restart: unless-stopped
radarr:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr
env_file: .env
volumes:
- /opt/media-stack/radarr/config:/config
- /data:/data
ports:
- "127.0.0.1:7878:7878"
networks: [media]
restart: unless-stopped
tdarr:
image: ghcr.io/haveagitgat/tdarr:latest
container_name: tdarr
env_file: .env
environment:
serverIP: 0.0.0.0
serverPort: 8266
webUIPort: 8265
internalNode: "true"
inContainer: "true"
ffmpegVersion: "7"
nodeName: MainNode
auth: "false"
volumes:
- /opt/media-stack/tdarr/server:/app/server
- /opt/media-stack/tdarr/configs:/app/configs
- /opt/media-stack/tdarr/logs:/app/logs
- /data/media:/media
- /data/cache/tdarr:/temp
ports:
- "127.0.0.1:8265:8265"
- "127.0.0.1:8266:8266"
networks: [media]
restart: unless-stopped
networks:
media:
name: media
EOF
sudo docker compose -f compose.yml config --quiet
printf 'compose_config_exit=%s\n' "$?"
sudo docker compose -f compose.yml pull
sudo docker compose -f compose.yml images
Example expected output (not observed):
compose_config_exit=0
CONTAINER REPOSITORY TAG IMAGE ID SIZE
sonarr lscr.io/linuxserver/sonarr latest <IMAGE_ID> <SIZE>
...
The image rows are illustrative placeholders. latest makes the file easy to read but is not a rollback plan. Before production, record the actual image IDs and immutable repository digests, back up each app, then pin the references you tested. Recheck every selected image's current variables, architecture, devices, and migration notes. The guide retains sudo docker because a fresh official installation does not grant the current user Docker access; membership in the docker group is root-equivalent. Docker's unless-stopped policy also starts containers after the daemon restarts, so the dedicated-host boot gate later in this guide prevents the daemon from starting before final media is mounted. Tdarr authentication is disabled only because both Tdarr ports are bound to host localhost during first setup; enable supported authentication before any broader exposure.
Host-to-Container Mount Map
| Service | Host path | Container path | Reason |
|---|---|---|---|
| SABnzbd | /data/downloads | /data/downloads | Incomplete and completed staging only |
| Sonarr/Radarr | /data | /data | One view of downloads and media for imports, hardlinks, or atomic moves |
| Plex | /data/media/movies, /data/media/tv | /movies, /tv | Read final libraries, never download staging |
| Tdarr | /data/media | /media | Process only final media and the isolated canary folder |
| Prowlarr | None | None | Indexer management does not require media access |
Remote path mappings should normally be unnecessary on this single-host layout because SABnzbd, Sonarr, and Radarr agree on /data/downloads. A remote path map is not a substitute for inconsistent mounts or permissions.
Gate 1: Start and Reach the Control Apps
cd /opt/media-stack
test -f /data/media/.media_mount_marker || \
{ echo 'media mount marker missing' >&2; exit 1; }
sudo docker compose -f compose.yml up -d prowlarr sabnzbd sonarr radarr
sudo docker compose -f compose.yml ps
sudo docker compose -f compose.yml logs --tail=80 \
prowlarr sabnzbd sonarr radarr
Example expected state (not observed): the marker test exits 0, each container is running, and logs have no database migration, bind, DNS, or permission failure. Running still does not prove ready. Open an SSH tunnel from the trusted workstation:
ssh \
-L 9696:127.0.0.1:9696 \
-L 8080:127.0.0.1:8080 \
-L 8989:127.0.0.1:8989 \
-L 7878:127.0.0.1:7878 \
-L 8265:127.0.0.1:8265 \
YOUR_ADMIN_USER@YOUR_SERVER_IP
With that session open, visit localhost ports 9696, 8080, 8989, and 7878. Set application authentication before any wider access. Confirm each settings page loads, the app reports no unexplained health warning, and its config folder remains populated after a container restart.
Gate 2: Configure Every Internal Handoff
| Owner | Setting | Value | Acceptance check |
|---|---|---|---|
| SABnzbd | Temporary folder | /data/downloads/incomplete | Writable by the configured UID/GID |
| SABnzbd | Completed/category folders | tv to /data/downloads/usenet/tv; movies to /data/downloads/usenet/movies | Test jobs remain outside final media |
| Prowlarr | Sonarr/Radarr app URLs | http://sonarr:8989, http://radarr:7878 | Each Prowlarr app Test passes |
| Sonarr | Root folder | /data/media/tv | Root folder is writable and not a download path |
| Radarr | Root folder | /data/media/movies | Root folder is writable and not a download path |
| Sonarr/Radarr | SABnzbd URL | http://sabnzbd:8080 | Download-client Test passes with matching category |
| Sonarr/Radarr | Completed download handling | Enabled after paths/categories pass | One authorized item imports without manual move |
Add one lawful indexer definition in Prowlarr, test it there, sync it to one app, and confirm its categories in the generated app entry. Do not add many sources to hide a failed category, URL base, API key, or profile. Use the dedicated Prowlarr troubleshooting guide when the indexer passes in Prowlarr but fails in Sonarr or Radarr.
Automation warning: Lists and indexers are intake controls, not permission controls. A list item, search result, or available file is not evidence that you have the right to obtain or process a work. Leave search-on-add disabled for new lists, tag list additions, review them before searching, and remove or exclude items you are not authorized to use.
Gate 3: Trace One Authorized Movie and Series
- Choose a small authorized movie and series sample whose filenames and titles will not expose household history.
- Run an interactive search in the owning Arr app and inspect accepted and rejected results.
- Send one authorized result to SABnzbd and confirm category
moviesortv. - Record the completed path and UTC time before import.
- Confirm Sonarr or Radarr imports into the final root without manual permission repair.
- Confirm the source was moved or retained exactly as the download-client policy requires.
- Record the final path, owner/group, mode, size, device, inode, and link count.
- Stop here if any path, ownership, or category differs from the ledger.
# Replace both placeholders with sanitized paths from your canary.
DOWNLOAD_FILE='/data/downloads/usenet/tv/AUTHORIZED_CANARY_FILE'
LIBRARY_FILE='/data/media/tv/AUTHORIZED_CANARY_FILE'
stat -c 'device=%d inode=%i links=%h owner=%U group=%G mode=%a name=%n' \
"$DOWNLOAD_FILE"
# Run after import against the final path:
stat -c 'device=%d inode=%i links=%h owner=%U group=%G mode=%a name=%n' \
"$LIBRARY_FILE"
Example expected output (not observed):
device=<DEVICE_ID> inode=<INODE> links=<COUNT> owner=<USER> group=media mode=664 name=<DOWNLOAD_PATH>
device=<DEVICE_ID> inode=<INODE> links=<COUNT> owner=<USER> group=media mode=664 name=<LIBRARY_PATH>
Matching device and inode values can support an atomic rename or hardlink conclusion when collected at the correct stages; link count helps distinguish hardlinks. The placeholders above prove nothing. A NAS protocol, separate mount, copy-on-write filesystem, reflink, or download-client retention policy can change the expected evidence. Record the actual filesystem and import behavior before making a storage-efficiency claim.
Gate 4: Add Plex and Prove Playback
cd /opt/media-stack
sudo docker compose -f compose.yml up -d plex
sudo docker compose -f compose.yml ps plex
sudo docker compose -f compose.yml logs --tail=80 plex
Example expected state (not observed): Plex starts without config or permission errors and is reachable from a trusted LAN client at http://YOUR_SERVER_IP:32400/web. Add only /movies and /tv as libraries. Do not add incomplete downloads, completed staging, Plex transcode cache, or Tdarr cache.
- Scan the library and confirm the two authorized canary items appear once with expected metadata.
- Play each from a normal client and open Plex's playback dashboard.
- Record Direct Play, Direct Stream, or Transcode; do not infer playback mode from low CPU use.
- Seek, change audio/subtitle selection, and play long enough to expose an immediate read or transcode failure.
- If hardware-accelerated streaming is in scope, recheck Plex Pass entitlement and supported hardware/device mapping, force a controlled transcode, and look for Plex's documented
(hw)indicator. A visible GPU alone is not proof that Plex used it.
Gate 5: Run a Reversible Tdarr Canary
Tdarr mutates files, so keep it last. Copy an authorized sample into /data/media/tdarr-canary, retain the original outside Tdarr's library, hash both inputs, and configure one worker. Do not point a first-run flow at the production movie or TV root.
cd /opt/media-stack
sudo docker compose -f compose.yml up -d tdarr
sudo docker compose -f compose.yml ps tdarr
sudo docker compose -f compose.yml logs --tail=100 tdarr
sha256sum /data/media/tdarr-canary/AUTHORIZED_CANARY_FILE
mediainfo /data/media/tdarr-canary/AUTHORIZED_CANARY_FILE
Example expected state (not observed): Tdarr's server and internal node are available through the localhost tunnel, the canary is readable under /media/tdarr-canary, and the commands print a real hash plus codec/container metadata. Save that output privately. Configure the flow to skip compliant files, transcode only when required, validate the output, and replace only the canary. Compare duration, streams, subtitles, playback, and post-job hash before any wider scope.
Firewall and Remote Access Boundary
Docker's current Ubuntu documentation warns that published container ports can bypass rules operators expect ufw or firewalld to enforce. Loopback bindings prevent the listed admin ports from listening on every host interface, but you must still inspect real sockets, Docker rules, cloud security groups, router forwards, and an off-LAN scan. Plex uses host networking in this example and needs its own host-firewall policy.
sudo ss -lntp | grep -E ':(7878|8080|8265|8266|8989|9696|32400)\b'
sudo docker compose -f /opt/media-stack/compose.yml ps
sudo ufw status verbose
Example expected state (not observed): Arr, Prowlarr, SABnzbd, and Tdarr listen only on 127.0.0.1; Plex listens according to its host-network configuration and is allowed only from intended LAN/VPN ranges. Validate from another LAN device and a genuinely off-LAN network. Do not present a host-only scan as proof of Internet exposure.
VPN note: In this article, VPN means a private administrative access path such as WireGuard, Tailscale, or OpenVPN for reaching your own services. A VPN does not create rights to access, copy, or distribute copyrighted material.
Restart, Backup, Update, and Rollback
After all five gates pass, gate the Docker service on the media mount and marker. This affects every container on the host, so use it only when this Ubuntu machine is dedicated to the media stack. For NAS storage, first convert the tested mount to a reviewed systemd mount unit or /etc/fstab entry; RequiresMountsFor cannot recreate a mount that was never made persistent. Docker then retains its native unless-stopped runtime and daemon-restart behavior, but the daemon cannot start when the required media path is absent.
sudo install -d -m 0755 /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/media-mount.conf \
>/dev/null <<'EOF'
[Unit]
RequiresMountsFor=/data/media
ConditionPathExists=/data/media/.media_mount_marker
EOF
sudo systemctl daemon-reload
sudo systemd-analyze verify docker.service
sudo systemctl enable docker.service
sudo systemctl status --no-pager docker.service
Example expected state (not observed): unit verification reports no error and Docker remains active before the test reboot. Reboot during a maintenance window, then verify findmnt --target /data/media, the marker, Docker status, and the five gates in that order. If the NAS mount fails, Docker should remain inactive rather than writing into the underlying directory; inspect systemctl status docker and journalctl -u docker -b, restore the mount, verify the marker, and start Docker manually. Stop if the marker exists on an empty local mountpoint.
Then create a restricted backup. Application config directories contain API keys, tokens, private hostnames, provider settings, request history, and database state. The exit trap below restarts the stack if archive creation fails after the stop.
set -Eeuo pipefail
cd /opt/media-stack
BACKUP_DIR=/secure-backups
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
sudo install -d -m 0700 "$BACKUP_DIR"
trap 'sudo docker compose -f compose.yml start' EXIT
sudo docker compose -f compose.yml stop
sudo tar -C /opt/media-stack -czf \
"$BACKUP_DIR/media-stack-config-$STAMP.tgz" .
sudo docker compose -f compose.yml start
trap - EXIT
sudo docker compose -f compose.yml ps
sudo ls -lh "$BACKUP_DIR/media-stack-config-$STAMP.tgz"
Example expected state (not observed): services stop cleanly, the protected archive is non-empty, and each service restarts even if the archive command exits early. This is backup creation, not restore proof. Copy the archive off-host, retain the matching Compose file and image digests, and restore one app into an isolated canary before broad automation.
Update one application at a time. Back up first, record the old image digest, pull only the chosen service, recreate it, inspect migrations and health, and run its dependent handoff. To roll back, return to the prior digest. If the new application migrated its database incompatibly, stop it and restore the pre-update config rather than starting an older binary against a newer database.
cd /opt/media-stack
sudo docker image inspect --format '{{json .RepoDigests}}' \
lscr.io/linuxserver/sonarr:latest
sudo docker compose -f compose.yml pull sonarr
sudo docker compose -f compose.yml up -d sonarr
sudo docker compose -f compose.yml logs --tail=100 sonarr
sudo docker compose -f compose.yml ps sonarr
Example expected state (not observed): the first command records at least one actual digest, and the updated service returns to its prior readiness and functional gates. The command does not itself prove database compatibility or a working rollback.
Troubleshooting the First Failed Handoff
| Symptom | Likely boundary | Check | Rollback |
|---|---|---|---|
| UI unreachable from workstation | SSH tunnel, loopback binding, or app readiness | Tunnel process, ss, Compose status/logs | Keep apps private; do not bind to all interfaces as a shortcut |
| Prowlarr app Test fails | Docker DNS, URL base, API key, or TLS | Internal service name and target app health | Restore previous app URL/key entry |
| SAB finishes but Arr does not import | Category, completed path, permissions, or mount view | Arr Activity/History, SAB path, stat, container mounts | Preserve queue and failed file; revert one changed path |
| Permission denied | UID/GID, umask, NAS ACL, or mount options | id, stat, findmnt, app logs | Restore mount options; do not run every container as root |
| Plex misses imported item | Wrong library path, unreadable file, or scan delay | Plex library path, file mode, scan/activity logs | Return to known library path; do not move files manually first |
| Tdarr output fails validation | Flow, codec, cache, device, or resource limit | Canary job report, MediaInfo, retained source | Discard canary output and restore retained source |
| Stack fails after reboot | NAS mount/order or missing marker | findmnt, marker, boot journal, Compose logs | Stop apps before they write into an empty local mountpoint |
Validation Ledger
- Compose renders with exit status 0 and actual image digests are recorded privately.
- Every service has a persistent config path and returns after a container restart.
- The media mount marker, free-space check, and UID/GID write test pass before apps start.
- Prowlarr tests one authorized indexer and synchronizes intended categories to Sonarr and Radarr.
- SABnzbd categories match the Arr download-client entries and completed paths.
- One authorized movie and series import without manual path or permission repair.
- Filesystem evidence supports the claimed move/hardlink behavior; no timing or space claim is inferred without measurement.
- Plex scans and plays both canaries with recorded playback decisions.
- Tdarr processes only a retained canary and the output passes MediaInfo plus playback validation.
- A host reboot preserves mounts, configuration, paths, and service readiness.
- A protected off-host backup exists and one app restore is tested in isolation.
- An off-LAN test confirms administrative ports are not publicly reachable.
Planned Evidence-Capture Checklist
Publication can remain documentation-backed, but it must not imply that TechGeeks performed these tests until the following reviewed artifacts exist.
- Create
artifacts/labs/plex-arr-tdarr-setup-ubuntu-beginners/YYYY-MM-DD/and record run ID, UTC times, Ubuntu/kernel, CPU/GPU/RAM, storage/filesystem/NAS protocol, network, Docker/Compose versions, image digests, and sanitized config hashes. - Run every copied command in a clean shell and pair it with exit status and unedited raw-private output.
- Capture Compose rendering, service startup, manual readiness, a reboot, and a safe negative permissions/path failure followed by recovery.
- Trace one authorized movie and series through search intent, SAB category, completion, import, Plex scan, and playback with matching UTC timestamps.
- Collect device/inode/link-count evidence before and after import and state whether it proves atomic move, hardlink, copy, or neither.
- Run one retained-source Tdarr canary and record input/output hashes, codec/container/audio/subtitle properties, validation, playback, and rollback.
- Back up one app, restore it into an isolated path, and prove it rejoins the workflow without stale automation.
- Capture only synthetic UI data. Record viewport and versions; fully cover keys, tokens, hostnames, addresses, account names, provider data, requests, filenames, history, and notifications; strip metadata; review at original resolution.
What This Evidence Does Not Prove
- A running container or HTTP response does not prove the application is configured or the end-to-end workflow works.
- The Compose example does not prove compatibility with every CPU architecture, image release, GPU, filesystem, NAS, network, provider, or client.
- One successful import and playback does not prove every codec, subtitle, quality profile, interrupted mount, concurrent job, or database migration.
- Matching inode evidence on one filesystem does not prove all future imports use hardlinks or atomic moves.
- A Plex
(hw)indicator for one stream does not prove every transcode uses hardware or that Tdarr can share the same device safely. - A created archive does not prove restore compatibility, secret availability, or acceptable recovery time.
- Private bindings and one scan do not prove the service is unreachable from every external path.
Related TechGeeks Resources
- Plex Homelab Architecture: Storage, GPU Transcoding, and Library Design
- Media Server Storage Design: NAS, CIFS/NFS Mounts, Permissions, and Local Cache
- Prowlarr Troubleshooting: Failed Indexer Tests and App Sync
- Sonarr Homelab Setup Guide
- Radarr Homelab Setup Guide
- Building a Production-Grade Tdarr GPU Transcoding Stack
- Backup and Disaster Recovery for Plex and the Arr Stack
- Monitoring and Health Checks for a Plex and Arr Homelab
- The Complete Plex, Arr, and Tdarr Homelab Media Automation Series
References
- Docker Engine: Install on Ubuntu
- Docker Compose startup and readiness order
- Docker Compose service reference
- Docker packet filtering and firewalls
- Servarr Docker Guide source
- TRaSH Guides: Hardlinks and Instant Moves
- LinuxServer.io container documentation
- Tdarr Docker Compose documentation
- Plex hardware-accelerated streaming
Final Checkpoint
The stack is ready for deeper tuning only when one authorized TV item and movie can move through the intended path without manual repairs, appear once in Plex, survive a host reboot, and leave a restore-ready configuration record. Tdarr remains a canary until its retained-source rollback and playback validation pass. That is the Day 0 baseline; lists, remote access, GPU tuning, and broad automation come later.
Need help applying this?
Bring TechGeeks into the real environment.
If you are working through this on a live network, WordPress site, Linux server, AI workflow, or PisoWiFi deployment, send the context and we can help turn it into a practical plan.



One thought on “Build and Test a Plex, Prowlarr, Sonarr, Radarr, SABnzbd, and Tdarr Stack”