Skip to content

Self-Hosting the Relay

The relay is open source under AGPL-3.0 and self-hostable, which is the way to remove Kangentic from your connection metadata entirely.

Source: github.com/Kangentic/relay.

The relay cannot read your traffic either way. What self-hosting changes is the metadata: who gets to observe your IP addresses, connection timing, and traffic shape. Running your own means the answer is “you”. See what a relay operator can still see.

  1. Clone and start it.

    Terminal window
    git clone https://github.com/Kangentic/relay.git
    cd relay
    cp .env.example .env
    docker compose up -d
    curl http://127.0.0.1:8080/healthz

    This pulls ghcr.io/kangentic/relay, the same image the hosted instance runs.

  2. Put TLS in front of it.

    The relay speaks plain HTTP and WebSocket and never terminates TLS. Your phone will refuse to pair with a relay that is not using TLS, so you need a reverse proxy. The repository ships a Caddyfile.example that handles certificates automatically.

  3. Point the desktop at it.

    In Settings > Mobile Devices, set Relay to Custom Relay and enter your address, for example wss://relay.example.com. Use Test connection to confirm it responds.

If you put a reverse proxy in front of the relay, you must set both TRUST_PROXY=true and TRUSTED_PROXY_CIDRS. The relay refuses to start with TRUST_PROXY=true and an empty CIDR list.

That refusal is deliberate. Trusting CF-Connecting-IP or X-Forwarded-For from any peer would let a client with no proxy in front of it forge either header and bypass every per-IP cap and rate limit.

Once a peer is trusted, X-Forwarded-For is read from the right, nearest hop first, skipping trusted-proxy hops. A proxy that appends to the header rather than replacing it is therefore still safe.

Your reverse proxy should also strip headers it is not authoritative for. The shipped Caddyfile.example does this:

Caddyfile
relay.example.com {
reverse_proxy relay:8080 {
header_up -CF-Connecting-IP
header_up X-Forwarded-For {client_ip}
header_up -X-Real-IP
header_up -Forwarded
}
}

The -CF-Connecting-IP strip matters: the relay reads that header first, so leaving it in place with no Cloudflare in the topology would let any client forge it.

Everything else is optional. Every variable has a working default, and Relay Reference documents the full set.

docker-compose.yml reads IMAGE_TAG and defaults to latest, which tracks the newest commit on the main branch rather than a release.

The address you give the desktop has to satisfy the phone as well, which enforces the same rules independently when it scans the pairing QR code:

  • wss:// is required. ws:// is accepted only for localhost, 127.0.0.1, and [::1].
  • No backslashes, no URL fragment, and no username or password in the URL.
  • 512 bytes maximum, because the address has to fit in the pairing QR payload.

The table matching two peers to a slot lives in the relay process’s memory, so both peers of a connection must reach the same process.

The same applies to DNS: never point a relay hostname at more than one server.

A small VPS goes a long way. Measured on a two-core instance, 500 concurrent pairs at 10 frames per second each hold sub-3 ms median latency at around 93 MB resident, and 2,500 pairs cost roughly 8 KB per socket. The practical ceilings are bandwidth and file descriptors rather than CPU, so raise LimitNOFILE or ulimit -n comfortably above MAX_CONNECTIONS. The shipped compose file sets it to 65535.

Because a slow consumer’s buffer can grow to MAX_BUFFERED_BYTES, connection caps alone do not bound memory. Set a container memory limit as the real backstop.

The relay is AGPL-3.0-only and self-hostable indefinitely. With no admission webhook configured, which is the default, every connection is admitted, and the relay never talks to any control plane. See Kangentic Relay.

Next: Relay Reference - every environment variable, endpoint, and close code.

See also:

Kangentic is free and open source. A star helps other people find it.

Star on GitHub