Use case · Media transcoding

Tdarr server on a dedicated machine: disks, workers, Compose and backups

A Tdarr server is the central process of Tdarr's distributed transcoding system: it keeps the library database and serves the web UI on port 8265, while Nodes connect to it on port 8266 and run FFmpeg or HandBrake jobs in CPU and GPU workers. On one dedicated machine the official tdarr image runs the Server plus an internal Node, so the library array, an SSD transcode cache and an optional NVIDIA GPU share one host with no network share in between.

Configure it

Server, Node and Worker

The Server does no encoding; the Getting Started page calls it the core every other module connects to. Nodes encode, locally or remotely, and each runs Workers that execute jobs in parallel.

RoleJobPortImage
ServerDatabase, plugins, flows, queue, web UI8265 (UI), 8266 (server)tdarr, optional internal Node
NodeTakes jobs from the Server for its WorkersNone inbound; connects out to 8266tdarr_node
WorkerOne job at a time (Transcode CPU/GPU or Health Check CPU/GPU)NoneInside a Node

A mapped Node, the standard type, must reach the media and the transcode cache at the same paths as the Server, or through path translators. Unmapped Nodes move working files over the network instead; that is a Tdarr Pro feature, and without Pro they stop at 10 MB video files. With library, cache and internal Node on one host there is no share to keep in step.

Sizing the disks before the cores

Tdarr writes each new file to the transcode cache, then copies it back over the original. The docs make the cache folder mandatory, suggest a separate drive for it, and recommend a cheap SSD.

Container pathHoldsWhere we would put it
/mediaThe library, replaced in place by finished filesHard disks or SATA SSDs in RAID 6 or RAID 10 (our choice; RAID is not a backup)
/tempTranscode cache, holding working files mid-jobAn SSD of its own
/app/serverDatabase, plugins, samplesThe system SSD; small, and the folder to back up

Our rule of thumb for the cache, not a Tdarr figure: the largest file you hold times the transcode worker count, plus headroom.

CPU workers or NVENC workers

A GPU is optional. Tdarr's FAQ says CPU-only transcoding is typically slower but gives smaller files and better quality, and puts the usual worker count at around 3 per Node. The project notes that one FFmpeg worker on a 64-core processor may reach only about 30% utilisation, hence parallel workers.

A Transcode GPU worker only takes jobs whose arguments contain GPU terms such as nvenc, cuda or vaapi; a CPU worker declines exactly those. GPU workers take CPU jobs too when the Node option that lets GPU workers do CPU tasks is enabled.

  • Encoder sessions. GeForce cards allow 12 concurrent NVENC encodes per system; data-centre GPUs such as the L4, L40S and T4 have no session limit. In our reading each GPU transcode holds at least one session while it runs, so keep GPU workers under 12. Tdarr's troubleshooting page says the OpenEncodeSessionEx out-of-memory message most likely means the card hit this stream limit.
  • Quick Sync. Only processors with on-chip Intel graphics have it. In our range that means the E-2286M and Core i5-9500 in Bucharest, plus the E3-1284L v3/v4 and E3-1585L v5, which are Moonshot cartridges and carry the disk caveat below; all are on the Intel processor page. No Xeon E5, Xeon D or Xeon Scalable part has it. Tdarr reaches Quick Sync and VAAPI through the /dev/dri device mapped in the Compose file.

Machines that fit

For NVENC, the GPU transcoding server is a whole machine with the full NVIDIA GPU on its own PCIe bus: no vGPU, no time slicing, root, the IPMI console and your choice of driver. Its standard GeForce GTX 1080 Ti encodes H.264 and HEVC; AV1 output needs an RTX 4090D, RTX 5090, L4 or L40S. Racked machines go live about 30 minutes after the order is approved. A GPU we have to fit from parts on hand means 4 to 24 hours.

