SpawnDev.RTC.ServerApp is a single executable that speaks the WebTorrent tracker wire protocol over WebSocket. Any WebTorrent client - JS, .NET, Rust, Go - can announce against it. Any SpawnDev.RTC.Signaling.TrackerSignalingClient can also announce against it with a room key that has nothing to do with a torrent.
There are three common deploy shapes. Pick the one that matches your ops. All three front the server with TLS via a reverse proxy - browsers will refuse WebSocket connections that aren't either same-origin or TLS.
Heads up: the same binary can also host an embedded STUN/TURN server on the same box (no coturn needed) - just set a couple of env vars. See stun-turn-server.md for credentials, NAT port-range setup, and the env-var turnkey config.
Build from source (a published image is planned but not yet on a public registry):
git clone https://github.com/LostBeard/SpawnDev.RTC.git
cd SpawnDev.RTC
docker build -t spawndev/rtc-signaling -f SpawnDev.RTC/SpawnDev.RTC.ServerApp/Dockerfile SpawnDev.RTC
docker run -d -p 8080:8080 --restart unless-stopped \
--name rtc-signaling \
spawndev/rtc-signalingThe container listens on 0.0.0.0:8080. Front it with your reverse proxy of choice (examples below). The container exposes /announce for WebSocket, plus /health and /stats for ops.
Self-contained executables are produced by dotnet publish with a target RID. Pick the one your server runs:
cd SpawnDev.RTC/SpawnDev.RTC.ServerApp
# Linux x64 server
dotnet publish -c Release -r linux-x64 --self-contained \
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
# Linux ARM64 (Raspberry Pi 4/5, AWS Graviton, Oracle ARM)
dotnet publish -c Release -r linux-arm64 --self-contained \
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
# Windows x64
dotnet publish -c Release -r win-x64 --self-contained \
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
# macOS Apple Silicon
dotnet publish -c Release -r osx-arm64 --self-contained \
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=trueThe resulting binary lives at bin/Release/net10.0/<rid>/publish/SpawnDev.RTC.ServerApp (or .exe on Windows). Copy it to the server. Run it:
ASPNETCORE_URLS=http://0.0.0.0:5590 ./SpawnDev.RTC.ServerApp/etc/systemd/system/rtc-signaling.service:
[Unit]
Description=SpawnDev.RTC signaling server
After=network.target
[Service]
Type=simple
User=rtc
Group=rtc
WorkingDirectory=/opt/rtc-signaling
ExecStart=/opt/rtc-signaling/SpawnDev.RTC.ServerApp
Restart=on-failure
RestartSec=5
Environment=ASPNETCORE_URLS=http://0.0.0.0:5590
# Optional: tune the tracker
Environment=RTC__AnnounceIntervalSeconds=120
Environment=RTC__MaxPeersPerAnnounce=50
# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/log/rtc-signaling
[Install]
WantedBy=multi-user.targetThen:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin rtc
sudo mkdir -p /opt/rtc-signaling /var/log/rtc-signaling
sudo cp SpawnDev.RTC.ServerApp /opt/rtc-signaling/
sudo chown -R rtc:rtc /opt/rtc-signaling /var/log/rtc-signaling
sudo systemctl daemon-reload
sudo systemctl enable --now rtc-signaling
sudo systemctl status rtc-signalingBrowsers require TLS for WebSocket connections to any origin that isn't localhost. Do not expose the raw http://:5590 port to the public internet. Always front it with one of these.
tracker.example.com {
reverse_proxy localhost:5590
}Caddy auto-provisions a Let's Encrypt cert on first request. That is the whole config.
server {
listen 443 ssl http2;
server_name tracker.example.com;
ssl_certificate /etc/letsencrypt/live/tracker.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/tracker.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:5590;
proxy_http_version 1.1;
# WebSocket upgrade - required for /announce
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket connections can be long-lived
proxy_read_timeout 24h;
proxy_send_timeout 24h;
}
}frontend fe_tracker
bind *:443 ssl crt /etc/haproxy/certs/tracker.pem alpn h2,http/1.1
default_backend be_tracker
backend be_tracker
option forwardfor
server rtc 127.0.0.1:5590 checkCloudflare's free tier proxies WebSockets for any paid Cloudflare domain. Set the orange cloud on your DNS record, enable "WebSockets" in Cloudflare dashboard (default on). That's the whole story. The server sees X-Forwarded-For with the real client IP and X-Forwarded-Proto: https. ForwardedHeaders middleware picks these up automatically.
All settings read from appsettings.json, environment variables (double underscore for nesting), or command-line args. Later sources override earlier ones.
| Setting | Env var | Default | Purpose |
|---|---|---|---|
RTC:Path |
RTC__Path |
/announce |
Endpoint path for the WebSocket |
RTC:AnnounceIntervalSeconds |
RTC__AnnounceIntervalSeconds |
120 |
Re-announce interval returned to clients |
RTC:MaxPeersPerAnnounce |
RTC__MaxPeersPerAnnounce |
50 |
Ceiling on peer list per announce response |
RTC:MaxMessageBytes |
RTC__MaxMessageBytes |
1000000 |
Drop WebSocket frames above this size |
RTC:SendTimeoutMs |
RTC__SendTimeoutMs |
10000 |
Per-send timeout when forwarding relays |
ASPNETCORE_URLS |
ASPNETCORE_URLS |
http://0.0.0.0:5590 |
Listen address(es). Override for TLS or a different port. |
/health returns JSON including current room count:
curl https://tracker.example.com/health
# {"status":"ok","rooms":0,"peers":0}/stats returns a live snapshot:
curl https://tracker.example.com/stats
# {"rooms":2,"totalPeers":4,"roomDetails":[{"roomKey":"…","peers":2,"seeders":1,"leechers":1},…]}Point a WebTorrent client at your tracker:
// Browser JS (using the webtorrent npm package)
const client = new WebTorrent({
announce: ['wss://tracker.example.com/announce']
});
client.add(magnetURI, torrent => { /* torrent using your tracker */ });Or point a SpawnDev.RTC consumer at it:
using SpawnDev.RTC.Signaling;
var peerId = new byte[20];
System.Security.Cryptography.RandomNumberGenerator.Fill(peerId);
await using var client = new TrackerSignalingClient("wss://tracker.example.com/announce", peerId);
var room = RoomKey.FromString("my-app-lobby-42");
client.Subscribe(room, new RtcPeerConnectionRoomHandler(new() { /* ICE config */ }));
await client.AnnounceAsync(room, new AnnounceOptions { Event = "started", NumWant = 5 });Both consumers produce room entries on the same tracker; neither knows or cares what the other is doing.
- One tracker is plenty for most apps. The WebSocket is long-lived and signaling happens off-path once peers meet. A single tracker on modest hardware handles thousands of concurrent rooms.
- No database. All state lives in memory. Restart loses the peer list; clients re-announce within
AnnounceIntervalSecondsand the swarm rebuilds itself. This is by design - trackers are meeting points, not sources of truth. - No persistence configuration. There is nothing to back up. You are a coordinator, not a data store.
- Logging. Default level is
InformationforMicrosoft.Hosting.*andWarningforMicrosoft.AspNetCore.*. Tune viaappsettings.jsonLogging:LogLevel. - Memory baseline. Empty server sits around 40-50 MB RAM on linux-x64. Per-connection overhead is a single WebSocket plus a 16 KB receive buffer.
- Scaling. If you outgrow one box, run multiple trackers. WebTorrent clients natively announce to a list of trackers and merge peer lists. The SpawnDev.RTC client does the same via multiple
TrackerSignalingClientinstances.
- You get signaling that Google/Amazon/Microsoft don't proxy for your users. Self-hosted infra for self-hosted apps.
- WebTorrent compatibility is a bonus. Every instance you run helps every WebTorrent client on the public network find peers. You strengthen a decentralized commons by pursuing your own product's needs. The two goals aren't opposed; they're the same action.
- Zero operational cost beyond a small VPS. 1 GB RAM is 10x what you need.