// XEM Guides

Deploy Your First XEM

A 30-minute walkthrough from a blank Linux host to a XEM agent enrolled with your router and serving at least one operational workspace. No GPU. Published container image, one compose file from the public repo, one join key. Stops at the first successful tool call.

Prerequisites

  • A Linux host with docker (or podman) and Compose v2. Minimum 1 CPU, 512 MB RAM. No GPU.
  • Outbound HTTPS to the router URL (for enrollment) and outbound TCP to the router's CurveZMQ port (for the data plane).
  • An API key with the management scope on your laptop, for minting the join key.
  • One of: a Kubernetes cluster, OpenStack, Docker daemon, or another target infrastructure the XEM should manage.

Step 1: Create a Join Key

From your workstation, mint a join key against the router that the XEM should enroll with. The --router-addr value is the CurveZMQ address the agent will dial after enrollment.

bash
xeroctl agents join-keys --create \ --name xem-fleet-pool \ --region us-east \ --router-addr tcp://router.example.com:5555 \ --ttl-seconds 900

Transcript:

text
Join key 'jk_01HX...' created successfully. Join Token: eyJhbGciOi... Save this token, it will not be shown again. ID: jk_01HX... Name: xem-fleet-pool Region: us-east Max Enrollments: 1 Expires: 2026-04-22T03:30:00Z

Copy the JWT. The token is TTL-bounded by --ttl-seconds and capped to --max-enrollments (defaults to 1). Once consumed it cannot be reused. The agent takes its registration name on first enrollment from XEROTIER_AGENT_REGISTRATION_NAME (set in compose/.env in Step 3).

The minting account's API key must carry the management scope. Mint one from the API Keys page if you do not already have one.

Step 2: Prepare the Host

Clone the cloudnull/xerotier-public repository on the XEM host (or copy just the compose/ directory) so the compose file referenced below is available locally.

bash
git clone https://github.com/cloudnull/xerotier-public.git cd xerotier-public/compose

Create the host directories the agent mounts for tool bundles, workspace credentials, runtime state, and its persistent signing key. The container runs as UID:GID 5153:5153.

bash
sudo mkdir -p /data/xerotier-xem/tools /data/xerotier-xem/credentials \ /data/xerotier-xem/state /data/xerotier-xem/config sudo chown -R 5153:5153 /data/xerotier-xem

The XEM agent ships as a published container image; there is no GPU or vLLM runtime to install. Deployment is the single compose.agent-xem.yaml stack from the repository, started in Step 4 once the environment is set.

Step 3: Configure the Environment

Create a .env file in the compose/ directory. Docker Compose reads it automatically for variable substitution. The agent refuses to start without the three required values.

compose/.env
# Required XEROTIER_AGENT_JOIN_KEY=eyJhbGciOi... XEROTIER_ROUTER_URL=https://router.example.com XEROTIER_AGENT_REGISTRATION_NAME=xem-fleet-01 # Image (pulls the published container image) XEM_IMAGE=ghcr.io/cloudnull/xerotier-public/xem:latest # Optional XEM_AGENT_LOG_LEVEL=info XEM_AGENT_MAX_CONCURRENT=20

The registration name must be unique per project. The agent generates a persistent Ed25519 signing key on first run and writes it under the mounted config volume so CURVE rotation acks survive restarts; the router address and CURVE public key arrive in the enrollment response. The lease duration is owned by the router server-side.

Treat this file as a secret: the join token is a bearer credential. Set chmod 600 .env; never commit it to a repository and never copy it onto an operator workstation.

Step 4: Start the Agent

bash
docker compose -f compose.agent-xem.yaml up -d docker compose -f compose.agent-xem.yaml logs -f xem-agent

Expected log lines on success:

text
[info] consuming join key from env XEM_JOIN_KEY [info] enrollment succeeded, agent_id=agt_01HX..., curve fingerprint=a1b2... [info] starting discovery probes [info] candidate workspace: kubernetes/prod-us-east-1 [info] candidate workspace: docker/localhost [info] dispatch queue open (WAL mode), 0 frames queued [info] lease established, heartbeat every 10s

The join key is consumed; the CURVE keys are written to the mounted config volume (/data/xerotier-xem/config). Do not re-use the join key even if the enrollment seems to have failed.

Step 5: Approve Discovered Workspaces

The XEM's boot-time discovery probes report candidate workspaces back to the router. Each candidate must be explicitly approved before it becomes a real workspace and starts accepting tool invocations.

List the agent's pending candidates from your workstation:

bash
xeroctl agents candidates list <agent-id>
text
ID Status Agent Created cand_01HX... pending agt_01HX... 2026-04-22T03:30:00Z cand_01HY... pending agt_01HX... 2026-04-22T03:30:00Z

Use xeroctl agents (no arguments) to look up the agent-id assigned at enrollment. Approve each candidate you want the XEM to serve:

bash
xeroctl agents candidates approve <agent-id> <candidate-id>

Approval promotes the candidate into a workspace bound to the agent. List active workspaces with xeroctl workspaces; the id column gives the ws_xxx handle used in Step 6.

Step 6: Run a Tool Call

bash
xeroctl exec invoke \ --workspace ws_01HX... \ --tool kubectl_get \ --args '{"resource": "pods", "namespace": "default"}'

If the tool is read-only and the approval policy allows, the invocation runs immediately and returns the pod list. If approval is required, the CLI blocks with an invocation ID you can approve via xeroctl approvals approve.

Congratulations, your first XEM is serving an operational workspace. Next, author a chat template to bake in a repeatable procedure: Author a Chat Template.

Troubleshooting

See XEM Troubleshooting for the full error-code matrix. Common first-deploy issues:

  • enrollment_rejected: the join key expired or the registration name collides with another agent. Mint a new join key and try again.
  • conflict_pill state: the registration name is claimed by another CURVE fingerprint. Run xeroctl agents revoke-credentials <agent-id> to force a clean re-enrollment.
  • queue_write_failed: the /data/xerotier-xem/state volume is not persistent, or the disk is full.