Server mode · self-hosting guide

Be your own signal, STUN & TURN layer.

A fresh install leans on the project's reference servers, so it just works. But none of that is infrastructure you're stuck with. Every piece of the plumbing is the same myownmesh binary you already have — flip it on and the fleet finds it. This is the step-by-step for running your own.

v0.2.0 · alpha signaling · STUN · TURN · relay no public infra required

What "server mode" actually is

There's no separate server build. A MyOwnMesh device is any combination of a mesh member and the infrastructure roles it hosts for everyone else. You turn roles on; the daemon starts the listeners and advertises them to the fleet. Hosting is configured device-wide (not per network) — a role serves every network this device is on, plus any outside client you point at it.

Role What it does Default Port
node Participate as a normal mesh member — joins your configured networks. Turn it off for a pure-infrastructure box that hosts services and joins nothing. on
signaling A self-hosted Nostr relay (NIP-01 over WebSocket) peers use in place of the public pool — with live presence, instant-departure, and flood limits, so it's safe to run publicly. off :4848
stun Answers RFC 5389 binding requests so a peer learns its own reflexive address. Pure reflexion — no auth, no allocations. off :3478
turn Relays media/data for peers behind symmetric NAT (RFC 5766), with an optional per-connection bandwidth cap. A TURN server answers STUN too, so it covers both on one port. off :3478
relay Application-layer routing: forwards typed-channel frames between roster members so peers that can each reach this box, but not each other, still talk. Roster-gated both ends; needs node on. off

Everything except node is off until you opt in. When a role is enabled the daemon advertises a capability tag (service:signaling, service:stun, service:turn, service:relay) in the hello handshake every peer already exchanges — which is how the fleet self-discovers your box.

Stand one up, step by step

We'll take a fresh box to a public, TLS-terminated signaling relay with STUN + TURN, running as a service that survives reboot. Skip the steps you don't need — STUN/TURN and signaling are independent.

1

Get the daemon on the box

One line installs the myownmesh daemon (and the desktop GUI, unless you skip it). On a headless server, pass --no-gui — the daemon is all you need to host.

# headless server — daemon only, no desktop GUI
curl -fsSL https://myownmesh.net/install.sh | sh -s -- --no-gui
# Windows (PowerShell) — daemon only
iex "& { $(irm https://myownmesh.net/install.ps1) } -NoGui"
# From source — needs cargo (https://rustup.rs)
git clone https://github.com/mrjeeves/MyOwnMesh
cd MyOwnMesh
cargo install --path crates/myownmesh

Start the daemon in the foreground to confirm it runs; later we register it as a real service. On a headless box a bare myownmesh just points you here — serve is the explicit daemon entry point.

# run it in the foreground (Ctrl-C to stop)
myownmesh serve
2

Decide the box's job

A device can host services and still be a member, or be pure infrastructure. A dedicated relay box usually wants the latter: host signaling / STUN / TURN, join no networks. That's one toggle — turn node off.

# pure-infrastructure box: host services, join nothing
myownmesh ctl services disable node

ctl commands talk to a running daemon over its local control socket, so leave myownmesh serve running in another shell (or finish step 6 first). Toggling node joins or leaves every configured network live — no restart.

3

Turn the services on

Three equivalent ways: the CLI (flips one flag and persists), config.json (hand-edit + restart), or the GUI (Settings → Services). The CLI is quickest for the simple on/off:

myownmesh ctl services enable signaling
myownmesh ctl services enable turn        # gives you STUN for free, same port
myownmesh ctl services status

The CLI toggle can't set TURN credentials, the public IP, the flood limits, or the bandwidth cap — those live in config.json. Open it with myownmesh config edit and write the services block. A minimal example for a signaling + TURN host:

{
  // ~/.myownmesh/config.json
  "version": 1,
  "services": {
    "signaling": { "enabled": true, "bind": "0.0.0.0", "port": 4848 },
    "turn": {
      "enabled": true,
      "bind": "0.0.0.0",
      "port": 3478,
      "public_ip": "203.0.113.7",          // this box's routable IP
      "realm": "myownmesh",
      "credentials": [
        { "username": "alice", "password": "use-a-real-secret" }
      ],
      "max_bps_per_connection": 0          // 0 = unlimited
    }
  }
}
Good to know node defaults on and the signaling limits fill in safe defaults, so neither has to appear in a hand-written config. After a hand edit, restart the daemon (myownmesh serve) or re-apply live from the GUI / CLI.
4

