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.
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.
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 | 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. | — |
| 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. | :4848 |
| stun | Answers RFC 5389 binding requests so a peer learns its own reflexive address. Pure reflexion — no auth, no allocations. | :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. | :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. |
— |
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.
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.
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
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.
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
}
}
}
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.
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:
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.
49152–65535) and open only that.
username/password pair, or it shows enabled, not running.public_ip — it can't guess its own routable address.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
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:
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.
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.
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.
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" }
]
}
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.
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.
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.
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.
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.
signaling.servers set to your wss:// host, public_fallback: false.stun_servers / turn_servers as explicit lists pointing only at your host.
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."
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 connectedThe 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 clientUDP 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.
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.
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.
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
4848 · plain ws://. Front it with TLS for public wss://.3478 — the IANA STUN/TURN port. Don't run standalone STUN and TURN on the same port; TURN covers both.{ username, password }. Mirror a pair into each peer's turn_servers.0 = unlimited.0 = OS ephemeral range (open it all). Pin a window to shrink the firewall surface.50 events/s, 20 REQ/s, 64 subscriptions, 16 filters/REQ, 65536 bytes/frame, 64 conns/IP. 0 = no limit.0 = unlimited.~/.myownmesh/config.json. Move the whole tree with MYOWNMESH_HOME.