Bootstrap Node Setup
Bootstrap Node Setup
Bootstrap nodes are the network's public-facing infrastructure: xe-node (which serves both the HTTP API and the embedded web UI) behind Caddy for TLS. This guide covers setting up a new bootstrap node — for provider-only nodes (bare metal, no reverse proxy), see Provider Node Setup.
Each node runs Ubuntu 24.04 with Caddy, Node.js, pm2, QEMU, and Lima installed. A one-off setup 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
Host setup script
The setup script installs all required software:
#!/bin/bash
set -euo pipefail
# Caddy (reverse proxy + TLS)
apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \
gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | \
tee /etc/apt/sources.list.d/caddy-stable.list
apt-get update && apt-get install -y caddy
systemctl disable caddy # pm2 manages it
systemctl stop caddy
# Node.js 22 + pm2
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt-get install -y nodejs
npm install -g pm2
pm2 startup # auto-start on boot
# QEMU + Lima (for compute leasing)
apt-get install -y qemu-system-x86 qemu-utils
LIMA_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
# Firewall
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 9000/tcp
# Service user (non-root, required for Lima)
id xe 2>/dev/null || useradd -r -m -d /home/xe -s /bin/bash xe
usermod -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.rules
chmod 666 /dev/kvm 2>/dev/null || true
# Directories
mkdir -p /opt/xe/deploy
mkdir -p /etc/caddy
mkdir -p /var/lib/xe-node
chown -R xe:xe /var/lib/xe-nodeDirectory layout
After setup, the host has the following structure:
/opt/xe/
└── deploy/
├── .env # DOMAIN, CORE_DOMAIN, NODE_FLAGS
└── ecosystem.config.js # pm2 process config
/etc/caddy/
└── Caddyfile # Caddy routing config
/usr/local/bin/
└── xe-node # Core node binary (scp'd by CI)
/var/lib/xe-node/
├── ledger/ # 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[!NOTE] No static web assets to deploy The explorer and wallet are compiled into
xe-node(core/web) and served by the node itself when started with-ui. There is nothing to rsync into/opt/xe/web/— that layout belonged to the earlier separate-SPA deployment.
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 — the deploy workflow only replaces the binary.
NODE_FLAGS is passed verbatim as the binary's arguments, so it must start with the node subcommand — xe-node dispatches on its first argument and exits with unknown command if it sees a flag there.
Standard node (no compute provider):
DOMAIN=ldn.test.network
CORE_DOMAIN=ldn.core.test.network
NODE_FLAGS=node -port 9000 -api-port 8080 -api-bind 0.0.0.0 -ui -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):
DOMAIN=bm1.test.network
CORE_DOMAIN=bm1.core.test.network
NODE_FLAGS=node -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>[!WARNING] Provider mode requires KVM The
-provideflag enables Lima VM provisioning, which requires/dev/kvmon the host. Standard cloud VPS instances do not expose hardware virtualization extensions. Only add-provideon bare metal servers or VPS with nested virtualization enabled. Without KVM, lease acceptance will succeed on-chain but VM provisioning will fail.
| Variable | Description |
|---|---|
DOMAIN | Primary domain for Caddy TLS (e.g. ldn.test.network) |
CORE_DOMAIN | Direct API domain (e.g. ldn.core.test.network) |
NODE_FLAGS | The node subcommand plus all node 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 the host setup script:
-
Create .env at
/opt/xe/deploy/.envwith the node's domain and peer dial addresses -
Copy Caddyfile to
/etc/caddy/Caddyfile -
Copy ecosystem.config.js to
/opt/xe/deploy/ -
Deploy xe-node binary to
/usr/local/bin/xe-nodeand set capabilities:chmod +x /usr/local/bin/xe-node setcap cap_net_bind_service=+ep /usr/local/bin/xe-node -
Start services:
cd /opt/xe/deploy && pm2 start ecosystem.config.js && pm2 save
Updating after deployment
Updates are a binary roll. The core repo's deploy workflow is triggered manually (workflow_dispatch), builds the binary, SCPs it to each host, and restarts the process. setcap is reapplied after every SCP since capabilities are lost when the binary is replaced.
Because the explorer and wallet are embedded in the binary, a UI change ships with the same roll — there are no static assets to sync separately.
Port summary
| Port | Protocol | Service | Firewall |
|---|---|---|---|
| 9000 | TCP | libp2p (peer-to-peer networking) | Open (ufw allow) |
| 8080 | TCP | HTTP API (-api-port, localhost only via Caddy) | Not exposed directly |
| 8000 | TCP | Embedded web UI (-ui-port, localhost only via Caddy) | Not exposed directly |
| 80 | TCP | Caddy (HTTP → HTTPS redirect, ACME challenges) | Open (ufw allow) |
| 443 | TCP | Caddy (HTTPS, TLS termination) | Open (ufw allow) |
| 2222 | TCP | SSH gateway (optional, if -ssh-port set) | Open if used |
[!WARNING] Firewall Unlike Docker, which bypasses UFW via iptables DOCKER chains, native services require explicit firewall rules. The setup script adds
ufw allowrules for ports 80, 443, and 9000. If the SSH gateway is enabled, its port must also be opened.
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:
curl -s https://ldn.core.test.network/node | jq -r '.id'See also
- Deployment Overview -- architecture and CI/CD pipeline
- Configuration -- all node flags and environment variables
- Provider Node Setup -- bare-metal provider setup (no web interface)