For a CPU-only library with many disks, the custom build page configures up to 24 drive bays in RAID 0/1/5/6/10 or JBOD across SATA, SAS, SSD and NVMe, handed over 4 to 24 hours after review. Avoid Moonshot cartridges here, since most carry one disk and none has hardware RAID. Nothing counts the traffic, so pulling a multi-terabyte library in costs no transfer fee. Playback from the same box is covered on the Jellyfin page; queue-free one-off encodes on FFmpeg GPU transcoding.

Installing with Docker Compose

Simplified from the official Compose example. What we changed, in full, is this list. Both ports are bound to 127.0.0.1; the image is pinned to 3.04.00 (newest tag on 2026-10-31) instead of latest; one container runs the internal Node, with no separate node service and no container_name; seededApiKey is added and apiKey set to the same key so the internal Node connects; TZ is Etc/UTC and nodeName is InternalNode; the host paths are ours; openBrowser, cronPluginUpdate, maxLogSizeMB, UMASK and network_mode are dropped. You need Ubuntu, Docker Engine 28 or later and, for NVENC, the NVIDIA driver plus the NVIDIA Container Toolkit, which Tdarr's hardware page requires on Linux; run that page's NVENC test first. Without an NVIDIA GPU, delete the deploy block; If /dev/dri does not exist (ls /dev/dri reports no such file), delete the service-level devices: line and the - /dev/dri:/dev/dri line under it, and leave the devices: inside deploy alone.

Block one enables ufw with only the OpenSSH rule, creates a tdarr system user owning the media and cache folders, and writes /srv/tdarr/.env at mode 600 with its ids and a random tapi_ key. If sshd listens on a port other than 22, run sudo ufw allow PORT/tcp with your port first. A re-run keeps .env only when it already holds a key and ids, so a half-written file is replaced and a good one, whose key seeded the admin account, never changes.

sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
sudo ufw allow OpenSSH && sudo ufw --force enable
id tdarr >/dev/null 2>&1 || sudo useradd --system --no-create-home --shell /usr/sbin/nologin tdarr
sudo install -d -m 750 -o tdarr -g tdarr /srv/media /srv/tdarr-cache
sudo install -d -m 700 /srv/tdarr /srv/tdarr/server /srv/tdarr/configs /srv/tdarr/logs
if sudo grep -q '^TDARR_KEY=tapi_' /srv/tdarr/.env 2>/dev/null && sudo grep -q '^PUID=[0-9]' /srv/tdarr/.env
then echo "keeping the valid /srv/tdarr/.env"
else k=$(openssl rand -hex 16) && [ -n "$k" ] && id -u tdarr >/dev/null \
  && sudo install -m 600 /dev/null /srv/tdarr/.env \
  && printf 'PUID=%s\nPGID=%s\nTDARR_KEY=tapi_%s\n' "$(id -u tdarr)" "$(id -g tdarr)" "$k" \
     | sudo tee /srv/tdarr/.env >/dev/null \
  && echo "key written" || echo "STOP - .env not written, fix before going on"; fi

Block two writes compose.yaml and starts the stack only if Docker reports version 28 or later and the key is present:

sudo tee /srv/tdarr/compose.yaml >/dev/null <<'EOF'
services:
  tdarr:
    image: ghcr.io/haveagitgat/tdarr:3.04.00
    restart: unless-stopped
    ports:
      - "127.0.0.1:8265:8265"
      - "127.0.0.1:8266:8266"
    environment:
      - TZ=Etc/UTC
      - PUID=${PUID}
      - PGID=${PGID}
      - serverIP=0.0.0.0
      - serverPort=8266
      - webUIPort=8265
      - internalNode=true
      - inContainer=true
      - ffmpegVersion=7
      - nodeName=InternalNode
      - auth=true
      - seededApiKey=${TDARR_KEY}
      - apiKey=${TDARR_KEY}
      - NVIDIA_DRIVER_CAPABILITIES=all
      - NVIDIA_VISIBLE_DEVICES=all
    volumes:
      - /srv/tdarr/server:/app/server
      - /srv/tdarr/configs:/app/configs
      - /srv/tdarr/logs:/app/logs
      - /srv/media:/media
      - /srv/tdarr-cache:/temp
    devices:
      - /dev/dri:/dev/dri
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
EOF
v=$(sudo docker version --format '{{.Server.Version}}') && [ "${v%%.*}" -ge 28 ] \
  && sudo grep -q '^TDARR_KEY=tapi_' /srv/tdarr/.env \
  && sudo docker compose -f /srv/tdarr/compose.yaml up -d \
  || echo "STOP - Docker Engine older than 28, no key in .env, or the start did not succeed"

