Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/target
/target-patched
/libtorrent-downloads
/rqbit-downloads
/vcpkg
/vcpkg_installed
err.txt
/System.Collections.Hashtable.Root/
.git
docs
scripts/tests
*.md
78 changes: 78 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# syntax=docker/dockerfile:1
#
# Multi-stage build for stream-server with the libtorrent backend.
# Mirrors the "check"/"build-linux" jobs in .github/workflows/release.yml:
# same base OS family (Debian/Ubuntu), same libtorrent version, same
# cargo feature flags, so the image matches what CI already validates.

########################################
# Stage 1: cargo-chef base
########################################
FROM rust:1-bookworm AS chef
RUN cargo install cargo-chef --locked
WORKDIR /app

########################################
# Stage 2: dependency recipe (for Docker layer caching)
########################################
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json

########################################
# Stage 3: builder
########################################
FROM chef AS builder

ENV DEBIAN_FRONTEND=noninteractive \
LIBTORRENT_STATIC=1 \
PKG_CONFIG_PATH=/usr/local/lib/pkgconfig

RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential cmake curl pkg-config \
libssl-dev libboost-dev libclang-dev \
libgtk-3-dev libayatana-appindicator3-dev \
&& rm -rf /var/lib/apt/lists/*

# Build libtorrent-rasterbar 2.1.1 from source, statically, the same way CI does.
COPY .github/scripts/install-libtorrent.sh .github/scripts/install-libtorrent.sh
RUN bash .github/scripts/install-libtorrent.sh

# Cache dependency compilation separately from application code.
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json --features libtorrent --no-default-features

COPY . .
RUN cargo build --release --features libtorrent --no-default-features

########################################
# Stage 4: runtime
########################################
FROM debian:bookworm-slim AS runtime

ENV DEBIAN_FRONTEND=noninteractive \
RUST_LOG=info

# server links against the desktop-integration stack (tray icon / settings GUI)
# on all cfg(unix/windows/macos) targets, so the runtime image needs the
# shared libraries those bring in even though it starts headless. Installing
# the -dev metapackages (rather than hand-picking .so names) guarantees every
# transitive runtime library apt would install for the equivalent build
# dependency is present too, avoiding "error while loading shared libraries"
# at startup.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates ffmpeg \
libssl-dev libboost-dev libgtk-3-dev libayatana-appindicator3-dev \
&& rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/target/release/server /usr/local/bin/stream-server

# HTTP API / streaming, HTTPS, and the default BitTorrent listen range
# (btListenInterfaces, configurable via /settings).
EXPOSE 11470/tcp 12470/tcp 42000-42010/tcp 42000-42010/udp

VOLUME ["/root/.config/stremio-server", "/root/.cache/stremio-server"]

# --no-tray forces the headless code path (no tray/GUI init), since the
# release binary otherwise defaults to tray mode, which requires a display.
ENTRYPOINT ["/usr/local/bin/stream-server", "--no-tray"]
127 changes: 127 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,133 @@ The server starts on `http://localhost:11470` by default (compatible with standa

---

## 🐳 Docker

A multi-stage `Dockerfile` builds the `libtorrent` backend the same way CI does (libtorrent 2.1.1 from source, `--features libtorrent --no-default-features`):

```bash
docker build -t stream-server:libtorrent .

docker run -d --name stream-server \
-p 11470:11470 \
-p 42000-42010:42000-42010/tcp \
-p 42000-42010:42000-42010/udp \
-v stream-server-config:/root/.config/stremio-server \
-v stream-server-cache:/root/.cache/stremio-server \
stream-server:libtorrent
```

- The container starts headless automatically (equivalent to `stream-server --no-tray`) — no display server required.
- Mount `/root/.config/stremio-server` (settings, HTTPS certs, logs) and `/root/.cache/stremio-server` (downloads/cache) as volumes so they survive container recreation. `HOME`/`XDG_CONFIG_HOME`/`XDG_CACHE_HOME`/`XDG_DATA_HOME` are also honored if you'd rather point everything at one mounted directory.
- BitTorrent peer traffic defaults to ports `42000-42010` (TCP+UDP, `btListenInterfaces` in `/settings`). Publish that range (or run with `--network host`) for good peer connectivity.
- `RUST_LOG` is set to `info` in the image; override with `-e RUST_LOG=debug` for more verbose logs.
- Pass `--tui` as an extra image argument (with `docker run -it`, or `-d -it` to attach later) for the built-in terminal stats dashboard — see [Accessing & Configuring the UI](#-accessing--configuring-the-ui).

### `--network host`

The default ports (`11470`/`12470`) or BitTorrent range (`42000-42010`) may already be taken by something else on the host (another stream-server instance, Stremio's own `server.js`, etc.) — this is common if you're running this alongside other services. Since `--network host` means Docker can no longer remap ports for you, the ports are overridable via environment variables:

| Variable | Default | Purpose |
| --- | --- | --- |
| `STREAM_SERVER_HTTP_PORT` | `11470` | HTTP API/streaming port |
| `STREAM_SERVER_HTTPS_PORT` | `12470` | HTTPS port (only serves if certs exist in the config dir) |
| `STREAM_SERVER_PUBLIC_BASE_URL` | `http://127.0.0.1:<http_port>` | Base URL the server reports about itself, e.g. in `/settings` |

```bash
docker run -d -it --name stream-server \
--network host \
--restart unless-stopped \
-v stream-server-data:/data \
-e HOME=/data -e XDG_CONFIG_HOME=/data/config -e XDG_CACHE_HOME=/data/cache -e XDG_DATA_HOME=/data/share \
-e STREAM_SERVER_HTTP_PORT=11472 -e STREAM_SERVER_HTTPS_PORT=12472 \
-e STREAM_SERVER_PUBLIC_BASE_URL=http://192.168.1.5:11472 \
--device /dev/dri:/dev/dri \
stream-server:libtorrent --tui
```

`--device /dev/dri:/dev/dri` passes through the host GPU for hardware-accelerated FFmpeg transcoding, if available. `-it` (kept even when running detached with `-d`) allocates a pty so `docker attach` can show the `--tui` dashboard later without disrupting the running server (see below).

This exact configuration has been built and deployed end-to-end (image build, container run, `/heartbeat`, `/settings` tuning, and TUI attach all verified) on an x86_64 Fedora/Docker host running alongside other, unrelated streaming services. Add `STREAM_SERVER_BT_*` env vars from [Tuning for playback speed](#tuning-for-playback-speed) here too if you want them baked into the deployment — note that they only take effect on a *fresh* volume/config dir; `stream-server-data` above is reused on purpose to carry over an existing deployment's cache and settings, so its already-persisted `settings.json` takes precedence regardless of env vars.

## 🖥️ Accessing & Configuring the UI

Stream Server has **no bundled web UI** — it's a drop-in backend that a Stremio client (web, desktop, or mobile app) talks to, the same way it talks to Stremio's official `server.js`. To use it:

1. Open the [Stremio web app](https://web.stremio.com) or a desktop/mobile Stremio client.
2. Go to **Settings → Streaming Server**.
3. Set **Server URL** to where this container/binary is reachable, e.g. `http://192.168.1.5:11470/` (swap in your host's IP/port).
4. Stremio should report the server as connected; playing a torrent stream will now route through this server instead of Stremio's built-in one.

It does have a **built-in terminal (TUI) stats dashboard** — a live view of active engines/torrents and files, built with `ratatui`. Start the binary/container with `--tui` (alongside `--no-tray` for headless/Docker use) to enable it:

```bash
# Native
stream-server --no-tray --tui

# Docker: run with -it (even when backgrounded with -d) so a pty exists to attach to
docker run -d -it --name stream-server ... stream-server:libtorrent --tui
docker attach stream-server # view the live dashboard
# detach without stopping the container: Ctrl+P, Ctrl+Q
```

The TUI needs a real terminal (it's a no-op, non-fatal, if none is attached), so it doesn't fit a plain `docker run -d` without `-it`.

For remote/scripted access instead, use the JSON endpoints directly:

```bash
curl http://192.168.1.5:11470/heartbeat # health check
curl http://192.168.1.5:11470/stats.json # server + torrent stats
curl http://192.168.1.5:11470/settings # current settings, including BitTorrent options
```

If desktop-app config is what you want instead, `settings-gui` (built alongside `server` on Windows/macOS/Linux desktop builds) provides a native settings window — not applicable to the headless Docker image above.

## ⚙️ Configuring libtorrent

BitTorrent/libtorrent behavior is configured at runtime via the same `/settings` API used above, and persisted to `settings.json` in the config directory (`~/.config/stremio-server/settings.json` on Linux, or wherever your config volume/`XDG_CONFIG_HOME` points in Docker) — no rebuild needed. Full option reference: [docs/bittorrent-settings.md](docs/bittorrent-settings.md).

```bash
# View current settings
curl http://192.168.1.5:11470/settings

# Update just the keys you want to change
curl -X POST http://192.168.1.5:11470/settings \
-H "Content-Type: application/json" \
-d '{"btEnableDht": false, "btEncryptionMode": "require"}'
```

Key options: `btEnableDht`, `btEnablePex`, `btEnableLsd` (peer discovery), `btEncryptionMode` (`allow`/`require`/`disable`), `btAnonymousMode`, `btAllowMultipleConnectionsPerIp`, `btValidateHttpsTrackers`, `btSsrfMitigation`.

### Tuning for playback speed

Every field in `/settings` can also be set as a `STREAM_SERVER_*` environment variable (e.g. `btMaxConnections` → `STREAM_SERVER_BT_MAX_CONNECTIONS`, camelCase → SCREAMING_SNAKE_CASE), which is read once when the server constructs its **default** settings on first startup. This only shapes the *initial* config for a fresh deployment (no `settings.json` yet in the config dir) — once `settings.json` exists, it's the source of truth and `/settings` (below) is how you change things from then on.

The shipped defaults are already tuned for a single-stream home connection in the ~100Mbps class:

| Env var | Default | Why |
| --- | --- | --- |
| `STREAM_SERVER_BT_MAX_CONNECTIONS` | `800` | Per-torrent connections are separately clamped to 200 once the global max reaches 800 (`TorrentSpeedProfile::effective_connection_limits`), so raising this further doesn't speed up a single stream — it only helps if you're running several torrents at once. |
| `STREAM_SERVER_BT_MIN_PEERS_FOR_STABLE` | `3` (was `5`) | The engine reports "stable" (and playback can start) with fewer peers connected — a little less early-playback smoothness for a faster start. |
| `STREAM_SERVER_BT_ALLOW_MULTIPLE_CONNECTIONS_PER_IP` | `true` (was `false`) | More usable connections when many peers sit behind the same IP (CGNAT, seedboxes). |
| `STREAM_SERVER_BT_ENCRYPTION_MODE` | `allow` | Accepts both encrypted and plaintext peers, maximizing the usable peer pool. Only use `require` if your ISP throttles unencrypted BitTorrent traffic. |
| `STREAM_SERVER_BT_DOWNLOAD_SPEED_HARD_LIMIT` | `11796480` (~11.25MB/s, bytes/sec) | ~90% of a 100Mbps line. This is the one speed-limit field actually wired to libtorrent (`download_rate_limit`); leaving headroom below your real line rate avoids bufferbloat, which costs more effective throughput — and playback smoothness — than it gains. **Adjust this for your own link speed**: roughly `(Mbps × 1_000_000 / 8) × 0.9`. Set to `0` for unlimited. |
| `STREAM_SERVER_BT_DOWNLOAD_SPEED_SOFT_LIMIT` | `0` | Present in the API for parity, but not currently wired to anything in the libtorrent backend — changing it has no effect yet. |
| `STREAM_SERVER_SEEDING_ENABLED` | `true` | Keeps seeding after download completes, which improves download speed too via tit-for-tat reciprocity with peers. |

All other `ServerSettings` fields (`btEnableDht`/`Pex`/`Lsd`, `btAnonymousMode`, `btListenInterfaces`, `btOutgoing*`, the `btProxy*` group, `btValidateHttpsTrackers`, `btSsrfMitigation`, `cacheSize`, `proxyStreamsEnabled`) follow the same `STREAM_SERVER_<FIELD_IN_SCREAMING_SNAKE_CASE>` pattern — see `server/src/routes/system.rs`'s `Default for ServerSettings` for the exact list. Don't set `btSsrfMitigation`/`btValidateHttpsTrackers` to `false` chasing speed — they don't meaningfully affect throughput, only safety.

Equivalent one-off tuning via the running API instead of env vars (useful for a change you don't want to bake into the deployment):

```bash
curl -X POST http://192.168.1.5:11470/settings \
-H "Content-Type: application/json" \
-d '{"btMaxConnections": 800, "btDownloadSpeedHardLimit": 11796480}'
```

Beyond `/settings`, run with `--network host` and publish/forward the `btListenInterfaces` port range — libtorrent's UPnP/NAT-PMP (always on) can then successfully port-map with your router for real incoming connections, which matters more for peer availability than any single setting above.

---

## 🔧 Build Instructions

<details>
Expand Down
22 changes: 19 additions & 3 deletions server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,26 @@ impl ServerConfig {
}

pub fn binary_default() -> Self {
// Overridable so multiple instances (or instances sharing a host with
// other services on the default ports) can run with --network host,
// where Docker can no longer remap ports for us.
let http_port = std::env::var("STREAM_SERVER_HTTP_PORT")
.ok()
.and_then(|v| v.parse::<u16>().ok())
.unwrap_or(DEFAULT_HTTP_PORT);
let https_port = std::env::var("STREAM_SERVER_HTTPS_PORT")
.ok()
.and_then(|v| v.parse::<u16>().ok())
.unwrap_or(DEFAULT_HTTPS_PORT);
let public_base_url = std::env::var("STREAM_SERVER_PUBLIC_BASE_URL")
.ok()
.filter(|v| !v.is_empty())
.unwrap_or_else(|| format!("http://127.0.0.1:{http_port}"));

Self {
http_addr: SocketAddr::from(([0, 0, 0, 0], DEFAULT_HTTP_PORT)),
https_addr: Some(SocketAddr::from(([0, 0, 0, 0], DEFAULT_HTTPS_PORT))),
public_base_url: Some(format!("http://127.0.0.1:{DEFAULT_HTTP_PORT}")),
http_addr: SocketAddr::from(([0, 0, 0, 0], http_port)),
https_addr: Some(SocketAddr::from(([0, 0, 0, 0], https_port))),
public_base_url: Some(public_base_url),
config_dir: None,
cache_dir: None,
use_tui: false,
Expand Down
Loading