Deployment
Run a node, bootstrap, or provider
[!WARNING] Testnet infrastructure The hostnames, IP addresses, and topology on the pages in this section describe the XE test network. They are the concrete deployment that exists today, not a mainnet specification.
Everything a node serves ships in one Go binary: the block lattice, the HTTP API, and the web UI. Caddy sits in front of it as reverse proxy and TLS terminator, managed alongside the node by pm2 on the bootstrap hosts and by systemd on the provider hosts.
Components
| Component | Stack | Deployment | Purpose |
|---|---|---|---|
| Core node | Go, libp2p, BadgerDB | Native binary (xe-node) | Block lattice, consensus, networking, HTTP API, VM management |
| Web UI | Plain HTML + ES modules, embedded in the binary | Served by the node itself with -ui | Explorer, wallet, DAO, and chat pages |
| Caddy | — | Native binary | TLS termination and reverse proxy |
The explorer and wallet are not separate applications any more. They live in core/web, are //go:embed'd into the binary, and have no build step — the directory is both source and artefact. xe node -ui starts a second HTTP listener (default 127.0.0.1:8000) that serves those files and reverse-proxies its own /api/* to the node's API listener (default 127.0.0.1:8080). Use -ui-dir to serve the UI from disk during development, and -wallet=false to serve 404 for /wallet/*.
Architecture
┌─────────────┐
│ 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, -api-port)
│ ▲
└── /api/* reverse proxy ─┘Two long-running processes per bootstrap host:
- xe-node — the core Go binary, running as the
xeservice user (non-root, required for Lima VM support) - caddy — reverse proxy and TLS terminator, running as root for port 80/443 binding
Each node has a DOMAIN (e.g. ldn.test.network) and a CORE_DOMAIN (e.g. ldn.core.test.network) used for direct API access. Both are read by Caddy from /opt/xe/deploy/.env.
Deployment targets
The test network runs across five hosts — three bootstrap nodes and two providers:
| Host | Region | Domain | Core Domain | Role |
|---|---|---|---|---|
| ldn | UK | ldn.test.network | ldn.core.test.network | Bootstrap (pm2 + Caddy) |
| nyc | US | nyc.test.network | nyc.core.test.network | Bootstrap (pm2 + Caddy) |
| ffm | DE | ffm.test.network | ffm.core.test.network | Bootstrap (pm2 + Caddy) |
| 189.1.171.51 | bare metal | — | — | Provider only (systemd) |
| 67.213.117.123 | bare metal | — | — | Provider only (systemd) |
The three bootstrap hosts run the node plus Caddy. The two provider hosts run only xe-node in provider mode under systemd — no Caddy, no TLS. They supply compute for VM leasing via Lima/QEMU with KVM acceleration.
Nodes bootstrap to each other using -dial flags with the other nodes' multiaddrs.
CI/CD
The core repo's GitHub Actions workflow builds and deploys the node binary. Deploys are gated on workflow_dispatch — pushing to master runs the test suite but never deploys; a deploy has to be triggered manually with the "Run workflow" button.
workflow_dispatch (ref, expected live network_id)
│
▼
test ──► preflight-network-id
(abort unless the binary's embedded genesis
and the live nodes both match the expected id)
│
├─► deploy-bootstrap (ldn, nyc, ffm)
│ go build → scp /usr/local/bin/xe-node
│ → chmod +x, setcap cap_net_bind_service, pm2 start
│
└─► deploy-providers (189.1.171.51, 67.213.117.123)
go build → scp → sudo mv /usr/local/bin/xe-node
→ chmod +x, systemctl restart xe-nodesetcap is reapplied on every bootstrap deploy because capabilities are lost when the binary is replaced.
[!IMPORTANT] The network-id preflight is a safety gate The genesis is embedded in the binary, so deploying a binary built against a different genesis would silently reset the network identity. The preflight compares the embedded id, the live nodes' id, and the id you type into the workflow input, and aborts unless all three agree.
Process management
pm2 configuration lives at /opt/xe/deploy/ecosystem.config.js. It reads the .env file directly to get NODE_FLAGS, which must begin with the node subcommand — the binary dispatches on its first argument and exits with unknown command if given a flag instead:
const fs = require('fs');
const env = loadEnv('/opt/xe/deploy/.env');
module.exports = {
apps: [
{
name: 'xe-node',
script: '/usr/local/bin/xe-node',
interpreter: 'none',
args: env.NODE_FLAGS || '',
uid: 'xe',
gid: 'xe',
env: { HOME: '/home/xe' },
restart_delay: 5000,
max_restarts: 10,
},
{
name: 'caddy',
script: '/usr/bin/caddy',
interpreter: 'none',
args: 'run --config /etc/caddy/Caddyfile --envfile /opt/xe/deploy/.env',
restart_delay: 5000,
max_restarts: 10,
}
]
};[!IMPORTANT] Non-root execution The xe-node process runs as the
xeuser, not root. This is required because Lima (the VM backend) refuses to run as root. Thexeuser owns/var/lib/xe-node/and haslimactlin its PATH. The binary hascap_net_bind_servicecapability set viasetcapso it can bind to privileged ports if needed.
See also
- Configuration -- all flags, environment variables, and data directory layout
- Bootstrap Node Setup -- public-facing nodes behind Caddy (pm2)
- Provider Node Setup -- bare-metal provider nodes (systemd, no Caddy)
- Node CLI -- the
xecommand surface