Bootstrap Node Setup

Bootstrap Node Setup

Bootstrap nodes run the full stack: xe-node, Caddy, explorer, wallet, and docs. They serve as the network's public-facing infrastructure with web interfaces and TLS. This guide covers setting up a new bootstrap node — for provider-only nodes (bare metal, no web interface), see Provider Node Setup.

Each node runs Ubuntu 24.04 with Caddy, Node.js, pm2, QEMU, and Lima installed. The setup-node.sh script automates the installation on a fresh host.

#Prerequisites

  • Ubuntu 24.04 LTS (or compatible Debian-based system)
  • Root SSH access
  • Ports 80, 443, and 9000 open in the firewall

#setup-node.sh

The setup script (explorer/deploy/setup-node.sh) installs all required software:

shell
#!/bin/bashset -euo pipefail # Caddy (reverse proxy + TLS)apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curlcurl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \    gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpgcurl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | \    tee /etc/apt/sources.list.d/caddy-stable.listapt-get update && apt-get install -y caddysystemctl disable caddy  # pm2 manages itsystemctl stop caddy # Node.js 22 + pm2curl -fsSL https://deb.nodesource.com/setup_22.x | bash -apt-get install -y nodejsnpm install -g pm2pm2 startup  # auto-start on boot # QEMU + Lima (for compute leasing)apt-get install -y qemu-system-x86 qemu-utilsLIMA_VERSION="1.0.6"curl -fsSL "https://github.com/lima-vm/lima/releases/download/v${LIMA_VERSION}/lima-${LIMA_VERSION}-Linux-x86_64.tar.gz" | \    tar xz -C /usr/local # Firewallufw allow 80/tcpufw allow 443/tcpufw allow 9000/tcp # Service user (non-root, required for Lima)id xe 2>/dev/null || useradd -r -m -d /home/xe -s /bin/bash xeusermod -aG kvm xe  # KVM access for VM provisioning # KVM permissions (pm2 uid/gid drops supplementary groups)echo 'KERNEL=="kvm", MODE="0666"' > /etc/udev/rules.d/99-kvm.ruleschmod 666 /dev/kvm 2>/dev/null || true # Directoriesmkdir -p /opt/xe/{deploy,web/explorer,web/wallet,web/docs}mkdir -p /etc/caddymkdir -p /var/lib/xe-nodechown -R xe:xe /var/lib/xe-node

#Directory layout

After setup, the host has the following structure:

diagram
/opt/xe/├── deploy/│   ├── .env                    # DOMAIN, CORE_DOMAIN, NODE_FLAGS│   └── ecosystem.config.js     # pm2 process config├── web/│   ├── explorer/               # Explorer SPA (rsync'd by CI)│   ├── wallet/                 # Wallet SPA (rsync'd by CI)│   └── docs/                   # MkDocs site (rsync'd by CI)/etc/caddy/└── Caddyfile                   # Caddy routing config/usr/local/bin/└── xe-node                     # Core node binary (scp'd by CI)/var/lib/xe-node/├── badger/                     # BadgerDB database├── host.key                    # libp2p ed25519 identity├── node.key                    # Node account ed25519 key├── ssh_host_key                # SSH gateway host key├── images/                     # VM base images (auto-downloaded)│   └── ubuntu-24.04-x86_64.img  # Ubuntu cloud image (~600 MB)├── lima/                       # Lima VM state (LIMA_HOME)│   └── xe-<hash>/              # Per-VM directory└── lima-templates/             # Lima YAML templates    └── xe-<hash>.yaml/home/xe/                       # Service user home

#Per-node .env

Each node has a .env file at /opt/xe/deploy/.env. This file is created manually per node and is not managed by CI (rsync excludes it).

Standard node (no compute provider):

shell
DOMAIN=ldn.test.networkCORE_DOMAIN=ldn.core.test.networkNODE_FLAGS=-port 9000 -api-port 8080 -api-bind 0.0.0.0 -data /var/lib/xe-node -dial /ip4/<peer1-ip>/tcp/9000/p2p/<peer1-id>,/ip4/<peer2-ip>/tcp/9000/p2p/<peer2-id>

Provider node (requires bare metal or KVM-enabled host):

shell
DOMAIN=bm1.test.networkCORE_DOMAIN=bm1.core.test.networkNODE_FLAGS=-port 9000 -api-port 8080 -api-bind 0.0.0.0 -data /var/lib/xe-node -provide -vcpus 2 -memory 2048 -disk 20 -dial /ip4/<peer1-ip>/tcp/9000/p2p/<peer1-id>,/ip4/<peer2-ip>/tcp/9000/p2p/<peer2-id>
VariableDescription
DOMAINPrimary domain for Caddy TLS (e.g., ldn.test.network)
CORE_DOMAINDirect API domain (e.g., ldn.core.test.network)
NODE_FLAGSAll xe-node CLI flags as a single string

Caddy reads DOMAIN and CORE_DOMAIN via --envfile. The ecosystem.config.js reads NODE_FLAGS and passes it as the xe-node args.

#Initial deployment

After running setup-node.sh:

  1. Create .env at /opt/xe/deploy/.env with the node's domain and peer dial addresses

  2. Copy Caddyfile to /etc/caddy/Caddyfile

  3. Copy ecosystem.config.js to /opt/xe/deploy/

  4. Deploy xe-node binary to /usr/local/bin/xe-node and set capabilities:

    shell
    chmod +x /usr/local/bin/xe-nodesetcap cap_net_bind_service=+ep /usr/local/bin/xe-node
  5. Deploy static sites to /opt/xe/web/explorer/, /opt/xe/web/wallet/, /opt/xe/web/docs/

  6. Start services:

    shell
    cd /opt/xe/deploy && pm2 start ecosystem.config.js && pm2 save

#Updating after deployment

CI handles all updates automatically. On push to master:

  • Core node: CI builds the binary, SCPs it to each host, pm2 stop/start restarts the process. setcap is reapplied after SCP since it's lost when the binary is replaced.
  • Explorer/Wallet/Docs: CI builds static files and rsyncs them. No restart needed — Caddy serves them directly from disk.
  • Config changes: The explorer CI rsyncs deploy/ files (Caddyfile, ecosystem.config.js) and restarts Caddy if the config changed.

#Port summary

PortProtocolServiceFirewall
9000TCPlibp2p (peer-to-peer networking)Open (ufw allow)
8080TCPHTTP API (xe-node, localhost only via Caddy)Not exposed directly
80TCPCaddy (HTTP → HTTPS redirect, ACME challenges)Open (ufw allow)
443TCPCaddy (HTTPS, TLS termination)Open (ufw allow)
2222TCPSSH gateway (optional, if -ssh-port set)Open if used

#Peer identity

The node's peer ID is derived from the ed25519 private key at {dataDir}/host.key. If this file is deleted (e.g., during a data wipe), the node generates a new identity. The -dial flags in NODE_FLAGS must then be updated on all other nodes to reference the new peer ID.

To get a node's current peer ID:

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

#See also