What Tdarr does to your files
Tdarr takes a collection that grew file by file and brings it to one standard. You describe what each file should look like; Tdarr scans, compares, and only touches the files that miss. It can also hunt for damaged files: a quick health check reads headers, a thorough one decodes every frame.
The catch is that it works on your real files. Flows usually finish with Replace Original File, which swaps the original for the new version, taking on the new file's name and container. Tdarr's licence also says it is not responsible for data loss. So start with a handful of copies, never the whole collection.
Server, Node, Worker: who does what
- The Server holds the database, your rules and the queue, and serves the web page on port 8265. It never encodes. Nodes talk to it on port 8266.
- A Node fetches jobs and gives them to its workers. It dials out to the Server, so it needs no open port.
- A Worker runs one job at a time, in four kinds: transcoding on CPU or GPU, and health checks on CPU or GPU.
The official tdarr Docker image bundles a Server and a Node; set internalNode=true to turn its Node on. An ordinary (mapped) Node must find your media and the cache folder under identical paths to the Server's, which you get for free when everything lives on the box with the disks. Nodes on other computers are optional extra muscle, and the unmapped kind that copies files across the network is capped at 10 MB video files unless you buy Tdarr Pro.
Where the disk space goes
Each file is encoded into a working folder, the transcode cache, and the result is then copied back into your library. Tdarr's docs say the cache must be set, may do better on another drive, and is best on a cheap SSD. Plan three places:
| What | Inside the container | Our suggestion |
|---|---|---|
| Your library | /media | Big drives in RAID 10 or RAID 6 (our pick; it survives a dead drive, not a bad flow) |
| Working cache | /temp | A separate SSD; we size it at your biggest file multiplied by the transcode workers running at once, plus spare room |
| Tdarr's own data | /app/server | The system disk; database and plugins, the part you back up |
That shape is what a storage dedicated server is for: hardware RAID and a remote console (iLO, iDRAC or BMC) on its blade and rack machines, plus a port no other customer shares, where nothing meters your traffic. Need more drives? Build to order reaches 24 bays, with drives from small SATA SSDs up to 30.72 TB SAS SSDs, set up as RAID levels 0, 1, 5, 6 and 10, or JBOD. A racked server is yours about 30 minutes after your order is approved; building one from parts we hold takes 4 to 24 hours, 5 to 10 working days if parts must be bought in, and 10 to 15 for a scarce part. The build-to-order page shows which applies.
Will the processor keep up?
Usually, if you give it enough workers. Tdarr's FAQ says you do not need a graphics card: CPU encoding is typically slower but makes smaller files of better quality. It suggests around 3 workers per Node as typical, and the project's own example explains why more help: a lone FFmpeg worker on 64 cores may use only about 30% of them.
When the backlog is huge and speed matters more than size, add a card. GPU workers only accept jobs whose settings name the GPU (nvenc, cuda, vaapi and similar), so CPU and GPU workers can share one flow; tick 'Allow GPU workers to do CPU tasks' in the Node options and they take CPU jobs as well. Our GPU servers hand the whole card to your own operating system, and fitting one from our stock takes 4 to 24 hours. The install below is CPU-only.
Getting it running
You need Ubuntu and Docker 28 or later. The Compose file trims Tdarr's official example, and this is everything we changed. Both ports listen on 127.0.0.1 only; the image tag is 3.04.00 (the latest on 31 October 2026) rather than latest; only the main container is defined, its built-in Node doing the work; seededApiKey is added and apiKey carries the same key; the folders, TZ (Etc/UTC) and nodeName (LibraryNode) are ours; and container_name, the GPU settings, /dev/dri, network_mode, UMASK, maxLogSizeMB, cronPluginUpdate and openBrowser are gone.
- Paste the first block. SSH on a non-standard port? Open it beforehand, for example
sudo ufw allow 2222/tcp. The block switches ufw on allowing only SSH, adds atdarruser owning the library and cache folders, and creates/srv/tdarr/.env(mode 600) holding that user's ids and a random key startingtapi_. Pasted again, it keeps a complete earlier file and rewrites an empty or half-written one. - Paste the second block. It saves the Compose file and starts Tdarr, but only when the key exists and Docker is 28 or later.
sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
sudo ufw allow OpenSSH && sudo ufw --force enable
getent passwd tdarr >/dev/null || sudo useradd --system --no-create-home --shell /usr/sbin/nologin tdarr
sudo install -d -m 750 -o tdarr -g tdarr /srv/library /srv/tdarr-cache
sudo install -d -m 700 /srv/tdarr /srv/tdarr/server /srv/tdarr/configs /srv/tdarr/logs
if sudo grep -q '^TDARR_API_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 key=$(openssl rand -hex 16) && [ -n "$key" ] && id -u tdarr >/dev/null \
&& sudo install -m 600 /dev/null /srv/tdarr/.env \
&& printf 'PUID=%s\nPGID=%s\nTDARR_API_KEY=tapi_%s\n' "$(id -u tdarr)" "$(id -g tdarr)" "$key" \
| sudo tee /srv/tdarr/.env >/dev/null \
&& echo "env file ready" || echo "STOP - the env file was not written"; fi
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=LibraryNode
- auth=true
- seededApiKey=${TDARR_API_KEY}
- apiKey=${TDARR_API_KEY}
volumes:
- /srv/tdarr/server:/app/server
- /srv/tdarr/configs:/app/configs
- /srv/tdarr/logs:/app/logs
- /srv/library:/media
- /srv/tdarr-cache:/temp
EOF
ver=$(sudo docker version --format '{{.Server.Version}}') && [ "${ver%%.*}" -ge 28 ] \
&& sudo grep -q '^TDARR_API_KEY=tapi_' /srv/tdarr/.env \
&& sudo docker compose -f /srv/tdarr/compose.yaml up -d && echo "Tdarr started" \
|| echo "STOP - needs Docker Engine 28+ and a key in .env; nothing was published"
This is safe to start because the seededApiKey setting makes Tdarr create an admin account on first launch with the key as its password, so no stranger can grab the setup screen. Change that password; the key still works as an API key for the built-in Node and shows in container details, so let nobody else into the docker group and guard the key and backups like an admin password. Unpinned ports listen on all IPv4 and IPv6 addresses, and Docker's IPv4 side skips ufw (IPv6 also, with Docker's IPv6 networking on). 127.0.0.1 keeps Tdarr on the server, which Docker 28+ enforces.
Your first sign-in
- On your own computer, start a tunnel with your usual login:
ssh -N -L 9265:127.0.0.1:8265 you@your-server. Keep it open and visit http://localhost:9265. - On the server, run
sudo sed -n 's/^TDARR_API_KEY=//p' /srv/tdarr/.envto show the key. Log in asadminwith thetapi_...value it prints, all of it, then choose a new password. - Look for the built-in Node on the Tdarr tab. If it is missing, the Server log explains why (no users yet, no key, or a wrong key).
- Make a sample folder with
sudo install -d -m 750 -o tdarr -g tdarr /srv/library/sample, copy a few spare files in withsudo cp, hand them over withsudo chown -R tdarr: /srv/library/sample, and create a library pointing at/media/samplewith/tempas its cache. - Set up your flow, run one worker, and play the results before Tdarr goes near the full collection.
A library is only processed while Process Library is on and the clock is inside its schedule. Want to watch the files from this box too? Our Jellyfin guide covers the player side.
Keeping the setup safe
Your settings, rules and history live in /srv/tdarr. The script archives that folder (minus logs) every Saturday at 04:15 by the server's clock, briefly stopping the container so the database is not caught mid-change (our precaution). Leave that slot out of your schedules, which run on the container's UTC clock; if timedatectl shows another zone, convert. Each archive is tested before it is kept, and four-week-old ones go only after a new one succeeds. Your media is not included; copy it, and these archives, to another computer yourself.
sudo install -d -m 700 /var/backups/tdarr
sudo tee /usr/local/sbin/tdarr-backup.sh >/dev/null <<'EOF'
set -euo pipefail
umask 077
cf=/srv/tdarr/compose.yaml
out=/var/backups/tdarr/tdarr-$(date +%F-%H%M%S).tar.gz
trap 'docker compose -f "$cf" start tdarr' EXIT
docker compose -f "$cf" stop tdarr
tar --numeric-owner --exclude=tdarr/logs -czf "$out.part" -C /srv tdarr
tar -tzf "$out.part" tdarr/.env tdarr/compose.yaml tdarr/server >/dev/null
mv "$out.part" "$out"
find /var/backups/tdarr -name 'tdarr-*.tar.gz' -mtime +28 -delete
EOF
printf '15 4 * * 6 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
To bring a backup back, become root with sudo -i and move to the parent folder with cd /srv; every step below runs in that same root shell:
- Name the archive and test it with
a=/var/backups/tdarr/tdarr-STAMP.tar.gz; tar -tzf "$a" tdarr/server tdarr/compose.yaml tdarr/.env > /dev/null && echo good. Only continue once you see good. - Stop Tdarr and park today's folder with
[ ! -e tdarr.old ] && docker compose -p tdarr down && mv tdarr tdarr.old && echo parked || echo "STOP - tdarr.old already exists or Tdarr would not stop; nothing was moved". On a brand-new server with no/srv/tdarryet, skip this step. - Unpack and launch with
[ ! -e tdarr ] && tar --numeric-owner -xzf "$a" && docker compose -f tdarr/compose.yaml up -d && echo launched || echo "STOP - a tdarr folder is still here or unpacking did not finish; redo the previous step". On a brand-new server, run just the first five lines of the first block (firewall, user, folders), unpack withtar --numeric-owner -xzf "$a"but do not start it yet, put the numbers fromid -g tdarrandid -u tdarrinto PGID and PUID intdarr/.env, then rundocker compose -f tdarr/compose.yaml up -d. - If you change your mind,
[ -d tdarr.old ] && [ ! -e tdarr.bad ] && docker compose -p tdarr down && mv tdarr tdarr.bad && mv tdarr.old tdarr && docker compose -f tdarr/compose.yaml up -d || echo "STOP - remove or rename tdarr.bad first"brings the parked folder back (-p tdarrnames the project after its folder, so it works even if the restored files are broken). - When you are happy with it, delete the parked copy with
rm -r tdarr.old.
Updating means changing one line. Take a backup, edit the version in compose.yaml, then as root, from /srv, run docker compose -f tdarr/compose.yaml pull && docker compose -f tdarr/compose.yaml up -d. Tdarr's docs prefer this, or a container manager, to the built-in updater for Docker, which only runs with enableDockerAutoUpdater=true and misses changes the container itself needs.
Before you commit: licence and fit
Tdarr is free to use but not open source. The LICENSE.md on its GitHub repository is an end-user agreement offering Personal Free, Personal Subscription, and Business Subscription & Trial, and it rules out redistributing or reverse-engineering the software. The community plugins sit in a separate Tdarr_Plugins repository under GPL-3.0. How we read it, as general information and not legal advice: running Tdarr on a rented server for your own files is using it; giving copies of those plugins to anyone else, changed or unchanged, means making their source available as well. If Tdarr works for a company, check which tier fits.
This is the wrong setup if your files have to stay at home. The Server and each mapped Node need identical folders, so a library at home would have to be shared across the internet to this machine, and unmapped Nodes are no way round it, since they pull every file from the Server. Keep every Tdarr port private. If a Node at home ever needs the Server, our WireGuard guide builds a private tunnel; you would also publish port 8266 on the tunnel's address (such as 10.8.0.1:8266:8266) and make media and cache reachable on that Node under matching paths, via a share plus path translators. Make WireGuard start before Docker, so that address is up when the container launches.
Questions people ask
- Is a separate Tdarr Node needed on the same machine?
- No. The
tdarrcontainer already includes one, switched on withinternalNode=true. A separatetdarr_nodecontainer or extra computers only add capacity. Start with the built-in Node and add more later if the queue moves too slowly. - What are ports 8265 and 8266 for?
- 8265 is the web page you log in to; 8266 is where Nodes talk to the Server. Nodes connect outward, so a Node never needs an open port. In this guide both stay on 127.0.0.1 and you reach the page through SSH.
- Should Tdarr use the CPU or a graphics card?
- Either. Tdarr's FAQ says CPU encoding is slower yet ends up with smaller, better-looking files, while a GPU clears a long backlog faster. You can mix them: GPU workers only take jobs written for the GPU (unless that Node option letting them do CPU tasks is ticked), and CPU workers handle the rest.
- How much disk does the transcode cache need?
- Tdarr gives no number, only that the cache is required and a cheap SSD is recommended. Our own rule is enough room for your largest file multiplied by the transcode workers running together, plus spare space. Keep it off the library drives.
- Can Tdarr damage my media?
- It replaces your originals, which is the point, so a bad flow can overwrite good files with worse ones. The licence leaves data loss with you. Test on a sample library and keep the originals on another machine until you trust the results.