Make TURN actually reachable — open the ports

This is the one that bites people. :3478 is only the control channel; every relayed allocation flows through a separate UDP port. By default those come from the OS ephemeral range, so you open that whole range too. The enable turn command prints the exact checklist:

what `enable turn` tells you to open
$ myownmesh ctl services enable turn TURN is on. For NAT'd peers to actually relay, these UDP ports must be reachable — at the host firewall AND your cloud/provider security group: • udp 3478 — STUN/TURN control • udp <OS ephemeral range> — relay allocations (one port per peer) find your range: sysctl net.ipv4.ip_local_port_range (e.g. 32768 60999) $ sudo ufw allow 3478/udp $ sudo ufw allow 32768:60999/udp
The classic failure A host firewall showing inactive does not mean your cloud provider lets the packets in — open the ports in the security group too. Opening only 443 (for the signaling proxy) and forgetting UDP is why every client shows 0 srflx · 0 relay candidates.

Want a smaller firewall rule? Pin a fixed window with relay_port_min / relay_port_max (e.g. 4915265535) and open only that.

  • TURN won't start without a credential — at least one username/password pair, or it shows enabled, not running.
  • TURN won't start on a wildcard bind without public_ip — it can't guess its own routable address.
5

Take signaling public over wss://

The relay speaks plain ws:// and has no built-in TLS. For a public endpoint you want TLS on 443 (it also sails through restrictive firewalls). One command installs Caddy, writes the site block, binds the relay to loopback, and starts the service:

myownmesh install caddy relay.example.com
go public over wss:// — one command
$ myownmesh install caddy relay.example.com Caddy installed. Wrote reverse-proxy block to /etc/caddy/Caddyfile Caddy service is running with the new config. Relay enabled and bound to 127.0.0.1 (reachable only via Caddy). Done. Peers can now point at wss://relay.example.com

The generated site proxies only WebSocket upgrades to the relay and answers everything else with a plain 200. Two things still have to be true for TLS to come up:

  • DNS — an A/AAAA record for the hostname resolves to this box.
  • Firewall — inbound TCP 80 and 443 open (Caddy needs 80 for the ACME challenge).
Give the relay its own host Point peers at the relay's hostname, not a static site. myownmesh.net is GitHub Pages — it can't host a WebSocket server, so wss://myownmesh.net would hit the site, not a relay. Use a dedicated subdomain that resolves to the box the daemon runs on.
6

Keep it running across reboots

Register serve with the host init system — systemd on Linux, launchd on macOS — so the daemon survives logout and reboot.

# per-user service: no root, starts at login (Linux enables lingering)
myownmesh service install

# or system-wide: root, starts at boot, state under /var/lib/myownmesh
sudo myownmesh service --system install

service status reports installed / enabled / running with a log hint; start · stop · restart · uninstall do the rest. On a headless / SSH-only Mac, prefer --system — a LaunchAgent needs a GUI session to load.

Two different "services" myownmesh service … manages the daemon process. myownmesh ctl services … toggles the mesh's hosted roles (relay / signaling / STUN / TURN). Easy to mix up; they're unrelated.

Point your peers at it

Hosting is only half of it — peers have to use your box. Each peer edits its network's transport config (GUI: a network's gear → Settings; or config.json). You can also let auto-discovery do it: with TURN's public_ip set, your host advertises concrete endpoint URLs that a peer can adopt straight into its config.

{
  // one network entry on a peer's config.json
  "id": "home",
  "network_id": "my-cool-mesh",
  "signaling": {
    "servers": ["wss://relay.example.com"],   // non-empty = ignore the public pool
    "public_fallback": true                    // false = strictly your relays
  },
  "stun_servers": [{ "urls": ["stun:relay.example.com:3478"] }],
  "turn_servers": [
    { "urls": ["turn:relay.example.com:3478"],
      "username": "alice", "credential": "use-a-real-secret" }
  ]
}

Signaling takes precedence

A non-empty signaling.servers list takes full precedence over the built-in public relays — that's how you cut a network off public Nostr. Set public_fallback: false to never touch public infra, even when yours is down.

STUN / TURN are additive

Entries add to the defaults. Want only your own? Write an explicit empty array first ("stun_servers": []) then add yours. TURN credentials must mirror a pair from the server's credentials.

