Deployment

Single-binary deployment with embedded UI, the published packaging (systemd, Docker, pm2, monitoring), bootstrap peers and provider requirements.


A single xe binary serves everything: the block lattice, the HTTP API, the operator listener and the web UI (embedded via //go:embed, enabled with --ui). The repository's deploy/ directory and docs/run-a-node.md are the canonical operator references; this page summarises them and adds what is specific to the public testnet hosts.

Listeners

ListenerDefaultFlagsServes
HTTP API127.0.0.1:8080--api, --api-port, --api-bindREST + SSE API, GET / manifest
Web UI127.0.0.1:8000--ui, --ui-port, --ui-bind, --wallet, --ui-faucetexplorer, wallet, DAO console; reverse-proxies /api/* to the API
Operator127.0.0.1:9095--metrics-addr (empty disables)/metrics (Prometheus), /health, /ready
libp2p0.0.0.0:9000--portgossip, sync, netcheck, tunnel
SSH gatewayoff--ssh-portSSH into leased VMs (provider mode)

Only the libp2p port needs to be reachable from the internet. Everything else binds to loopback by default, and exposing it publicly is an explicit choice that should go through a reverse proxy. IPv6 is not supported — the node listens on /ip4/0.0.0.0/tcp/<port> only.

xe node --ui \
  --genesis-dir /etc/xe/genesis \
  --dial /ip4/45.77.226.208/tcp/9000/p2p/12D3KooW... \
  --api-bind 0.0.0.0 \
  --ssh-port 2222

(Replace 12D3KooW... with a current peer ID — see Bootstrap Peers — and put the published ledger-genesis.json and statechain-genesis.json for the network in the genesis directory.)

[!WARNING] Provider mode requires KVM --provide is deliberately absent from the example above — it is opt-in, and the live bootstrap nodes do not run it. It enables QEMU/Lima VM provisioning, which hard-requires KVM (/dev/kvm) on the host, and a provider node auto-accepts matching leases as soon as the flag is set. Only add --provide on bare metal or a VPS with nested virtualization enabled, and set the provider policy flags to bound what it will accept.

Packaging

ArtifactPathNotes
systemd unitdeploy/xe-node.service (+ deploy/systemd/)Hardened: ProtectSystem=strict, ReadWritePaths=/var/lib/xe; reads /etc/xe/xe-node.env (deploy/xe-node.env.example lists every knob). Providers add a kvm.conf drop-in with SupplementaryGroups=kvm
Dockerdeploy/DockerfileStatic, non-root, distroless image; mount the genesis at /genesis and pass --genesis-dir /genesis
pm2deploy/pm2/ecosystem.config.jsWhat the public bootstrap hosts use, behind Caddy
Monitoringdeploy/monitoring/Prometheus scrape config, alert rules, Alertmanager, Grafana dashboard, compose file — all against the operator listener

Builds are reproducible (make build, make dist, make verify-repro; pinned toolchain in .go-version, -trimpath, -buildvcs=false, CGO_ENABLED=0). No release has been tagged yet, so docs/run-a-node.md's download-and-verify recipe (SHA256SUMS, Sigstore, gh attestation verify) describes the intended flow; build from source today.

Public Hosts (Caddy + pm2)

On the three bootstrap hosts, Caddy terminates TLS and proxies to the node's two listeners; both processes are managed by pm2.

        ┌─────────────┐
        │   Caddy     │ :80/:443 (TLS termination)
        └──────┬──────┘
       ┌───────┴────────┐
       ▼                ▼
 xe-node UI server  xe-node HTTP API
  127.0.0.1:8000     127.0.0.1:8080
   (--ui, --ui-port)  (--api-port)
       │                ▲
       └─ /api/* proxy ─┘

*.core.test.network is the API listener; ldn.test.network is the UI listener (wallet and explorer). The explorer and wallet are not separate applications and there are no static UI assets to deploy. Because external clients behind Caddy share an egress bucket in the node's per-IP rate limiter, a plain sequential sweep of the public URLs can trip 429 — back off rather than retry hot.

Data Directory

PathContent
ledger/BadgerDB database
host.keylibp2p identity (peer ID)
node.keyNode account key
ssh_host_keySSH gateway host key
lima/Lima VM state
lima-templates/Lima YAML templates
images/VM base images

The database lives in ledger/ — wiping any other directory does not clear the chain state. A data directory is bound to one network: after a testnet wipe, start with a fresh --data directory alongside the new genesis bundle, or the node stops with genesis mismatch: this data dir belongs to a different network.

Bootstrap Peers

NodeLocationIPAPI
ldnLondon45.77.226.208https://ldn.core.test.network
ffmFrankfurt192.248.176.245https://ffm.core.test.network
nycNew York144.202.4.117https://nyc.core.test.network

Dial multiaddrs take the form /ip4/<ip>/tcp/9000/p2p/<peer-id>; the current set is listed in Getting Started. Peer IDs are derived from each node's host.key and change if that key is regenerated, so confirm them against the API:

curl -s https://ldn.core.test.network/node | jq -r .id
curl -s https://ffm.core.test.network/node | jq -r .id
curl -s https://nyc.core.test.network/node | jq -r .id

Providers

Providers participate over libp2p only; they have no public DNS or HTTP API of their own, and they advertise themselves on the xe/marketplace gossip topic. Query the live list rather than trusting any static table:

curl -s https://ldn.core.test.network/providers | jq

Two providers are registered at the time of writing (4 vCPU / 8 GB / 50 GB each, up to 5 concurrent leases). Requirements for running one: Ubuntu 24.04 or similar, /dev/kvm, QEMU and Lima (--limactl-path if not on PATH), a reachable libp2p port, and a valid performance certificate — the node benchmarks itself on startup and GET /node → certificate.valid must be true before it can be leased from. Advertise 60–70% of physical resources.

Health, Readiness and Metrics

/health (liveness: process up, sampler running, store readable) and /ready (synced, peered, delegated weight present, quorum reachable, finality advancing) are served on both the API and the operator listener; /metrics only on the operator listener. Wire supervisors to /health only — during a network-wide quorum incident every node reports not-ready, and restarting them all makes it worse. Load balancers can drain on /ready. xe_delegated_weight_micro_xe at zero and xe_finality_advances_total flat are the two alerts that matter.

Release Process

Development happens in a private repository; the public tree is its published release, cut per network (release: testnet-0005 …). Deploys to the bootstrap hosts are gated on the binary's genesis matching the live network's network_id, and protocol changes ship as a testnet wipe: new genesis bundle under genesis/<network-id>/, fresh data directories, new network ID. The documentation sites are separate Next.js applications with their own deploy workflows.