Deployment
Run a node, bootstrap, or provider
The XE network runs three deployable components managed by pm2 on each host, with Caddy as the reverse proxy and TLS terminator. Static web assets are served directly from disk.
Components
Component
Stack
Deployment
Purpose
Core node
Go, libp2p, BadgerDB
Native binary via pm2
Block lattice, consensus, networking, HTTP API, VM management
Explorer
SvelteKit 2, Svelte 5
Static files on disk
Block explorer and network dashboard
Web wallet
SvelteKit 2, Svelte 5
Static files on disk
Browser-based wallet
Docs
MkDocs Material
Static files on disk
Technical documentation
Architecture
┌─────────────┐
│ Caddy │ :80/:443
│ (pm2) │
└──────┬──────┘
│
┌────────────┼────────────┬────────────┐
│ │ │ │
▼ ▼ ▼ ▼
/api/* /wallet/* /docs/* /*
┌─────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ xe-node │ │ Static │ │ Static │ │ Static │
│ (pm2) │ │ files │ │ files │ │ files │
│ :8080 │ │ /opt/xe/ │ │ /opt/xe/ │ │ /opt/xe/ │
└─────────┘ │ web/ │ │ web/ │ │ web/ │
│ wallet/ │ │ docs/ │ │ explorer/│
└──────────┘ └──────────┘ └──────────┘pm2 manages two processes:
- xe-node — the core Go binary, running as the
xeservice user (non-root, required for Lima VM support) - caddy — reverse proxy and static file server, running as root for port 80/443 binding
Static web assets are built by CI and rsynced to /opt/xe/web/ on each host:
Path
Content
/opt/xe/web/explorer/
Explorer SPA build
/opt/xe/web/wallet/
Wallet SPA build
/opt/xe/web/docs/
MkDocs static build
Caddy routing
Caddy handles TLS termination (automatic Let's Encrypt certificates) and routes requests:
Path
Target
Behaviour
/api/*
localhost:8080
Reverse proxy to xe-node HTTP API (path prefix stripped)
/wallet/*
/opt/xe/web/wallet/
Static file serving with SPA fallback
/docs/*
/opt/xe/web/docs/
Static file serving
/*
/opt/xe/web/explorer/
Explorer SPA with fallback to index.html
Each node also has a CORE_DOMAIN (e.g., ldn.core.test.network) that proxies directly to the node API without path stripping — used for direct API access.
{$DOMAIN} {
redir /wallet /wallet/ 301
redir /docs /docs/ 301
handle_path /api/* {
reverse_proxy localhost:8080
}
handle_path /wallet/* {
root * /opt/xe/web/wallet
try_files {path} /index.html
file_server
}
handle_path /docs/* {
root * /opt/xe/web/docs
file_server
}
handle {
root * /opt/xe/web/explorer
try_files {path} /index.html
file_server
}
}
{$CORE_DOMAIN} {
reverse_proxy localhost:8080
}Deployment targets
The test network runs across four nodes:
Host
Region
Domain
Core Domain
Role
London
UK
ldn.test.network
ldn.core.test.network
Full stack
New York
US
nyc.test.network
nyc.core.test.network
Full stack
Frankfurt
DE
ffm.test.network
ffm.core.test.network
Full stack
bm1
DE (bare metal)
—
bm1.core.test.network
Provider only
The first three hosts run the full stack (xe-node + Caddy + static sites). bm1 is a bare-metal server running only xe-node in provider mode — no Caddy, no static sites. It provides compute resources (4 vCPUs, 8 GB RAM, 50 GB disk) for VM leasing via Lima/QEMU with KVM acceleration.
Nodes bootstrap to each other using -dial flags with the other nodes' multiaddrs.
CI/CD
Deployment is automated via GitHub Actions across four repositories:
Push to master
│
▼
Build (per repo)
├── core: go build → xe-node binary
├── explorer: npm build → static files
├── wallet: npm build → static files
└── docs: mkdocs build → static files
│
▼
Deploy (parallel to 3 hosts)
├── core: scp binary → pm2 restart xe-node
├── explorer: rsync build/ → /opt/xe/web/explorer/
├── wallet: rsync build/ → /opt/xe/web/wallet/
└── docs: rsync site/ → /opt/xe/web/docs/The core CI also syncs Caddy config and ecosystem files to each host, then restarts Caddy if config changed.
Process management
pm2 configuration lives at /opt/xe/deploy/ecosystem.config.js. It reads the .env file directly to get NODE_FLAGS:
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 -- full-stack nodes with web interfaces (Caddy + pm2)
- Provider Node Setup -- bare-metal provider nodes (systemd, no web interface)
- Node CLI -- node startup flags