A modern web UI for rtorrent, packaged as a single Docker image with rtorrent baked in. TypeScript end to end — React on the front, a dependency-light Node backend that speaks rtorrent's XML-RPC over SCGI.
- One container, batteries included — rtorrent, its config, and the UI. Nothing else to run, published for amd64 and arm64, and buildable from source in one command.
- rtorrent 0.16.23, compiled from source — the version in the image is exactly the upstream tag you asked for, not whatever a distro packaged. Any other tag builds with one build arg.
- Works across backend versions — the server probes
system.listMethodson connect and picks command names from what the running rtorrent actually implements, hiding unsupported controls in the UI instead of failing. - Everything rtorrent exposes — upload
.torrentfiles, magnet links and URLs, global and per-torrent throttling, file priorities, tracker management, peers, labels, plus a raw API console and an XML-RPC passthrough for anything the UI does not wrap. The settings dialog covers the full tunable surface: slots, peer ranges, ports, binds, proxies, encryption, DHT, tracker TLS verification, disk preload/sync, socket buffers and resource limits — each control greyed out when the running rtorrent lacks it. - Drop torrents anywhere — drag
.torrentfiles onto the window and they are added and started on the spot, no dialog in the way. - Lightly gamified — a level and a set of badges earned from real transfer totals, with a confetti burst when a download lands (stepped pixel rain in retro; falling ash, embers and distant lightning in black metal). Off with one environment variable if it is not for you.
- Five themes — system, light, dark, a retro 8-bit CRT mode, and a grim, frostbitten black metal mode. Preferences are stored on the server, so they follow the install rather than the browser.
- Works on a phone — the table becomes cards with their own sort control, the sidebar becomes a drawer that also carries the tools, and detail and dialogs become full-screen sheets.
- Configured entirely from
docker run— the rtorrent backend options are environment variables.
Images are published to GitHub Container Registry for linux/amd64 and linux/arm64 (so a Raspberry Pi 4/5 or an Apple-silicon Docker host works the same as an x86 server):
docker run \
-d \
--name=cascade \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=Europe/Stockholm \
-e WEB_USER=admin \
-e WEB_PASS=change-me \
-e RT_PORT_RANGE=50000-50000 \
-p 8080:8080 \
-p 50000:50000 \
-p 50000:50000/udp \
-v /home/torrent/config:/config \
-v /home/torrent/downloads:/downloads \
-v /home/torrent/watch:/watch \
--restart unless-stopped \
ghcr.io/johanlindvall/cascade:latestOpen http://localhost:8080.
Two volumes matter: /config holds rtorrent's session, its log and
cascade-state.json (preferences, progress, throttle groups) and should be persistent;
/downloads is where the data lands. Port 50000 is the peer port — publish it on TCP and UDP
so DHT works — and PUID/PGID should match the owner of your download directory.
Stop it with docker stop cascade rather than docker rm -f: rtorrent only releases its session
lock on a clean shutdown.
Every push to main is published, so tags are cheap and specific:
| Tag | Points at |
|---|---|
latest |
The newest published build |
v0.1.42 |
One exact release, built against the default rtorrent |
v0.1.42-0.16.23 |
The same release, naming the rtorrent version explicitly |
The -<rtorrent-version> suffix is always present so that builds against other rtorrent releases
can be published under the same scheme later. Pin vX.Y.Z-<rtorrent> for anything you care about
keeping still; latest moves with main.
docker pull ghcr.io/johanlindvall/cascade:v0.1.42-0.16.23 # pin a release
docker pull ghcr.io/johanlindvall/cascade:latest # follow mainNo login is needed — the package is public. If a pull ever comes back
denied, check Packages → cascade → Package settings → Change visibility.
Upgrading is a pull and a re-create; all state lives in the two volumes:
docker pull ghcr.io/johanlindvall/cascade:latest
docker stop cascade && docker rm cascade
docker run -d --name cascade ... # same flags as beforeNothing here needs the published image — the whole thing builds from source, and that is also how you get an rtorrent version other than the default:
docker build -t cascade .
make build && make run # build, run against ./data, open a browserThe published images carry the default rtorrent; for any other version, build it yourself. rtorrent and libtorrent are always compiled from upstream tags. The default is 0.16.23:
docker build -t cascade . # 0.16.23
docker build --build-arg RTORRENT_VERSION=0.15.2 -t cascade:0.15.2 .
docker build --build-arg RTORRENT_VERSION=0.9.8 -t cascade:0.9.8 .
make matrix # 0.9.8, 0.15.2, 0.16.23libtorrent is pinned to the matching release automatically — rtorrent 0.9.x pairs with libtorrent
0.13.x, 0.10.x with 0.14.x, and from 0.15 the two share a version. LIBTORRENT_VERSION overrides
that, and RTORRENT_REPO/LIBTORRENT_REPO point at forks. ALPINE_VERSION (default 3.22) picks
the base image.
A client identifies itself twice: the HTTP User-Agent rtorrent sends to a tracker, and the peer
id prefix libtorrent gives every peer and tracker. Private trackers whitelist client versions and
refuse anything newer than their list, which rtorrent surfaces only as a failed announce — so
0.16.23, being too new for some of them, presents itself as 0.16.20 by default
(USER_AGENT=rtorrent/0.16.20, PEER_NAME=-lt1014-). Every other version presents itself as what
it is. Both are build arguments, because rtorrent and libtorrent bake them in at compile time and
expose no command to change them while running:
make build USER_AGENT=rtorrent/0.16.23 PEER_NAME=-lt1017- # be 0.16.23 outright
make build USER_AGENT=rtorrent/0.9.8 PEER_NAME=-lt0D80-
docker build --build-arg USER_AGENT=rtorrent/0.9.8 --build-arg PEER_NAME=-lt0D80- -t cascade .
make version # what the image presentsSet the two together: a tracker that checks both sees a mismatch if only one moves. The peer id
prefix per release is -lt0D80- (0.13.8, with rtorrent 0.9.8), -lt0F02- (0.15.2), -lt1014-
(0.16.20), -lt1016- (0.16.22) and -lt1017- (0.16.23).
The UI adapts at runtime, so one build of the frontend drives any of them — 0.9.8, 0.15.2 and 0.16.23 are all exercised by the same API suite. Which backend you got is shown under the logo and in Settings → Backend.
0.16 renamed and removed a number of commands. Cascade probes for them rather than assuming, so older backends keep working:
| Area | ≤ 0.15 | 0.16 |
|---|---|---|
| Listening port | network.port_range |
network.listen.port.range |
| Scheduler | schedule2 |
schedule (the string form, which all versions accept) |
| HTTP connections | network.http.max_open (writable) |
network.http.max_total_connections (read-only) |
| Proxy | network.proxy_address |
network.proxy.global / network.proxy.http |
0.16 also adds options Cascade now exposes when present: per-host HTTP connection limits, a global proxy, separate IPv4/IPv6 bind addresses, a DHT announce-port override, outgoing-connection blocking, and a random-access hint for hashing. On older backends those controls are hidden.
Everything is an environment variable on docker run. Only what you set is applied — anything
left unset keeps rtorrent's own default.
The image lists them all itself, so you never need this page to be up to date:
docker run --rm ghcr.io/johanlindvall/cascade:latest --helpBoth that output and the tables below are generated from one catalog in the source
(server/src/options.ts), and CI fails if either drifts from what the container actually reads.
| Variable | Default | Meaning |
|---|---|---|
PUID |
1000 |
User id rtorrent and the web server run as |
PGID |
1000 |
Group id rtorrent and the web server run as |
TZ |
UTC |
Container timezone |
RT_DOWNLOAD_DIR |
/downloads |
Default download directory |
RT_COMPLETED_DIR |
unset | Move finished downloads here |
RT_SESSION_DIR |
/config/session |
rtorrent's session state |
RT_WATCH_DIR |
/watch |
.torrent files dropped here are loaded and started |
RT_WATCH_ENABLE |
1 |
Set 0 to ignore the watch directory |
RT_WATCH_INTERVAL |
10 |
Watch-directory poll interval, seconds |
RT_LOG_FILE |
/config/rtorrent.log |
rtorrent's log file, surfaced in the UI |
RT_LOG_LEVEL |
info |
Log scopes: info, debug, dht_debug, tracker_debug, … (more can be raised live from the log dialog) |
RT_UMASK |
0022 |
umask rtorrent creates files with |
CASCADE_STATE_FILE |
/config/cascade-state.json |
Preferences, progress, add times and throttle groups |
CASCADE_CHOWN_DOWNLOADS |
0 |
Set 1 to chown the download directory at startup (slow on large libraries) |
RT_SESSION_LOCK_KEEP |
0 |
Set 1 to keep a leftover rtorrent.lock instead of clearing it |
Rates are in KiB/s; 0 means unlimited.
| Variable | Default | Meaning |
|---|---|---|
RT_DOWNLOAD_RATE |
rtorrent default | Global download limit |
RT_UPLOAD_RATE |
rtorrent default | Global upload limit |
RT_MAX_UPLOADS |
rtorrent default | Upload slots per torrent |
RT_MIN_UPLOADS |
rtorrent default | Minimum upload slots per torrent |
RT_MAX_UPLOADS_GLOBAL |
rtorrent default | Upload slots across all torrents |
RT_MAX_DOWNLOADS |
rtorrent default | Download slots per torrent |
RT_MIN_DOWNLOADS |
rtorrent default | Minimum download slots per torrent |
RT_MAX_DOWNLOADS_GLOBAL |
rtorrent default | Download slots across all torrents |
| Variable | Default | Meaning |
|---|---|---|
RT_MIN_PEERS |
rtorrent default | Minimum peers while leeching |
RT_MAX_PEERS |
rtorrent default | Maximum peers while leeching |
RT_MIN_PEERS_SEED |
rtorrent default | Minimum peers while seeding (-1 disables) |
RT_MAX_PEERS_SEED |
rtorrent default | Maximum peers while seeding (-1 disables) |
RT_PEX |
rtorrent default | Peer exchange, yes/no |
| Variable | Default | Meaning |
|---|---|---|
RT_PORT_RANGE |
50000-50000 |
Incoming peer port range |
RT_PORT_RANDOM |
no |
Pick a random port from the range, yes/no |
RT_PORT_OPEN |
rtorrent default | Open the listening port, yes/no (removed in rtorrent 0.16) |
RT_ENCRYPTION |
rtorrent default | e.g. allow_incoming,try_outgoing,enable_retry |
RT_BIND |
unset | Bind address for outgoing connections |
RT_IP |
unset | Address reported to trackers |
RT_BIND_IPV4 |
unset | IPv4 bind address (rtorrent 0.16+) |
RT_BIND_IPV6 |
unset | IPv6 bind address (rtorrent 0.16+) |
RT_PROXY |
unset | HTTP proxy for tracker announces |
RT_PROXY_HTTP |
unset | Proxy for all HTTP traffic (rtorrent 0.16+) |
RT_PROXY_GLOBAL |
unset | Proxy for all traffic (rtorrent 0.16+) |
RT_BLOCK_OUTGOING |
rtorrent default | yes refuses outgoing connections (rtorrent 0.16+) |
| Variable | Default | Meaning |
|---|---|---|
RT_DHT |
rtorrent default | disable, off, auto or on |
RT_DHT_PORT |
rtorrent default | DHT UDP port |
RT_DHT_OVERRIDE_PORT |
rtorrent default | Announce a different DHT port (rtorrent 0.16+) |
RT_UDP_TRACKERS |
rtorrent default | Allow UDP trackers, yes/no |
RT_TRACKER_NUMWANT |
rtorrent default | Peers requested per announce (-1 leaves it to the tracker) |
RT_HTTP_CAPATH |
unset | Directory of CA certificates for tracker TLS |
RT_HTTP_CACERT |
unset | CA bundle file for tracker TLS |
RT_SSL_VERIFY_PEER |
rtorrent default | Verify tracker TLS certificates, yes/no |
RT_SSL_VERIFY_HOST |
rtorrent default | Verify tracker TLS hostnames, yes/no |
| Variable | Default | Meaning |
|---|---|---|
RT_PREALLOCATE |
rtorrent default | Preallocate files, yes/no |
RT_HASH_ON_COMPLETION |
rtorrent default | Re-verify on completion, yes/no |
RT_ADVISE_RANDOM_HASHING |
rtorrent default | Random-access hint while hashing, yes/no (rtorrent 0.16+) |
RT_MEMORY_MAX |
rtorrent default | Piece memory cap, bytes |
RT_MAX_FILE_SIZE |
rtorrent default | Largest accepted file, bytes |
RT_SYNC_TIMEOUT |
rtorrent default | Piece disk-sync timeout, seconds |
RT_PRELOAD_TYPE |
rtorrent default | Piece preload: 0 off, 1 madvise, 2 direct paging |
RT_PRELOAD_MIN_SIZE |
rtorrent default | Only preload torrents above this piece size, bytes |
RT_PRELOAD_MIN_RATE |
rtorrent default | Only preload above this upload rate, bytes/s |
| Variable | Default | Meaning |
|---|---|---|
RT_MAX_OPEN_FILES |
rtorrent default | Open file handle cap |
RT_MAX_OPEN_SOCKETS |
rtorrent default | Open socket cap |
RT_MAX_HTTP_OPEN |
rtorrent default | Concurrent HTTP requests (read-only on rtorrent 0.16+) |
RT_HTTP_MAX_HOST |
rtorrent default | HTTP connections per host (rtorrent 0.16+) |
RT_DNS_CACHE_TIMEOUT |
rtorrent default | DNS cache lifetime, seconds |
RT_RECEIVE_BUFFER |
rtorrent default | Socket receive buffer, bytes |
RT_SEND_BUFFER |
rtorrent default | Socket send buffer, bytes |
| Variable | Default | Meaning |
|---|---|---|
RT_SCGI_SOCKET |
/run/rtorrent/rpc.socket |
Unix socket rtorrent listens on |
RT_SCGI_PORT |
unset | Also listen for SCGI on this TCP port (unauthenticated — keep it private) |
RT_SCGI_BIND |
127.0.0.1 |
Interface for RT_SCGI_PORT |
RT_XMLRPC_SIZE_LIMIT |
16777216 |
Max XML-RPC request size, bytes (raises the .torrent upload ceiling) |
CASCADE_SCGI |
RT_SCGI_SOCKET | Endpoint the web server talks to — a path, or host:port for a remote rtorrent |
| Variable | Default | Meaning |
|---|---|---|
WEB_PORT |
8080 |
HTTP port |
WEB_HOST |
0.0.0.0 |
Bind address |
WEB_USER |
unset (no auth) | Basic-auth user; auth is enabled only when both are set |
WEB_PASS |
unset (no auth) | Basic-auth password |
WEB_BASE_PATH |
/ |
Serve under a sub-path, e.g. /rtorrent |
CASCADE_ALLOW_RAW_RPC |
1 |
Set 0 to disable the API console and /RPC2 |
CASCADE_ALLOW_DATA_DELETE |
1 |
Set 0 to forbid deleting downloaded data |
CASCADE_DELETE_ROOTS |
download + completed dirs | Extra :-separated roots data may be deleted from |
CASCADE_MAX_UPLOAD_MB |
64 |
Largest accepted .torrent upload, MiB |
CASCADE_POLL_MS |
1000 |
Backend sampling interval for the rate graph, ms |
CASCADE_GAMIFY |
1 |
Set 0 to remove levels, badges and celebrations |
CASCADE_WEB_ROOT |
/app/web |
Directory the built UI is served from |
Settings given as environment variables are applied over XML-RPC at startup rather than written into rtorrent.rc, so changes made in the UI last until the container restarts.
| Variable | Default | Meaning |
|---|---|---|
RT_EXTRA_CONFIG |
unset | Raw rtorrent.rc lines appended to the generated config |
RT_EXTRA_CONFIG_FILE |
unset | File of extra rtorrent.rc lines to append |
RT_CONFIG_FILE |
/config/rtorrent.rc |
Use this rtorrent.rc verbatim instead of generating one |
RT_CONFIG_KEEP |
1 |
Set 0 to regenerate RT_CONFIG_FILE on every start |
CASCADE_BOOT_SETTINGS |
/run/cascade/boot-settings.json |
Where the entrypoint stages the settings it hands the server |
The keyboard works throughout: / focuses search, n opens the Add dialog, ↑/↓ walk the
list, Ctrl/⌘-A selects everything visible, Delete removes the selection (Shift-Delete also
deletes its data), Esc clears the selection or closes what is open, and the column headers sort
from the keyboard too. Removing asks first, in a dialog that lists what is about to go; setting a
label offers the labels already in use.
The log reads as a log: rtorrent's raw epoch seconds become clock times in your own timezone, with the level shown as colour (warnings amber, errors red) and a separator wherever the log crosses midnight. A line that is not in rtorrent's format is shown exactly as written.
The log dialog carries a Verbosity row: the scopes RT_LOG_LEVEL baked in at container start
show as fixed tags, and the rest — debug, tracker_debug, dht_debug and friends — toggle live,
no restart. Raising one takes effect immediately and is remembered (re-attached after every
rtorrent restart, like throttle groups); switching one off stays live until rtorrent next starts,
because rtorrent has no command to detach a log scope — the toast says so when it happens. The
subsystem groups moved between releases (0.9.x has tracker_debug and friends, 0.16 replaced
them with tracker_events), so the row offers the union and a scope this build does not have is
refused by name. A raised scope is remembered in the state file, so it survives a container
restart as well — written straight into the generated rtorrent.rc, which is what lets it cover
rtorrent's own startup rather than starting once the web server has connected.
A torrent that rtorrent has stopped with an error — most famously "Download registered as completed, but hash check returned unfinished chunks" — carries a Recheck & restart button on the error banner in its details (and in the right-click menu): the data is rechecked and the torrent started again the moment the check completes, so the missing chunks are fetched instead of the torrent sitting stopped behind a finished progress bar. A plain Force recheck still leaves the torrent stopped for inspection.
Click a torrent for details — general, files with per-file priority, live peers and trackers. Peer and tracker rows expand for everything rtorrent knows: peer id, protocol extensions, direction, encryption and the preferred/snubbed/unwanted/banned flags.
Trackers show type, state, scrape counts and the peers returned by the last announce, with a countdown to the next one; expanding a row adds announce intervals, success and failure timings, and the latest event.
Files can be prioritised individually — skip, normal or high — with per-file progress.
Drag .torrent files onto the window — anywhere — and drop to add them. Magnet links and torrent
URLs can be dragged in the same way, straight from another browser tab.
Dropped files are added and started immediately — no dialog, no questions. The confirmation is a
pickup: a shockwave and sparks at the point of impact, a +N torrents score, and the payload
flying up into the Add button, which takes the hit. In the retro theme the pickup goes full
arcade — pixel rings, an eight-way pixel burst and points on the score; in black metal the drop
becomes a summoning — a sigil cast at the impact point, shards of ash and ember that fall as
they die, and the score counted in offerings.
Torrents are checked before rtorrent sees them, so a file that is not really a torrent tells you so instead of vanishing.
Use the Add torrent button when you want to choose a destination directory or label first, or to paste magnet links and URLs.
Sharing earns levels and badges. Every number behind them is a real transfer total pulled from
rtorrent — uploaded bytes, completed downloads, peak rates — accumulated across restarts, and XP
leans on uploading rather than downloading. Set CASCADE_GAMIFY=0 and the whole layer disappears.
rtorrent's live settings are editable, with anything the running version does not support greyed out.
rtorrent has no per-torrent rate limit — it throttles by named group. Create groups here and assign torrents to them from the right-click menu.
Anything not wrapped by the UI is reachable from the API console, which lists every command the backend exposes with its help text.
Themes are chosen from the header: System (follows the OS), Light, Dark, a Retro 8-bit mode with CRT phosphor colours, hard pixel edges, stepped progress bars, scanlines and a pixel-art arcade wordmark — and Black Metal: flat black, bone lettering, blood accents, film grain, torn sawtooth edges, jagged progress bars, and the wordmark replaced by a properly unreadable band logo. The gamification layer is re-carved to match — levels become ranks like Sower of Plagues, and the badges become sigils such as First Blood and Eternal Winter.
The retro treatment is driven entirely by the same design tokens, so it reaches every dialog:
And the light theme:
UI preferences — theme, sort column, detail-pane height — are saved server-side, so a new browser
or a different machine picks up the same setup. They live in /config/cascade-state.json next to
gamification progress, add times and throttle groups; that one file is the whole of Cascade's
persistent state, and deleting it resets everything.
The layout adapts rather than shrinking. Columns drop by usefulness as the window narrows, and below 720px the table becomes a card list with its own sort control in the toolbar, the sidebar becomes a drawer behind the filter button — carrying the settings, throttle and console tools as well as the filters — and the detail pane and dialogs become full-screen sheets. The live transfer rates stay in the header. Touch gets larger targets, a long press stands in for right-click, and polling pauses while the tab is in the background.
Keyboard: n add torrents · / search · ↑/↓ move through the list · Ctrl/⌘+A select all ·
Delete remove · Shift+Delete remove with data · Esc close details and clear the selection.
Rows support Ctrl-click and Shift-click ranges, and the right-click menu can copy a torrent's
magnet link.
All endpoints live under /api and honour the same Basic auth as the UI.
| Method | Path | Purpose |
|---|---|---|
GET |
/healthz |
Liveness (deliberately outside Basic auth, for healthchecks) |
GET |
/api/state |
Torrents, global status, throttle groups (the UI's poll) |
GET |
/api/torrents?view=main |
Torrent list for an rtorrent view |
GET |
/api/status |
Global rates, limits and backend summary on their own |
GET |
/api/capabilities |
Backend version and supported feature map |
GET |
/api/game |
Level, XP and badge progress |
GET/PATCH |
/api/prefs |
UI preferences (theme, sort, layout) |
GET |
/api/torrents/:hash/files | /peers | /trackers |
Per-torrent detail |
POST |
/api/torrents/upload |
Multipart: torrents[], urls, start, directory, label |
POST |
/api/torrents/url |
Add one magnet/URL as JSON |
POST |
/api/torrents/:hash/action/:action |
start, stop, pause, resume, recheck, recheck-restart, announce |
POST |
/api/torrents/action/:action |
Same, for a list of hashes |
PATCH |
/api/torrents/:hash |
priority (0 off … 3 high), label, throttle, directory, maxUploads, maxDownloads |
POST |
/api/torrents/remove |
Remove hashes, optionally deleteData |
POST |
/api/torrents/:hash/files/:index/priority |
0 skip, 1 normal, 2 high |
POST |
/api/torrents/:hash/trackers/:index/enabled |
Enable/disable a tracker |
GET/POST |
/api/settings |
Read/write rtorrent's live settings |
GET/POST/DELETE |
/api/throttles |
Manage throttle groups |
GET |
/api/log |
Tail of the rtorrent log |
GET/POST |
/api/log/scopes |
Log verbosity: raise scopes live, on top of RT_LOG_LEVEL |
GET |
/api/rpc/methods, POST /api/rpc |
Every rtorrent command, as JSON |
POST |
/api/rpc/help |
system.methodHelp / methodSignature for one command |
POST |
/RPC2 |
Raw XML-RPC passthrough |
Malformed input — a hash that is not forty hex digits, a priority outside its range, a file
index that is not a number — is answered with a 400 naming the field rather than forwarded to
rtorrent; the bulk routes apply their action per hash and return the failures by hash in
errors instead of stopping at the first.
/RPC2 lets existing tooling drive rtorrent over HTTP:
curl -u admin:change-me -X POST http://localhost:8080/RPC2 \
-H 'content-type: text/xml' \
--data '<?xml version="1.0"?><methodCall><methodName>system.client_version</methodName></methodCall>'import xmlrpc.client
rt = xmlrpc.client.ServerProxy("http://admin:change-me@localhost:8080/RPC2")
print(rt.system.client_version(), rt.d.multicall2("", "main", "d.name=", "d.down.rate="))To expose rtorrent's own SCGI socket instead, set RT_SCGI_PORT=5000 and
RT_SCGI_BIND=0.0.0.0, then publish the port. SCGI is unauthenticated — anyone who reaches
it has full control of rtorrent and can run commands on the host through execute. Keep it on a
private network, or prefer /RPC2, which sits behind Basic auth.
- File names longer than Linux allows are shortened to fit. A path component is capped at 255
bytes on ext4/xfs/btrfs and libtorrent opens files under the exact name from the torrent, so a
Thai, CJK or emoji-heavy title of ~85 characters used to fail with "Hash check I/O error at
chunk 0: Filename too long" and never start. The rtorrent in the image is built with a small
libtorrent patch (
docker/patches/) that cuts such a name at a character boundary, keeps the extension, and appends~plus a short tag of the original so two long names cannot collide; the torrent keeps its own names in the list and the Files tab, which notes on disk as … where the two differ. - Global settings changed in the UI are not persisted to
rtorrent.rc; the environment is the source of truth on restart. - "Change directory" updates rtorrent's session only. Move already-downloaded files yourself, or recheck afterwards.
- Deleting torrent data is confined to
RT_DOWNLOAD_DIR,RT_COMPLETED_DIRand anyCASCADE_DELETE_ROOTS; requests outside those are refused. - Throttle groups cannot be removed from a running rtorrent — deleting one sets it to unlimited and drops it from the UI list.
- rtorrent runs inside a detached
screensession, sodocker exec -it cascade screen -r rtorrentgives you the real curses UI. If rtorrent dies, the entrypoint restarts it. - rtorrent locks its session directory and only releases the lock on a clean shutdown, so a killed
container (
docker rm -f, OOM, host reboot) leaves one behind and every later start fails. The entrypoint clears a lock that no live process in the container holds; setRT_SESSION_LOCK_KEEP=1if you deliberately share a session directory and want the check to refuse instead. Stop the container withdocker stop(asmake stopdoes) to avoid it entirely.
The whole toolchain lives in the image; no local Node is required. The Makefile wraps the usual
work — make on its own lists every target.
make build # build the image (typechecks + unit tests, both halves)
make run PORT=8080 # run it, mounting ./data, then open it in a browser
make run OPEN=0 # ...without launching a browser
make open # wait for it to answer, then open it
make smoke # build, boot, exercise the API, tear down
make matrix # build against 0.9.8, 0.15.2 and 0.16.23
make build RTORRENT_VERSION=0.9.8
make attach # attach to rtorrent's curses UI
make logs / shell / stopBoth halves carry unit tests beside their sources (server/src/*.test.ts, web/src/*.test.ts),
written for node's built-in test runner — no frameworks, no new dependencies. The server suite
covers everything from the XML-RPC codec up to the HTTP routes: the client and the capability
probe run against a scripted transport, the service against a fake client that records what
would have reached rtorrent, and the express app is mounted on a spare port and driven with
fetch. They run inside every image build, so a red suite fails the build exactly as a type
error does; to run them alone (the repo root is mounted because the options check reads the
entrypoint and the README):
docker run --rm -v "$PWD":/r -w /r/server node:22-alpine sh -c 'npm install && npm test && npm run options:check'
docker run --rm -v "$PWD":/r -w /r/web node:22-alpine sh -c 'npm install && npm test'CI (GitHub Actions) runs the same checks: a fast typecheck and the unit tests of both TypeScript halves on every push and pull request, plus a full image build with an API smoke test on pull requests. A compatibility matrix against rtorrent 0.9.8 and 0.15.2 can be run from the Actions tab (Run workflow → full-matrix).
Pushing to main releases. The release workflow tags the commit v0.1.<run number>, builds the
image for amd64 and arm64 on native runners, boots each one and probes its API, and only then
joins them into the tagged manifests described under Image tags — so a build that
does not run never claims a tag. Pushing a vX.Y.Z tag yourself publishes under that name
instead of an auto-generated one.
make run waits for /healthz before launching the browser, so it opens on a working page rather
than a connection error. The launcher is xdg-open (BROWSER= overrides it, open is used as a
fallback on macOS); with no display detected it just prints the URL.
Working on the frontend with live reload, against a running container:
cd web && npm install && npm run dev # proxies /api to localhost:8080Layout:
Makefile build/run/test wrappers around Docker
server/src/ XML-RPC codec, SCGI transport, capability probe, REST API
web/src/ React UI (components/, styles.css)
docker/ entrypoint that renders rtorrent.rc and supervises both processes
See CLAUDE.md for the architecture details and the rtorrent quirks worth knowing before changing the backend.

















