Sign inSign up

thefroglab/frogstream

By thefroglab

•Updated about 7 hours ago

Headless torrent client for servers. Native AOT, no shell, non-root, ~15 MB.

Image
Networking
0

679

thefroglab/frogstream repository overview

⁠FrogStream

Headless torrent client for servers. Native AOT, no runtime to install, ~15.2 MB to pull.

Drop a .torrent or .magnet file into a watched directory and it downloads, then seeds. No web UI, no daemon protocol, no database — it reads a directory and writes files.

Prefer a screen to a directory? The FrogStream apps for Android and Windows — full standalone torrent clients in their own right — can pair with this container over your LAN and drive it: browse its queue live, send it new torrents, pause, resume and remove. Nothing is exposed to the internet and no account is involved.

Get the apps: Android — Google Play⁠ · Windows — Microsoft Store⁠

docker pull thefroglab/frogstream

⁠Quick start

mkdir -p downloads torrents
sudo chown -R 1654:1654 downloads torrents      # see "Volume permissions" below — do not skip

docker run -d --name frogstream \
  -p 65525:65525 \
  -v "$PWD/downloads:/data/downloads" \
  -v "$PWD/torrents:/data/torrents" \
  -v frogstream-config:/config \
  --restart unless-stopped \
  thefroglab/frogstream:0.3.3

Then drop files in and watch:

cp ~/Downloads/something.torrent ./torrents/
echo 'magnet:?xt=urn:btih:...' > ./torrents/something.magnet
docker logs -f frogstream
⁠docker compose
services:
  frogstream:
    image: thefroglab/frogstream:0.3.3
    restart: unless-stopped
    stop_grace_period: 15s
    ports:
      - "65525:65525/tcp"
    volumes:
      - ./downloads:/data/downloads   # chown 1654:1654
      - ./torrents:/data/torrents     # chown 1654:1654 — drop .torrent/.magnet here
      - frogstream-config:/config
    logging:                          # the metrics line is ~36 MiB/day per torrent
      driver: json-file
      options: {max-size: "10m", max-file: "3"}
volumes:
  frogstream-config:

⁠Control it from the Android or Windows app

FrogStream also ships as an app for Android and Windows. Each is a complete torrent client on its own — they don't need this container and this container doesn't need them. Pair them and the app becomes a remote for the server: its queue appears in the app, live, and you can add torrents, pause, resume and remove them from your phone or PC.

Get the apps: Android — Google Play⁠ · Windows — Microsoft Store⁠

The link is direct and encrypted, device to device on your network. There is no account, no cloud relay, and nothing to open on your router.

services:
  frogstream:
    image: thefroglab/frogstream:0.3.3
    restart: unless-stopped
    network_mode: host          # simplest — see "Bridge networking" below
    environment:
      DeviceLink__Enabled: "true"
      DeviceLink__DeviceName: "home-server"            # the name you'll see in the app
      DeviceLink__AcceptPairingSecondsOnStart: "300"   # 5-minute enrolment window on boot
      DeviceLink__AutoPair: "true"                     # container confirms the code itself
    volumes:
      - ./downloads:/data/downloads
      - ./torrents:/data/torrents
      - frogstream-config:/config
volumes:
  frogstream-config:

docker compose up -d, then docker compose logs frogstream should show [link] accepting inbound pairing for 300s (config bootstrap). Pair within that window.

⁠2. Pair from the app
  1. Open the app → Settings → turn on Connected devices.
  2. Open the Devices tab → Pair a new device.
  3. Pick the container from Devices on your network (it shows under its DeviceName). Not listed — bridge network, VPN, another subnet? Type the server's address under Connect by address (192.168.1.20, or 192.168.1.20:65526) and tap Connect.
  4. The app shows a 6-digit code. The container prints the same code in its log: [link] pairing (incoming) from <your phone> · fingerprint 482196. Check they match, then tap Codes match, confirm. With AutoPair on, the container confirms its side on its own.
  5. The server now sits under Paired devices. Trust is stored in the /config volume, so it survives restarts and upgrades — and the enrolment window does not reopen once a device is paired, which is what keeps restart: unless-stopped from leaving it permanently open.
