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.

Interactive setup flow
Build the Stack in This Order

Click each step to see what must be true before moving on. The goal is one clean authorized import before broad automation.

  1. 1. FoundationUbuntu + Docker

    Patch the host, install Docker or native packages, and confirm the server can reboot cleanly.

  2. 2. Paths/data Layout

    Create shared download, media, and cache paths before apps start creating files with mismatched ownership.

  3. 3. SourcesProwlarr

    Add only lawful sources and sync indexers outward. Prowlarr finds candidates; it does not decide what belongs in the library.

  4. 4. DownloadsSABnzbd

    Use category folders for movies and TV. Keep incomplete work away from final Plex library roots.

  5. 5. DecisionsSonarr / Radarr

    Set roots, profiles, categories, and one controlled test item before enabling lists or broad searching.

  6. 6. PlaybackPlex

    Point Plex at final media only, then confirm it can see the imported test item without permission fixes.

  7. 7. Optimize LaterTdarr

    Add Tdarr only after imports work. Start with one test file, validate output, then scale carefully.

Rights gateUse these workflows only for media you own or are authorized to store, download, organize, or transcode.
One controlled item firstProve one authorized TV item and one authorized movie item before lists, GPU tuning, or library-wide Tdarr.
Private control planeAdmin UIs and app APIs should stay LAN, localhost, Docker network, or VPN by default.

Use Five Acceptance Gates

GateQuestionEvidence
RunningDid the container process start?docker compose ps and startup logs
ReadyCan the app answer its UI/API and finish startup?HTTP check, application health page, no migration or permission error
ConfiguredAre paths, credentials, profiles, categories, and app URLs correct?Sanitized settings ledger and each app's own Test action
FunctionalDoes one authorized item cross the handoff?Correlated queue, import, scan, and playback records
RecoverableCan 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

ServiceHost pathContainer pathReason
SABnzbd/data/downloads/data/downloadsIncomplete and completed staging only
Sonarr/Radarr/data/dataOne view of downloads and media for imports, hardlinks, or atomic moves
Plex/data/media/movies, /data/media/tv/movies, /tvRead final libraries, never download staging
Tdarr/data/media/mediaProcess only final media and the isolated canary folder
ProwlarrNoneNoneIndexer 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

OwnerSettingValueAcceptance check
SABnzbdTemporary folder/data/downloads/incompleteWritable by the configured UID/GID
SABnzbdCompleted/category folderstv to /data/downloads/usenet/tv; movies to /data/downloads/usenet/moviesTest jobs remain outside final media
ProwlarrSonarr/Radarr app URLshttp://sonarr:8989, http://radarr:7878Each Prowlarr app Test passes
SonarrRoot folder/data/media/tvRoot folder is writable and not a download path
RadarrRoot folder/data/media/moviesRoot folder is writable and not a download path
Sonarr/RadarrSABnzbd URLhttp://sabnzbd:8080Download-client Test passes with matching category
Sonarr/RadarrCompleted download handlingEnabled after paths/categories passOne 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

  1. Choose a small authorized movie and series sample whose filenames and titles will not expose household history.
  2. Run an interactive search in the owning Arr app and inspect accepted and rejected results.
  3. Send one authorized result to SABnzbd and confirm category movies or tv.
  4. Record the completed path and UTC time before import.
  5. Confirm Sonarr or Radarr imports into the final root without manual permission repair.
  6. Confirm the source was moved or retained exactly as the download-client policy requires.
  7. Record the final path, owner/group, mode, size, device, inode, and link count.
  8. 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

SymptomLikely boundaryCheckRollback
UI unreachable from workstationSSH tunnel, loopback binding, or app readinessTunnel process, ss, Compose status/logsKeep apps private; do not bind to all interfaces as a shortcut
Prowlarr app Test failsDocker DNS, URL base, API key, or TLSInternal service name and target app healthRestore previous app URL/key entry
SAB finishes but Arr does not importCategory, completed path, permissions, or mount viewArr Activity/History, SAB path, stat, container mountsPreserve queue and failed file; revert one changed path
Permission deniedUID/GID, umask, NAS ACL, or mount optionsid, stat, findmnt, app logsRestore mount options; do not run every container as root
Plex misses imported itemWrong library path, unreadable file, or scan delayPlex library path, file mode, scan/activity logsReturn to known library path; do not move files manually first
Tdarr output fails validationFlow, codec, cache, device, or resource limitCanary job report, MediaInfo, retained sourceDiscard canary output and restore retained source
Stack fails after rebootNAS mount/order or missing markerfindmnt, marker, boot journal, Compose logsStop 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

References

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.

Request helpGet field notesRecommended gear

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

Leave a Reply

Your email address will not be published. Required fields are marked *