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 xe service 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 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