⁠3. Use it
In the appWhat it does on the server
Devices → your server → Open queueLive view of the container's transfers: progress, speed, peers, free disk space
Pause / Resume / Remove on a transferActs on that download in the container. The header button pauses or resumes everything
+ (Add torrent) → paste a magnet or pick a .torrent → under Download on, pick your serverThe download runs in the container and lands in /data/downloads; the app shows it with the server's badge
Pull here on a finished transferCopies the completed files from the server to this phone or PC
Send in the server's queueSends files from this device to the server. They land under /config/received/ unless you set DeviceLink__ShareReceiveDirectory

If the server goes offline, its queue stays visible as Read-only · device offline until it is back.

⁠Bridge networking
  • network_mode: host is the easy path. On a bridge, the multicast discovery that lists devices automatically does not cross the bridge. Publish 65526/tcp and 65528/tcp, and pair with Connect by address instead of picking it off the list. Everything after pairing works either way.
  • AcceptPairingSecondsOnStart with AutoPair is trust-on-first-use. Anything that can reach the control port during that window can pair. Use it on a network you trust and keep the window short. Without AutoPair the container needs an attached console (-it) to accept confirm; AutoPair is the headless path.

⁠Volume permissions

The container runs as uid 1654 and never as root. A bind mount you create belongs to you, so the container cannot write it and the process exits on startup with UnauthorizedAccessException: Access to the path '/data/downloads' is denied.

sudo chown -R 1654:1654 ./downloads ./torrents

Named volumes (like frogstream-config above) get the right ownership automatically — this only affects bind mounts. If you need the files owned by your own user, run the container as yourself instead: --user "$(id -u):$(id -g)", with the directories owned to match.

⁠Configuration

Every setting is an environment variable. The defaults baked into the image are production defaults; you only need to set something to change it.

VariableDefaultWhat
Ingest__WatchDirectory/data/torrentsDirectory polled for .torrent / .magnet files
Ingest__DownloadDirectory/data/downloadsWhere completed data lands
Ingest__KeepSeedingtrueKeep seeding after a download completes
Ingest__Sources__0—Start a magnet or path at boot, no watch file needed
Ingest__WatchPollInterval00:00:10How often the watch directory is scanned
Torrent__ListeningPort65525Inbound peer port — publish and forward it
Torrent__MaxConnectedPeerCount1000Peers kept connected, across all transfers
Torrent__MaxPeersPerTorrent400Per-torrent sub-cap under the above (0 = none)
Torrent__MaxConcurrentDials0 (= 2 × MaxConnectedPeerCount, so 2000 by default)Connection attempts in flight, across all transfers. Above the peer cap on purpose — most dials never answer, so this is what sets discovery speed. Leave it at 0 unless you mean to pin it: lowering MaxConnectedPeerCount on a small host then lowers the socket count with it
Torrent__MaxDownloadBytesPerSecond0Rate limit in bytes/sec, process-wide, 0 = unlimited
Torrent__MaxUploadBytesPerSecond0Rate limit in bytes/sec, process-wide, 0 = unlimited
Torrent__UploadSlots4Peers unchoked for upload at once
Torrent__MetricsSampleInterval00:00:01How often the per-torrent stats line is emitted
Logging__LogLevel__DefaultInformationSet Warning to silence the per-second metrics line
Logging__LogLevel__FrogStream.Metrics.Peers(inherits)Set Debug for one record per connected peer per sample, without putting the whole app in Debug
Logging__LogLevel__FrogStream.Metrics.Files(inherits)Same, for per-file progress on a multi-file torrent
Logging__File__EnabledfalseOpt-in rotating log file. stdout stays the primary path
Logging__File__Directory/logsMount your own directory here — see below
Logging__File__LogLevel__Default(inherits)The file's own level, e.g. Debug to file while the console stays at Information
Logging__File__MaxSizeBytes / __MaxFiles10485760 / 3Rotation threshold and how many files are kept
Otel__Endpoint(empty)Set to an OTLP gRPC endpoint to export traces/metrics/logs
DeviceLink__EnabledfalsePair with the Android / Windows apps — see above
DeviceLink__DeviceName(hostname)The name this container shows up as in the apps
DeviceLink__AcceptPairingSecondsOnStart0Open a one-time pairing window on boot, in seconds
DeviceLink__AutoPairfalseAuto-confirm pairing requests during that window
DeviceLink__ShareReceiveDirectory(derived) /config/receivedWhere files sent from a paired app land

