Nodes and Multiple Machines - Kheopsian/Hydranos GitHub Wiki
Applies to Hydranos 4.4.
This page is for you if you run Hydranos on more than one machine and want to see and move torrents between them from one web UI. It covers adding a machine (enrol or declare), what the enrolment script does to it, and which actions work on torrents of another machine. Whether you need several machines at all: Deployment Topologies.
A node is another complete Hydranos instance: the same hydranos program, with its own config file, store, engines, API key and web UI. There is no special node mode and no separate node daemon.
- The instance where you add a node is its controller. The controller keeps the node's name, URL and API key in its own store (table
nodesinhydra.db), not indefault.toml. The key is stored in plain text. - The relationship goes one way. The controller calls the node's normal HTTP API with the node's API key (header
X-API-Key). The node never calls the controller after enrolment, and it does not store the controller's address or key. - A node keeps running on its own. If the controller is down or deletes it, the node keeps seeding and its own UI keeps working. Removing a node on the controller only deletes the row (Remove node {name}? Its own data is untouched.).
- Torrent data never goes through the API. When a torrent moves between machines, only the
.torrentfile goes over HTTP. The bytes go over BitTorrent between the two engines' listen ports, and each piece is hash-checked. - Every call from the controller to a node (health probe, list, relayed action, handoff, fetch, key rotation) has a 4-second HTTP timeout.
- These calls never use the daemon's
egressroute, its[proxy]setting or a tunnel, even with the kill switch armed (anHTTP_PROXY/HTTPS_PROXYset in the daemon's environment is not switched off, so the HTTP client's default may still apply it): they leave by the host's normal route (VPN and Egress).
There are two ways to add a node:
| + Enrol a machine | + Declare an existing one | |
|---|---|---|
| Use it for | a fresh Linux machine with nothing installed | an instance that is already running (any OS, including Windows) |
| What you type | one curl … | sh command, on the new machine |
name, URL and the node's API key, on the controller |
| Who installs Hydranos | the script (Docker, a binary with a system unit, or a binary in one user's home) | you, beforehand (Docker, Linux, Windows) |
| Does the controller test the node first | no | yes, it refuses a node that does not answer |
On the controller, open the Nodes tab and click + Enrol a machine. The controller creates a one-time token and shows a command like this:
curl -fsSL http://192.0.2.10:8199/install.sh | sh -s -- --register-to http://192.0.2.10:8199 --token 0123456789abcdef0123456789abcdef- The token is 32 hex characters, valid for 30 minutes and usable once. The duration is fixed in the code and cannot be changed.
- The address in the command is the one your browser used to reach the controller (HTTP
Hostheader). If that islocalhostor a name the new machine cannot resolve, open the UI by an address it can reach and click again. - The command always uses
http://, even behind an HTTPS reverse proxy: the token and the node's new API key travel unencrypted. - Copy copies the command; Close hides the card (the token stays valid).
The script is the file install.sh from the repository root, built into the binary and served without authentication at /install.sh. Run it on a Linux machine. It installs Hydranos in one of three ways, picked in this order:
| Mode | When | What it installs |
|---|---|---|
| Docker |
docker is on the PATH
|
a container |
| system | no Docker, run as root |
/usr/local/bin/hydranos and a systemd unit |
| user | no Docker and not root (a seedbox account), or --user
|
~/.local/bin/hydranos and a systemd --user unit |
The Docker mode writes to /opt/hydranos by default: run it as root, or pass a --dir you can write. Here is what the script does:
-
It reads its options:
--register-toand--token(both required),--name(default: the output ofhostname -s),--port(default8199),--dir(see step 4),--container(Docker only, defaulthydranos),--image(Docker only, see step 5),--user(force the user mode). Any other option stops it with exit code 2. -
It finds the address the controller will use to reach this machine: the source address of
ip route get <controller>. If that fails, it stops. -
It picks the release to install: the controller's own version, read from the controller's
/health(tagv<version>), so a node runs the same release as its controller even when enrolled months later. The environment variableHYDRANOS_VERSION_PINoverrides it with a tag such asv4.4.0(curl … | HYDRANOS_VERSION_PIN=v4.4.0 sh -s -- …). If neither gives a version, Docker uses the image:latestand the binary modes ask GitHub for the latest release. -
It creates the config and data folders:
Mode Config file Data folder data_dirwritten in the fileDocker <dir>/default.toml(/opt/hydranos)<dir>/data/configs, the folder as the container sees itsystem <dir>/default.toml(/opt/hydranos)<dir>/data<dir>/datauser $XDG_CONFIG_HOME/hydranos/default.toml(default$HOME/.config/hydranos)$XDG_DATA_HOME/hydranos(default$HOME/.local/share/hydranos)the data folder In user mode,
--dirreplaces both:<dir>/default.tomland<dir>/data. If the config file does not exist, it writes one with a new random API key (48 hex characters):[daemon] api_host = "0.0.0.0" api_port = 8199 api_key = "<48 hex characters>" data_dir = "<see the table>" [race] listen_port = 16371 enable_ipv6 = true [hoard] listen_port = 16372 enable_ipv6 = true
An existing file is kept and its
api_keyreused. In user mode a new file is made readable by its owner only (mode 600): on a shared seedbox the key is the node's whole authority. BitTorrent ports are 16371 (race) and 16372 (hoard), not the usual 16171/16172: open them (TCP and UDP). -
Docker mode. If a container with the
--containername already exists, the script replaces it only if it carries the labelio.hydranos.enrolled=true, that is, if this script made it. Any other container stops the script witha container named hydranos already exists and was not made by this script; pass --container <another-name>, or remove it yourself. Then it runs:docker run -d --name hydranos --label io.hydranos.enrolled=true --restart unless-stopped --network host \ --stop-timeout 150 \ -v /opt/hydranos:/configs -v /opt/hydranos/data:/data \ ghcr.io/kheopsian/hydranos:v<controller version> --config /configs/default.toml
--imagereplaces the whole image name. The container uses the host network, so ports 8199, 16371 and 16372 are the host's ports. The config folder is mounted at/configs, and the store lives in/opt/hydranoson the host becausedata_dir = "/configs". -
System mode (root, no Docker). It downloads
hydranos-<tag>-linux-amd64.tar.gz(or-arm64; any other architecture stops it) from the GitHub release, installs/usr/local/bin/hydranos, writes/etc/systemd/system/hydranos.service(rewritten on every run; it runs as root, withRestart=always,TimeoutStopSec=150andLimitNOFILE=1000000), then runssystemctl enableandsystemctl restart. Withoutsystemctl, it prints the command to start Hydranos and exits with an error, before registering. -
User mode. It installs the same tarball's binary in
$HOME/.local/bin/hydranos, then:- if
systemctl --userworks in this login, it writes$XDG_CONFIG_HOME/systemd/user/hydranos.service(default$HOME/.config/systemd/user;Restart=always,TimeoutStopSec=150; noLimitNOFILE, the unit raises the open-file limit to the account's maximum instead), enables and restarts it. It then triesloginctl enable-lingerfor the account. If lingering stays off, it warnslingering is off for <user>: the node stops when you log out and stays down after a reboot.and asks you to have the host runloginctl enable-linger <user>; - otherwise (no user manager, for example an
sshlogin withoutpam_systemd), it starts Hydranos withnohup, writes its PID to<data>/hydranos.pid, and prints the command to start it again: nothing restarts it after a crash or a reboot. A re-run stops the previous process (same binary only) and waits up to 150 s for it.
- if
-
It polls
http://127.0.0.1:<port>/healthup to 60 times, one second apart, each try allowed 2 s: between about 1 and 3 minutes. A try counts only when/healthanswers and no longer says"status": "starting"(the port opens while the catalogue still loads). If the node never gets there, it exits without registering and names where the logs are. -
It posts
{token, name, url, api_key}to the controller's/api/nodes/register, withurl=http://<address from step 2>:<port>. -
If registration fails, it prints the URL and the API key so you can add the node by hand with + Declare an existing one, and exits with an error. The node stays installed and running.
A name that is already declared on the controller is refused (HTTP 409) before the token is spent, with a node named <name> already exists; the token is still valid, try --name: run the same command again with --name <another name>.
Other limits of the script:
- Linux only (
sh,ip,getent,curl,/dev/urandom, andtaroutside Docker). There is no enrolment script for Windows or macOS; declare those instances instead. - An IPv6-only route to the controller would produce a URL with the IPv6 address written without brackets, which the controller cannot use. Use IPv4, or declare the node by hand.
- Re-running the command on an enrolled machine keeps its config and key. It reinstalls on the controller's version (Docker: recreates its own container; system: replaces the binary and rewrites the unit), then registration ends in 409 (name taken), without spending the token: the node row is already there.
A name may contain - (seedbox-de, the placeholder of the Declare form, or a hostname -s default). A remote row is labelled <node>-<engine>, and the UI and the server match that label against the declared names, longest first: with a node nas-2, the row nas-2-hoard is node nas-2, engine hoard. A name cannot contain / or ..: it is a path segment of the Open link.
sequenceDiagram
actor Admin
participant C as Controller
participant N as New machine
Admin->>C: Nodes tab, + Enrol a machine
C->>C: POST /api/nodes/enrol, token valid 30 min, single use
C-->>Admin: curl command built from the browser's Host header
Admin->>N: paste the command in a shell
N->>C: GET /install.sh (no authentication)
C-->>N: the script
N->>N: own address = source of ip route get controller
N->>C: GET /health (no authentication)
C-->>N: the controller's version, the release to install
N->>N: write default.toml with a new API key (if absent)
N->>N: Docker container (replaces only one labelled by the script),<br/>or binary + system unit (root), or binary + user unit (--user)
N->>N: poll GET /health on 127.0.0.1 until it is not starting, 60 tries
N->>C: POST /api/nodes/register with token, name, url, api_key
alt missing token, name or url, loopback URL, or / or .. in the name
C-->>N: 400, token not spent
else name already known (checked before the token)
C-->>N: 409, token not spent, try --name
else token unknown, already used or expired
C-->>N: 401
else name taken meanwhile (checked again after the token)
C->>C: token spent
C-->>N: 409, existing node not overwritten
else accepted
C->>C: token spent, node stored, node not probed<br/>an empty api_key is accepted
C-->>N: 200
end
opt registration failed
N-->>Admin: prints URL and API key for + Declare an existing one
end
Admin->>C: open the Nodes tab later
C->>N: GET /api/status and GET /api/engines (live probe)
Use + Declare an existing one for an instance already installed: Docker, Windows, a machine where the script failed, or an upgraded 3.x machine.
- On the node, find its API key (Config › Advanced settings (default.toml) › General ›
api_key, or[daemon] api_keyin itsdefault.toml). See Security and Access. - On the controller, Nodes tab, + Declare an existing one, then fill in:
-
Name: no
/or..(see Node names above). -
URL: the node's origin, as other machines reach it, for example
http://198.51.100.5:8199. A trailing/is removed. - API key (the remote's own): the node's key, not the controller's.
-
Name: no
- Click Test. It probes the node without saving anything. On success it shows
Hydranos <version>, <engines>, <n> torrents. On failure it shows the reason (see the status table below). - Click Add. The controller probes again and refuses (HTTP 400) if:
- the name or URL is empty, or the name contains
/or..; - the URL host is a loopback address (
127.x.x.x,localhost,::1,0.0.0.0): Open sends the browser there, so other machines must be able to use it; - the probe fails.
- the name or URL is empty, or the name contains
Add with a name that already exists still replaces its URL and key, without asking. To change a node, use Edit on its row instead (see below).
An https:// URL is accepted (for a node behind a reverse proxy). This has not been tested for 4.4, and a handoff to such a node may not reach it (see Handoff below).
The tab loads once when you open it. It does not refresh on its own.
- This node: the local engines (columns Engine, Role, Listen port, Interface, IPv6, Socket, Torrents, Actions) and + Add an engine. Engines are covered in Engines: Race and Hoard.
-
Other nodes: one row per declared node, probed live each time the tab loads (all nodes at once). Columns: Name, URL, Version, Engines (from the node's
GET /api/engines), Torrents (sum over all its engines), Status, Actions. With no node: No other node.-
Open (only when the node is online) opens the node's own UI in a new tab, already logged in: the controller redirects the browser to
<node URL>/#key=<node API key>, and the node's page moves the key into its own browser storage. - Edit changes the node's name, URL or key (see Edit a node).
- Rotate key (greyed while the node is offline) gives the node a new API key (see Rotate a node's key).
- Remove deletes the row on the controller. Nothing happens on the node. A move to that node still waiting on the Jobs tab then fails, and the local copy is kept.
- Version is shown only. Nothing compares the controller's and the node's versions, and no route refuses a mismatch. Mixed versions are not tested: keep every machine on the same release.
-
Open (only when the node is online) opens the node's own UI in a new tab, already logged in: the controller redirects the browser to
Status values (the error strings come from the server and are shown untranslated, in French without accents):
| Status | Meaning | What to check |
|---|---|---|
online |
/api/status answered with JSON |
— |
timeout |
no answer within 4 s | node down, firewall, wrong port |
connexion refusee |
the connection could not be set up: refused, DNS failure, no route, TLS handshake | port, host name, firewall, https vs http
|
injoignable |
the request failed after the connection was set up | reverse proxy, node restarting |
cle API refusee |
node answered 401 or 403 | the node's key changed: declare it again with the new key |
HTTP <code> |
any other non-success status | a reverse proxy between the controller and the node |
| an error saying the answer is not from Hydranos | 200 but not JSON | something else listens on that URL |
There is no "enabled" switch: the store's enabled flag is never set to false.
Edit opens the Declare form filled with the node's name and URL. The button reads Save, and the API key field is empty with the placeholder unchanged: the stored key is never sent to the browser, and an empty field keeps it. Save sends PATCH /api/nodes/<current name>:
- A new URL or key is probed first, as on Add: refused (400) if the probe fails or the host is a loopback address. The form always sends the URL, so the probe always runs: an offline node cannot be renamed from the UI (the API accepts
{"name": …}alone, without a probe). - A new name follows the same rules as on Add (no
/or..). A name already taken is refused with 409a node named <name> already exists. If the URL or key changed in the same save, they are already stored when that 409 comes back. - Moves to this node still waiting on the Jobs tab follow the new name.
- Test in this form probes with the key field as typed: left empty, it tests without a key.
Rotate key asks Rotate the API key of {name}? Then:
- The controller asks the node for a new key (
POST /api/auth/api-key/rotateon the node, with the current key). The node generates it and keeps it pending, in memory, for 10 minutes; the current key still works. - The controller confirms by presenting the new key (
POST /api/auth/api-key/confirm). The node writes it to[daemon] api_keyin itsdefault.tomland refuses the old key from the next request. Browser sessions logged in with a password on the node stay valid. - The controller stores the new key and shows it once (New API key of {name} — Shown once. Copy it for the clients of this node that used the old key.). Give it to whatever else used the old key: Sonarr, Radarr or autobrr pointed at the node, scripts. Open hands the new key to the browser.
Errors: the node is unreachable, the node refused the current key: declare it again with its key (fix it with Edit), the node is too old to rotate its key (needs 4.4). If the controller stops between the two steps, the node keeps its old key and forgets the pending one after 10 minutes or a restart.
What the controller merges:
- The Hoard list (
/api/hoard/page) shows the rows of every node next to local rows. The Location column showslocal-<engine>for a local row and<node>-<engine>for a remote one (the API field is still namedagent, a legacy name). The node's own merge is included, so a node's extra hoard-role engines appear too. A node that errors or takes more than 4 s is left out of the page without a warning. Filter chip counts are summed over all nodes. - Ctrl+A on the Hoard list ("everything the filter matches") includes remote rows.
- The Race tab does not show remote rows: it reads the local race engine only.
The table below gives the result of each context-menu action from the controller's UI. "Relayed" means the controller sends the action to the node that holds the row, on the node's own route for that engine's copy; a refusal comes back as <node>: <the node's error>. "Skipped" means the bulk task reports {n} not on this node (or {n} not applicable for a race row) and does nothing. Bulk tasks and their progress panel are described on Jobs, Bulk Actions and Moving Data.
| Action | Local row | Remote row (<node>-<engine>) |
|---|---|---|
| Stop / Start | works | relayed (POST /api/engines/<engine>/pause on the node) |
| Reannounce | works | relayed |
| Recheck (shown when the selection holds a hoard row) | works, race rows included | relayed |
| Remove torrent / Remove + delete files | works, on the selected copy only | relayed: removes the node's copy in that engine |
| Change category › (label only) | works | relayed |
| Change category + move files › | works (job or graduation) | relayed: the node moves its own files (a job on the node). A refusal for hardlinks comes back as an error, without the consent prompt |
| Set location… | works | relayed, as above |
| Limit rate…, Sequential download, First/last piece first | works | relayed |
| Edit tags ›, Force download / Stop forcing | works on hoard rows | skipped |
| Share limits… | works | skipped |
Duplicate to engine › local-<id> (same files, second identity) |
works | not offered |
Move to engine › local-<id> (here, no copy) |
works | not offered |
Duplicate to engine › / Move to engine › <node>-<engine> (handoff) |
works | not offered |
local-<id> (bring it here) (fetch) |
not offered | works; offered under both Duplicate and Move, and the remote copy is always kept |
Move to engine › <node>-<engine> (there, no copy) |
not offered | works, between two engines of the node that holds the row |
| Detail panel, Edit trackers | works | the detail is looked up locally and does not load; Edit trackers edits this instance's copy of the same hash, if any |
| Export (zip, hash list, CSV) | works |
.torrent files come from the local store only; remote-only hashes go to missing.txt in the zip |
For the skipped items, click Open on the Nodes tab and do it in the node's own UI. A remote row whose node is no longer declared fails with no declared node holds <label>.
Known bug in 4.4: Edit trackers in the detail panel of a row from another node appears to edit that node's torrent — the detail of a remote row does not load, and the button edits this instance's own copy of the same hash, if there is one — click Open and edit the trackers in the node's own UI.
The Node group (Duplicate to engine ›, Move to engine ›) appears as soon as a node is declared or this instance has two local engines (every default install has race and hoard). The engine picker lists this instance's engines first, then one section per node; an offline node shows offline, a node without engines no engine, and if nothing applies you see Nowhere else to send it. A selection that spans several nodes, or mixes local and remote rows, gets no target.
Select local rows, then Duplicate to engine › or Move to engine › and pick <node>-<engine>.
- If the torrent has a category, the controller asks the node for its categories (a torrent without one, or a node whose category list cannot be read, skips this check). If the node has some, but not this one, the handoff fails and lists them: create the category on the node (with its own save path) and retry.
- It uploads the
.torrentto the node (POST /api/torrents/upload, with category and the chosen engine). The node places it as for any add (Categories and Routing). - It tells the node where the data is (
POST /api/torrents/<hash>/peerswithauto:<port>). The node replacesautoby the IP the request came from and dials the controller's engine on that port. If the node sits behind a reverse proxy, that IP is the proxy's and the dial goes nowhere, while the task still reports success. - The node downloads the data from the controller over BitTorrent.
Duplicate stops there: both instances keep a copy, and the bulk task reports success. Move (then: remove) also creates a handoff job on the controller, and the bulk task reports {n} sent; the local copy goes once the node holds it (Jobs). On the Jobs tab (Jobs, Bulk Actions and Moving Data) the job shows the state Waiting for the node, the destination <node>-<engine> and the node's progress:
- Every 30 s the controller asks the node for the torrent's progress (
GET /api/engines/<engine>/page?search=<hash>on the node; every engine of the node when none is named). Any engine works, extra ones such asvpn1included. - When the node reports 100 %, the job is Done and the controller removes the torrent from every local engine, with its files (files that another torrent still reads are kept).
- A failed poll is ignored and polling goes on. The deadline, 6 hours after the handoff, is stored in the job: a controller restart resumes the wait. Past it, the job fails with
the node did not report a full copy within 6 hours; the local copy is kept. - If the node is removed meanwhile, the job fails with
the node <name> was removed before it confirmed; the local copy is kept. A rename with Edit is followed. - Cancel on the Jobs tab stops the wait: both copies are kept.
- A second move of the same torrent to a node while one waits is refused (409):
a move of this torrent to a node is already waiting; see Jobs.
sequenceDiagram
actor Admin
participant C as Controller
participant N as Node
Admin->>C: Move to engine, pick node-hoard
opt torrent has a category
C->>N: GET /api/categories
end
alt node has categories, not this one
C-->>Admin: error listing the node's categories
else category known, node has none, no category, or the categories call failed
C->>N: POST /api/torrents/upload with .torrent, category, engine
C->>N: POST /api/torrents/HASH/peers with auto:PORT
C->>C: job handoff, state waiting, deadline in 6 h
C-->>Admin: bulk task: sent, the local copy goes once the node holds it (Jobs)
N->>N: auto = IP of the caller, queue a dial
N->>C: BitTorrent connection to the controller's engine
C-->>N: pieces over BitTorrent, hash-checked on the node
loop every 30 s while the job waits, resumed after a restart
C->>N: GET /api/engines/ENGINE/page?search=HASH
Note over C,N: a failed poll is ignored, polling goes on
end
alt node reports progress 1
C->>C: job done, remove the torrent from every local engine, with its files
else deadline passed
C->>C: job failed, local copy kept
else node removed on the controller
C->>C: job failed, local copy kept
else Cancel on the Jobs tab
C->>C: job cancelled, both copies kept
end
end
Select rows of one node, then Duplicate to engine › or Move to engine › and pick local-<id> (bring it here). The controller:
- refuses if the engine is not a local engine (400) or if this instance already has the torrent (409);
- downloads the
.torrentfrom the node (GET /api/torrents/<hash>/torrent) and reads the listen port of the node's engine that holds it (GET /api/engines); - adds the torrent to the chosen local engine, without a category, and dials the node on that port.
The data then comes over BitTorrent. A fetch never removes anything on the node: remove its copy in the node's own UI (Open) if you want.
On remote rows, Move to engine › <node>-<engine> (there, no copy) asks the node to move the torrent to its other engine (POST /api/torrents/<hash>/engine on the node). Nothing crosses the network; the node's answer is passed through.
The same operations exist as routes on the controller: POST /api/nodes/:name/handoff ({info_hash, engine, category, then, from}; with then: "remove" the answer carries the job id), POST /api/nodes/:name/fetch ({info_hash, engine, from_engine, category}, from_engine defaults to hoard), POST /api/nodes/:name/move-engine ({info_hash, engine}). See REST API.
Managing nodes:
-
PATCH /api/nodes/:name: any of{name, url, api_key}, at least one (else 400nothing to change: send name, url and/or api_key). Answers{"status":"ok","name":…,"health":…}; 404 for an unknown node. -
POST /api/nodes/:name/rotate-key: answers{"status":"ok","name":…,"api_key":<new key>,"health":…}; 502 with the reason when the node does not rotate. - On any instance, the two steps of a rotation:
POST /api/auth/api-key/rotate(current key inX-Api-Key; answers{"pending_key", "expires_in": 600, "confirm"}), thenPOST /api/auth/api-key/confirmwith the pending key inX-Api-Key. They also rotate the key of an instance that is not a node (Security and Access).
curl -X POST http://192.0.2.10:8199/api/nodes/seedboxde/handoff \
-H "X-Api-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"info_hash":"<40-hex info hash>","engine":"hoard","then":"keep"}'These stay per instance, even with nodes declared:
- Adding torrents: the + Add Torrent tab, watched folders and the shim add to local engines only. To put a torrent on a node from here, hand off one the controller already has.
- Header and Overview totals, rates, records and benchmark figures.
- The Race tab, the Trackers tab and tracker statistics.
- The qBittorrent shim (
/api/v2/*): Sonarr, Radarr, autobrr and cross-seed see the controller's torrents only. Point each tool at the instance that should hold its torrents (qBittorrent Shim and Automation). - Workflows: they run on the instance where they are defined, on its own torrents.
- Jobs, the drain, categories and configuration.
- MCP: read tools list the merged rows, but write tools act on local torrents only (MCP Server (AI clients)).
- The controller holds every node's API key, in plain text in its store. Anyone with the controller's API key or UI login can act on every node, and Open hands a node's key to any browser logged in to the controller. The enrolment card says the controller "never holds a credential" for the node: that is not true in 4.4. What is true: the controller never gets a shell or SSH access to the node. If the controller's key or store leaks, rotate every node's key with Rotate key, and the controller's own key (Security and Access).
-
/install.shand/api/nodes/registerneed no API key; register accepts only a valid, unexpired, unused token (expired tokens stay in the store, harmlessly). - Enrolment and node calls use plain HTTP by default. Keep nodes on a private network or a VPN between your machines. For HTTPS, put the node behind a reverse proxy and declare it with its
https://URL (see the handoff caveat above). See Security and Access. - Each node exposes its own API on port 8199 (
api_host = "0.0.0.0"with the enrolment script). Firewall it like any other instance.
- Deployment Topologies: when several machines are worth it.
- Engines: Race and Hoard: engines inside one instance, extra engines.
- Jobs, Bulk Actions and Moving Data: bulk tasks, jobs, data moves.
- Install with Docker and Install on Linux: installing a node yourself before declaring it.
- Known Limitations in 4.4: every gap in one list.