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(orpodman) 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
managementscope 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.
xeroctl agents join-keys --create \
--name xem-fleet-pool \
--region us-east \
--router-addr tcp://router.example.com:5555 \
--ttl-seconds 900
Transcript:
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.
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.
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.
# 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
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:
[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:
xeroctl agents candidates list <agent-id>
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:
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
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_pillstate: the registration name is claimed by another CURVE fingerprint. Runxeroctl agents revoke-credentials <agent-id>to force a clean re-enrollment.queue_write_failed: the/data/xerotier-xem/statevolume is not persistent, or the disk is full.