With seededApiKey set, Tdarr creates an admin user on first start whose password is the key, so nobody can claim the setup screen. The key stays a valid API key after you change that password (the internal Node uses it) and docker inspect shows it; keep the docker group to yourself and treat the key and backups as admin credentials. Unaddressed mappings publish on 0.0.0.0 and [::], where published IPv4 bypasses ufw (IPv6 too with Docker's own IPv6 networking); 127.0.0.1 keeps both ports local, enforced from Engine 28.

First sign-in and a test library

  1. On your workstation, run ssh -N -L 8265:127.0.0.1:8265 you@your-server; the UI is then at http://localhost:8265.
  2. Print the key with sudo sed -n 's/^TDARR_KEY=//p' /srv/tdarr/.env and sign in as admin; the whole value, tapi_ included, is the password. Then set a new one.
  3. Confirm the internal Node appears on the Tdarr tab. If not, the Server log names the cause (no users, no API key, or an invalid key).
  4. Create a test folder with sudo install -d -o tdarr -g tdarr /srv/media/test, copy a few sample files in with sudo cp, run sudo chown -R tdarr:tdarr /srv/media/test, and add a library with source /media/test and cache /temp.
  5. Build the flow, start one worker, inspect the output before touching the real collection.

The Replace Original File plugin overwrites the original with the working file, name and container included, and Tdarr's EULA leaves data loss with the user. Keep the originals copied elsewhere until the flow is proven. Workers only process inside each library's schedule, with Process Library on.

Backups, restore and updates

The archive covers /srv/tdarr, meaning database and plugins, configs, compose.yaml and .env. Media is excluded; ship it off the box with your own tools. The script stops the container during the copy (our precaution), so keep Sunday 03:30 server time out of every library schedule. Cron runs on the host clock while the schedule follows the container's Etc/UTC, so check timedatectl and convert if the server is not on UTC. Four weeks are kept, pruned only after a verified archive.

sudo install -d -m 700 /var/backups/tdarr
sudo tee /usr/local/sbin/tdarr-backup.sh >/dev/null <<'EOF'
set -euo pipefail
umask 077
c=/srv/tdarr/compose.yaml
f=/var/backups/tdarr/tdarr-$(date +%F-%H%M%S).tar.gz
trap 'docker compose -f "$c" start tdarr' EXIT
docker compose -f "$c" stop tdarr
tar --numeric-owner --exclude=tdarr/logs -czf "$f.part" -C /srv tdarr
tar -tzf "$f.part" tdarr/.env tdarr/compose.yaml tdarr/server >/dev/null
mv "$f.part" "$f"
find /var/backups/tdarr -name 'tdarr-*.tar.gz' -mtime +28 -delete
EOF
printf '30 3 * * 0 root umask 077; bash /usr/local/sbin/tdarr-backup.sh >> /var/backups/tdarr/backup.log 2>&1\n' \
  | sudo tee /etc/cron.d/tdarr-backup >/dev/null
sudo bash /usr/local/sbin/tdarr-backup.sh && sudo ls -l /var/backups/tdarr

Push the archives to a second machine as well. Restore as root (or with sudo):

  1. Check the archive by running sudo tar -tzf /var/backups/tdarr/tdarr-STAMP.tar.gz tdarr/.env tdarr/compose.yaml tdarr/server >/dev/null && echo ok. No ok means stop.
  2. Stop Tdarr and set the live folder aside with [ ! -e /srv/tdarr.previous ] && sudo docker compose -p tdarr down && sudo mv /srv/tdarr /srv/tdarr.previous && echo moved. Stop here unless it prints moved; a leftover /srv/tdarr.previous has to be deleted or renamed first.
  3. Extract and start with `[ -d /srv/tdarr.previous ] && [ ! -e /srv/tdarr ] && sudo tar --numeric-owner -xzf /var/backups/tdarr/tdarr-STAMP.tar.gz -C /srv && sudo docker compose -f /srv/tdarr/compose.yaml up -d && echo restored || echo "STOP - /srv/tdarr still exists or the unpack failed; go back a step", then sign in and check the libraries. On a new machine, with nothing to set aside, run only block one's first five lines (firewall, user, folders; not its .env part), extract with sudo tar --numeric-owner -xzf /var/backups/tdarr/tdarr-STAMP.tar.gz -C /srv without starting, set PUID and PGID in /srv/tdarr/.env to the output of id -u tdarr and id -g tdarr, then run sudo docker compose -f /srv/tdarr/compose.yaml up -d`.
  4. To roll back, run [ -d /srv/tdarr.previous ] && [ ! -e /srv/tdarr.failed ] && sudo docker compose -p tdarr down && sudo mv /srv/tdarr /srv/tdarr.failed && sudo mv /srv/tdarr.previous /srv/tdarr && sudo docker compose -f /srv/tdarr/compose.yaml up -d. Compose named the project tdarr after its folder, so -p tdarr works without the file.
  5. Once the restored server is right, remove the old copy with sudo rm -r /srv/tdarr.previous.

To update, run the backup, edit the tag in compose.yaml, then run sudo docker compose -f /srv/tdarr/compose.yaml pull && sudo docker compose -f /srv/tdarr/compose.yaml up -d. For Docker the docs favour a tag change or a container manager over the internal updater, which needs enableDockerAutoUpdater=true and skips container-specific changes.

Licence and limits

Tdarr is not open source. LICENSE.md in its repository is an end-user licence agreement with three tiers (Personal Free, Personal Subscription, Business Subscription & Trial) that forbids redistribution and reverse engineering. The Tdarr_Plugins repository is GPL-3.0. Our general reading, not legal advice: running Tdarr for your own library on a rented server is use, not distribution; hand on copies of those plugins, modified or not, and the GPL obliges you to pass on their source too. A business should check which EULA tier applies.

This layout is a poor fit for a library that must stay on a home NAS. The Server and every mapped Node have to see the same folders, so the collection would need sharing over the network to this server; unmapped Nodes do not change that, because they fetch their files from the Server. Tdarr is also not a live transcoder; channels belong on a streaming engine.

Questions

Do the Tdarr Server and Node need separate machines?

No. The tdarr image contains a Server and a Node, and internalNode=true switches the Node on. Extra Nodes elsewhere only add capacity. On one box the internal Node sees the Server's paths, so no path translators are needed.

Which Tdarr ports have to be reachable?

8265 is the web UI and 8266 the Server port Nodes connect to; a Node listens on nothing. Here both stay on 127.0.0.1 behind an SSH tunnel. A Node added elsewhere later needs a private route such as a WireGuard tunnel; publish 8266 on the WireGuard address as well (for example 10.8.0.1:8266:8266), and give that Node the same media and cache paths through a share and path translators. Set WireGuard to start before Docker, so the address exists when the container starts.

Can Tdarr work without a graphics card?

Yes. Its FAQ says CPU transcoding is typically slower but produces smaller files and better quality. Run several CPU workers per Node so the cores are used. A GPU pays off when the queue is long and speed beats file size.

What happens to my originals when Tdarr transcodes them?

With a Replace Original File step, the finished file replaces the original, including its name and container. The EULA leaves data loss with the user. Test every flow on a copied sample library and keep originals backed up elsewhere until you trust it.

Size the GPU and the disks, then build it

Count GPU workers against the encoder limit, then configure the transcoding server, or for a CPU-only library choose the cache SSD and the array on the custom build page.

Configure it

Related on Server Room

Other use cases