Transmission was running fine as a native service on Rocky Linux — until we moved everything else into Docker and the web interface started throwing a “No exit found” error. The fix was straightforward: containerise Transmission too, point it at the existing config and downloads, and sort out a few ownership quirks along the way.

This post covers moving an existing Transmission installation into Docker while preserving all existing torrents, resume data, and download locations. No re-downloading, no lost progress.

Why Bother

With everything else running in Docker, having Transmission as a native service felt inconsistent. More practically, Home Assistant was previously running with network_mode: host which meant it had access to all ports — including 9091. Once HA moved to bridge networking, requests to port 9091 started hitting the wrong place. Containerising Transmission and giving it an explicit port binding sorted this cleanly.

Prerequisites

  • Docker and Docker Compose running
  • Existing Transmission installation with config at /var/lib/transmission/.config/transmission-daemon
  • Existing downloads at their current path (in this case /mnt/raid5/Torrent/Downloads)
  • Know which user owns your config vs downloads — they may differ

Check Ownership First

This is the most important step and the one most likely to cause problems if skipped. The Docker container runs as a specific user defined by PUID and PGID — if those don’t match the ownership of your config and download directories, Transmission either won’t start or won’t find its data.

# Check config ownership
ls -la /var/lib/transmission/.config/transmission-daemon/

# Check downloads ownership
ls -la /mnt/raid5/Torrent/Downloads/

# Get the IDs of your chosen user
id patto

In this setup the resume and torrent files were owned by patto but the downloads directory was owned by transmission. The fix was to align everything to patto:

sudo chown -R patto:patto /mnt/raid5/Torrent/Downloads

Create Required Subdirectories

The linuxserver Transmission image expects complete and incomplete subdirectories inside the downloads volume. If they don’t exist the container logs permission errors on every start.

mkdir -p /mnt/raid5/Torrent/Downloads/complete
mkdir -p /mnt/raid5/Torrent/Downloads/incomplete
chown -R patto:patto /mnt/raid5/Torrent/Downloads/complete
chown -R patto:patto /mnt/raid5/Torrent/Downloads/incomplete

Stop the Existing Service

Stop and disable the native Transmission service before starting the container — both cannot bind to port 9091 at the same time.

sudo systemctl stop transmission-daemon
sudo systemctl disable transmission-daemon

Docker Compose File

mkdir -p /mnt/raid5/docker/transmission/watch
vi /mnt/raid5/docker/transmission/docker-compose.yml
services:
  transmission:
    image: lscr.io/linuxserver/transmission:latest
    container_name: transmission
    restart: unless-stopped
    environment:
      - PUID=1000    # replace with output of: id patto
      - PGID=1000    # replace with output of: id patto
      - TZ=Europe/London
    volumes:
      - /var/lib/transmission/.config/transmission-daemon:/config
      - /mnt/raid5/Torrent/Downloads:/mnt/raid5/Torrent/Downloads
      - /mnt/raid5/docker/transmission/watch:/watch
    ports:
      - 9091:9091
      - 51413:51413/tcp
      - 51413:51413/udp

Key: The downloads volume must be mapped to the same path inside the container as it is on the host. The resume files store absolute paths — if the container path differs from the host path, Transmission shows “No data found” for every torrent even though the files exist.

Start the Container

cd /mnt/raid5/docker/transmission
docker compose up -d
docker logs transmission --tail 30

What the Logs Should Show

A healthy start looks like this:

[custom-init] No custom files found, skipping...
Connection to localhost (127.0.0.1) 9091 port [tcp/*] succeeded!

These messages are harmless and expected:

ln: failed to create symbolic link '/transmissionic/index.html': File exists
ln: failed to create symbolic link '/combustion-release/index.html': File exists

These mean the UI symlinks already exist from a previous run — not a problem.

Warning: If you see permission errors on /downloads/complete or /downloads/incomplete it means those subdirectories don’t exist or have wrong ownership. Create them and fix ownership as shown above, then restart.

Access the Web Interface

Access Transmission locally at:

http://[ServerIP]:9091/transmission/web/

Note the trailing slash — without it some browsers redirect incorrectly.

This is intentionally local-only. The container has no proxy_net membership and no Nginx config, so it is not accessible externally. Port 9091 is only reachable from inside the local network.

Browser Cache Gotcha

Chrome in particular caches redirects aggressively. If Chrome shows the wrong page after the move, either clear the cache for that IP or test in an incognito window first. Firefox tends to behave better here.

Local DNS Resolution

If you want to access Transmission via a hostname rather than an IP internally, add an A record to your local BIND DNS server rather than routing it through Cloudflare. Since Transmission is local-only there is no reason for it to be publicly accessible.

Lessons Learned

  1. Map volumes to the same path as the host. The linuxserver image lets you choose the container path freely — always use the identical absolute path so resume files resolve correctly.
  2. Check ownership before anything else. Mismatched PUID/PGID between config and downloads causes two separate classes of failure that look unrelated.
  3. Create complete and incomplete subdirectories manually. The container expects them to exist and logs misleading permission errors if they don’t.
  4. Stop the native service first. Two processes cannot bind to 9091 simultaneously — the container will start but immediately fail to bind.
  5. Chrome caches redirects. Always test in incognito after moving services between ports or hosts.

Final Checklist

  • Config and downloads ownership aligned to same user
  • complete and incomplete subdirectories created
  • transmission-daemon service stopped and disabled
  • PUID and PGID set correctly in compose file
  • Downloads volume mapped to identical host path
  • Container started and logs show port 9091 succeeded
  • Existing torrents visible in web UI
  • Access confirmed at http://[ServerIP]:9091/transmission/web/