This Terraform module manages the persistent Hetzner Cloud VM(s) that host the elizaOS Cloud control-plane:
eliza-provisioning-worker— pulls jobs from thejobstable and SSHs into sandbox coreseliza-agent-router— subdomain HTTP routingcloudflared— secure tunnel forsandboxes.waifu.funheadscale— VPN mesh for cross-core agent traffic
The data plane (the sandbox cores themselves) is not managed here —
those are provisioned and drained at runtime by
node-autoscaler.ts
which talks to the Hetzner Cloud API directly. See
../ARCHITECTURE.md for the full split.
- Hetzner Cloud project with API token (
HCLOUD_TOKEN). - Cloudflare account with API token + DNS edit on
elizacloud.ai(CLOUDFLARE_API_TOKEN). - Cloudflare R2 bucket
eliza-terraform-statefor remote state. Generate an R2 API token, editbackend-staging.hcl/backend-production.hclwith your CF account ID, then export the R2 token asAWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEYbeforeterraform init. - Terraform >= 1.5.0 locally.
cd packages/cloud-infra/cloud/terraform/hetzner/control-plane
# 1. Pull providers + connect remote state.
terraform init -backend-config=backend-staging.hcl
# 2. Copy + fill tfvars.
cp tfvars/staging.tfvars.example tfvars/staging.tfvars
$EDITOR tfvars/staging.tfvars
# 3. Plan + apply.
export HCLOUD_TOKEN=...
export CLOUDFLARE_API_TOKEN=...
terraform plan -var-file=tfvars/staging.tfvars
terraform apply -var-file=tfvars/staging.tfvars
# 4. Output gives you the VM IP. Copy the cloud env file into place:
scp packages/cloud-shared/.env.local root@<vm-ip>:/opt/eliza/cloud/.env.local
# 5. Trigger first deploy from GitHub Actions
# (workflow: deploy-eliza-provisioning-worker.yml, manual dispatch).The current prod manager VM (89.167.63.246, hostname milady) was
created by-hand via packages/scripts/cloud/admin/bootstrap-provisioning-worker-host.mjs
on May 7th. To bring it under Terraform without recreating it:
# 1. Look up the Hetzner Cloud server ID:
hcloud server list # find the one with IP 89.167.63.246
# 2. Import the existing resource into state:
terraform init -backend-config=backend-production.hcl
terraform import \
-var-file=tfvars/production.tfvars \
'hcloud_server.control_plane["1"]' \
<SERVER_ID>
# 3. Run `terraform plan` and adjust variables until the diff is empty.
# Common drift: labels, ssh_keys (manually added during initial setup).- Headscale state (preauth keys, ACLs) — manual via
headscaleCLI. - Cloudflared tunnels — config lives at
/root/.cloudflared/on the VM and is created viacloudflared tunnel createone-shot. - The systemd units — installed by
deploy-eliza-provisioning-worker.ymlon every push. - The actual eliza Cloud sandbox cores (data plane) — runtime autoscale.
These are tracked as TODOs in
../ARCHITECTURE.md.
| Component | Resource | Monthly (€) |
|---|---|---|
| 1× cpx21 (3 vCPU / 4 GB) | control VM | ~5 |
| 1× IPv4 + IPv6 | floating IP | included |
| Cloudflare R2 state | < 100 KB | 0 |
| Total per environment | ~5 |
A 2nd control-plane VM (HA, currently unused) doubles the line. The data-plane autoscale cost is separate and elastic.