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

ComponentStackDeploymentPurpose
Core nodeGo, libp2p, BadgerDBNative binary (xe-node)Block lattice, consensus, networking, HTTP API, VM management
Web UIPlain HTML + ES modules, embedded in the binaryServed by the node itself with -uiExplorer, wallet, DAO, and chat pages
CaddyNative binaryTLS 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 xe service 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:

HostRegionDomainCore DomainRole
ldnUKldn.test.networkldn.core.test.networkBootstrap (pm2 + Caddy)
nycUSnyc.test.networknyc.core.test.networkBootstrap (pm2 + Caddy)
ffmDEffm.test.networkffm.core.test.networkBootstrap (pm2 + Caddy)
189.1.171.51bare metalProvider only (systemd)
67.213.117.123bare metalProvider 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-node

setcap 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 xe user, not root. This is required because Lima (the VM backend) refuses to run as root. The xe user owns /var/lib/xe-node/ and has limactl in its PATH. The binary has cap_net_bind_service capability set via setcap so it can bind to privileged ports if needed.

See also