⁠Logs

The container logs to stdout, which is what docker logs, docker compose logs and every logging driver read. That is the primary path and needs no configuration.

Two optional extras:

# One record per connected peer per sample, without putting the whole app into Debug
docker run ... -e 'Logging__LogLevel__FrogStream.Metrics.Peers=Debug' thefroglab/frogstream

# Keep log files after the container is gone — mount your own directory at /logs
sudo chown -R 1654:1654 ./logs
docker run ... -e Logging__File__Enabled=true -v "$PWD/logs:/logs" thefroglab/frogstream

/logs is a mount point of its own, separate from /config, so you can put logs on a different disk or point a log shipper at them without touching the state volume. If you enable the file sink and don't mount anything there, the files go into the container's writable layer and disappear when the container is removed — the size cap still applies, but you get nothing back after a docker rm.

⁠Ports

PortPurpose
65525/tcpInbound peers. Publish and forward this, or you can download but barely seed.
65526/tcp, 65528/tcpDeviceLink control / file transfer. Only when DeviceLink__Enabled=true.

UPnP and NAT-PMP cannot reach your router from a bridged container, so the port has to be forwarded on the router by hand. network_mode: host avoids that, on Linux hosts.

⁠Tags

TagMeaning
0.3.3Immutable release. Pin this in production. Built on .NET 11 RC1.
0.3.2Previous release (.NET 11 RC1). Still available, no longer updated.
0.3.1Previous release (.NET 11 RC1). Still available, no longer updated.
0.3.0Previous release (.NET 11 RC1). Still available, no longer updated.
0.2.0Previous release (.NET 10). Still available, no longer updated.
0.1.0Older release. Still available, no longer updated.
latestMoves with every release. Convenient, not reproducible.

linux/amd64 only today. arm64 is planned.

⁠What it deliberately does not have

  • No web UI and no HTTP API. It is driven by the watch directory and env vars — or, if you turn DeviceLink on, by a paired FrogStream app over the LAN. There is nothing to expose to the internet.
  • No HEALTHCHECK. The image has no shell to exec one and the app exposes no HTTP surface. Watch docker logs instead.
  • No shell, no package manager. docker exec into it will not work — that is the point. It is a chiseled image with 119 files and no executables outside /app.

⁠Security posture

  • Runs as non-root uid 1654 by default.
  • No shell, no package manager, no setuid binaries.
  • Runs clean under --read-only --tmpfs /tmp, --cap-drop=ALL and --security-opt=no-new-privileges.
  • Idle RSS around 11 MB; starts inside --memory=64m.

FrogStream is proprietary software, free to run and redistribute as an unmodified image. See /app/LICENSE in the image.

It transfers whatever data you point it at. It hosts nothing, indexes nothing, and ships with no content or trackers. You are responsible for the legality of what you transfer.

Third-party components and their licences: /app/THIRD-PARTY-NOTICES.md, plus per-package copyright files under /usr/share/doc/ inside the image.

Licence questions: [email protected]⁠. Security disclosures: [email protected]⁠.


froglab.org⁠

Tag summary

Content type

Image

Digest

sha256:6706a1646…

Size

14.8 MB

Last updated

about 7 hours ago

docker pull thefroglab/frogstream