clone

a self-hosted sync node for lofi apps: jazz sync over iroh, ticket-gated, with no static ip or cloud needed

release/0.4.0
lofi-node / docs / site / beyond-the-lan.md
beyond-the-lan.md 7.4K rendered
1# Deploying beyond the LAN23<!-- Source: FelineStateMachine/lofi-node README.md, docs/hosting-lofi-apps.md,4 deno.json compile task, src/native/loader.ts. -->56A node on a trusted LAN needs nothing beyond the tutorial. Leaving the LAN — a VPS, a reachable home7server, tickets that travel — adds four concerns: the address you pin into tickets, TLS, the relay8your mesh rides, and the binary you actually run.910## Pin the public URL1112```sh13lofi-node init --public-url https://sync.example.net14```1516`publicUrl` is the base embedded in every issued ticket. It must be the address enrolled devices17will reach — not `localhost`, not an internal IP that stops resolving off-network. It can be changed18per-issuance (`ticket issue --url …`), but pinning it at init keeps tickets consistent.1920## Front the gate with TLS2122The gate speaks plain HTTP; beyond a trusted LAN, put TLS in front of it (a reverse proxy such as23Caddy or nginx terminating `https://` and forwarding to the gate's port) and issue tickets with the24`https://` public URL. Two reasons, one hard and one practical: the ticket URL is a bearer25credential and should not transit networks unencrypted; and installed PWAs generally require a26secure origin, so an app enrolled over plain http may work in a tab and fail as an installed app.2728Note what TLS does **not** change: the ticket still gates access, revocation still works the same,29and the WebSocket upgrade still rides the same URL — the browser client derives its endpoints from30the http(s) base.3132## Redact ticket secrets from proxy logs3334Every gated request carries `/t/<secret>` in its path, and a reverse proxy's default access log35writes request paths — so a stock logging config quietly collects bearer credentials on disk. Redact36the path before it is logged (or disable access logging for the gate's host).3738Caddy, with its log field filter:3940```caddyfile41node.example.com {42 reverse_proxy 127.0.0.1:480243 log {44 format filter {45 wrap console46 fields {47 request>uri regexp /t/[A-Za-z0-9_-]{43} /t/<redacted>48 }49 }50 }51}52```5354nginx, by logging a scrubbed copy of the URI:5556```nginx57map $request_uri $redacted_uri {58 "~^(?<pre>.*/t/)[A-Za-z0-9_-]{43}(?<post>.*)$" "${pre}<redacted>${post}";59 default $request_uri;60}61log_format redacted '$remote_addr - [$time_local] '62 '"$request_method $redacted_uri $server_protocol" '63 '$status $body_bytes_sent';64access_log /var/log/nginx/access.log redacted;65```6667The same concern applies to anything else that records URLs on the request path — tracing68middleware, error reporters, CDN logs. The node itself compares digests and logs ticket ids, never69secrets; keep the perimeter to the same standard. If a secret does land in a log, revoke that ticket70(`lofi-node ticket revoke <id>`) — reissuing is one command and live sockets close within seconds.7172## Bring your own relay7374Paired nodes hole-punch direct connections wherever possible; an75[iroh relay](https://docs.iroh.computer/concepts/relays) assists that hole-punching and carries the76traffic that cannot go direct. Out of the box a node uses n0-computer's **public relays**: shared by77every iroh developer, rate-limited, no uptime guarantees. That default exists so the tutorial works78in thirty seconds; n0 blesses it for development and testing, and a production mesh should not lean79on it.8081```sh82lofi-node init --relay https://relay.example.net83```8485The relay is part of what you self-host, and it fits the machine you already have: `iroh-relay` is a86single open-source binary with built-in Let's Encrypt support, cheap to run because it only carries87the non-direct residue of your own mesh's traffic. The publicly reachable machine hosting your root88node is its natural home. Each node elects its **own** relay (`relay` in89[config.json](configuration.md)), and the election travels inside that node's pairing tickets, so90nothing has to be coordinated across the mesh — no lofi-run infrastructure, no shared choke point.91Your mesh's availability depends only on machines you operate.9293`--relay` accepts several URLs (comma-separated or repeated) for failover. `--no-relay` opts out94entirely; defensible for nodes that are all mutually reachable (static IPs, a VPN), but it also95forgoes relay-assisted address discovery, so expect hole-punching to suffer across NATs.9697## One binary9899```sh100deno task compile # → dist/lofi-node101```102103The compiled binary embeds the prebuilt native transport layer and extracts it to a version-keyed OS104cache on first run. Artifact digests are pinned in the source, so an extraction that doesn't match105its pin fails loudly. Release binaries cover macOS arm64 and Linux x86_64.106107**Windows** is a documented gap: the native layer's build path needs a `libnode.dll` import library108the upstream toolchain doesn't ship for this configuration. On Windows the node runs LAN-only — the109Jazz server works, `status().mesh` reports `{ state: "unavailable", reason: … }`, and110`ticket()`/`pair()` throw typed errors rather than degrade silently.111112**Linux aarch64** is a second documented gap: the Jazz engine (jazz-napi) publishes no113linux-arm64-gnu build, so neither a compiled binary nor a native container image can boot on arm64114Linux. The iroh transport layer is built for the platform already; the target returns when the115upstream package appears.116117## Run it in Docker118119The container image wraps the same compiled binary in a minimal glibc base:120`ghcr.io/felinestatemachine/lofi-node`, published per release, linux/amd64 (per the platform note121above; Docker Desktop and qemu run it on arm64 hosts under emulation).122123```sh124docker run --rm -v lofi-node-data:/data ghcr.io/felinestatemachine/lofi-node \125 init --dir /data --port 4802 --public-url https://sync.example.net126docker run -d --name lofi-node --network host \127 -v lofi-node-data:/data ghcr.io/felinestatemachine/lofi-node128```129130Everything durable lives in the `/data` volume: `config.json`, the node identity key,131`tickets.json`, and the SQLite store. CLI verbs run through `docker exec` against the same directory132and take effect without a restart, exactly as outside a container:133134```sh135docker exec lofi-node lofi-node ticket issue --dir /data --label phone136```137138Two container-specific choices matter:139140- **Networking.** Host networking keeps Docker's bridge NAT out of iroh's hole-punching path. A141 bridge network with a published port (`-p 4802:4802`) serves apps through the gate without issue,142 but paired nodes behind it are more likely to fall back to relayed traffic143 ([the relay's role](pair-two-homes.md)). Host networking is a Linux feature; Docker Desktop144 supports it from version 4.34.145- **The health check** probes `http://127.0.0.1:4802/health`. A node initialized with a different146 `--port` needs the check overridden in compose.147148The image runs as uid 1000. Named volumes inherit that ownership; a bind mount must be writable by149uid 1000 (`chown 1000:1000 <dir>`) or the storage probe fails at boot, naming the path. The150repository's [compose.yaml](https://github.com/FelineStateMachine/lofi-node/blob/main/compose.yaml)151is the reference deployment.152153## The version invariant154155A node pins the **exact** Jazz alpha of the apps it serves; version bumps are coordinated changes,156never drive-by. Before pointing the apps you rely on at your node, confirm the pins match — a157mismatched pair fails in protocol-level ways no amount of network configuration fixes. See158[troubleshooting](troubleshooting.md) for what the mismatches look like.