Or skip the URLs entirely

With public_ip set, the host advertises ws:// / stun: / turn: URLs in its capability blob. Peers read it and adopt the host on their own — nothing to hand out.

One host can do it all

TURN answers STUN on the same port, so stun: and turn: can point at one host:3478. Pair that with your signaling relay and a network needs nothing outside its own walls.

A fully internet-isolated network

Put it together and you get a network that touches no infrastructure you don't own — no Google STUN, no Cloudflare TURN, no public Nostr relay. One always-on box (or a few) supplies every piece of plumbing a closed fleet needs.

Discovery
Your signaling relay only — signaling.servers set to your wss:// host, public_fallback: false.
NAT traversal
Your STUN + TURN — stun_servers / turn_servers as explicit lists pointing only at your host.
Trust
Unchanged. ed25519 mutual auth runs over the WebRTC channel regardless of who hosts signaling — a forged relay event buys an attacker nothing but a failed handshake.
Degradation
The smart relay is an accelerator, never a coordinator. If it goes away, peers fall back to the engine's own reconnection ladder and the mesh keeps working.

Verify it & troubleshoot

ctl services status tells you what's bound and — for signaling — whether peers are actually reaching you. That last bit separates "nobody's connecting" from "I'm broken."

myownmesh ctl services status
$ myownmesh ctl services status { "node": { "enabled": true, "joined": 1 }, "signaling": { "enabled": true, "running": true, "listen": "0.0.0.0:4848", "activity": { "connections": 2, "rooms": 1, "events_relayed": 42 } }, "stun": { "enabled": false, "running": false, "listen": null }, "turn": { "enabled": true, "running": true, "listen": "0.0.0.0:3478" } }

A running: false on an enabled service means the start failed — usually a port already in use, or TURN missing its credentials / public IP. STUN shows running: false while turn is on because TURN already answers STUN on :3478 — that's expected, not a fault.

!

listening but no client connected

The relay is fine — traffic isn't arriving. Check in order: DNS (does the host resolve to this box?), TLS / proxy (is the proxy upgrading WebSockets?), then firewall. Confirm with npx wscat -c wss://relay.example.com.

!

0 srflx · 0 relay on every client

UDP isn't getting through. You opened TCP 443 for the proxy but not the TURN control port and its relay range — at the host firewall and the cloud security group. Re-run the enable turn checklist.

!

TURN shows enabled, not running

It needs at least one credential, and a public_ip when bound to a wildcard. Set both in config.json, then re-apply (GUI / CLI) or restart the daemon.

!

Want the underlying chatter?

The daemon logs connects, disconnects, and a 5-minute relay-activity heartbeat at info. For a deeper look, run with MYOWNMESH_LOG=debug,myownmesh=trace.

Reference

CLI cheat-sheet

myownmesh serve                          # run the daemon (foreground, headless)
myownmesh service install                # register as a background OS service
myownmesh ctl services status            # what's hosted + where it's listening
myownmesh ctl services enable  <svc>     # node | relay | signaling | stun | turn
myownmesh ctl services disable <svc>
myownmesh ctl services disable node      # → pure-infrastructure box
myownmesh install caddy <domain>         # TLS reverse proxy for wss://
myownmesh caddy path                     # print the Caddyfile location
myownmesh config edit                    # open ~/.myownmesh/config.json
myownmesh identity show                  # this device's id

Defaults & knobs

signaling.port
4848 · plain ws://. Front it with TLS for public wss://.
stun.port / turn.port
3478 — the IANA STUN/TURN port. Don't run standalone STUN and TURN on the same port; TURN covers both.
turn.public_ip
Required on a wildcard bind. The routable address handed out in relay allocations.
turn.credentials
At least one { username, password }. Mirror a pair into each peer's turn_servers.
turn.max_bps_per_connection
Per-allocation byte/sec cap, each direction. 0 = unlimited.
turn.relay_port_min / max
0 = OS ephemeral range (open it all). Pin a window to shrink the firewall surface.
signaling.limits
Per-connection flood guards. Defaults: 50 events/s, 20 REQ/s, 64 subscriptions, 16 filters/REQ, 65536 bytes/frame, 64 conns/IP. 0 = no limit.
relay.max_fanout
Broadcast fan-out ceiling. 0 = unlimited.
config + state
~/.myownmesh/config.json. Move the whole tree with MYOWNMESH_HOME.