Skill: maverick-software/openclaw-vps-deploy (version 1.1.0)

--- name: openclaw-vps-deploy description: > Deploy a custom OpenClaw repo (official or forked) to a Hostinger VPS and make it accessible via the cloud. Use when setting up a new OpenClaw instance on a remote VPS, deploying a custom or forked OpenClaw build to a server, making OpenClaw accessible at a public URL, or setting up OpenClaw as a cloud-hosted service on Hostinger. Handles SSH connection, Node.js install, OpenClaw install (npm package or git clone+build), systemd service, firewall, and auth token generation. --- # OpenClaw VPS Deploy Deploy OpenClaw (official or custom fork) to a Hostinger VPS with full cloud access. ## Quick Deploy ```bash # Official package (most common) python3 scripts/deploy.py \ --ip 187.124.84.25 \ --key ~/.ssh/id_ed25519 \ --name "Koda" # Custom git fork python3 scripts/deploy.py \ --ip 187.124.84.25 \ --key ~/.ssh/id_ed25519 \ --repo https://github.com/your-org/openclaw \ --name "Koda" \ --gw-port 18789 ``` The script auto-reads `ANTHROPIC_API_KEY` from the local OpenClaw vault (`~/.openclaw/secrets.json`) or environment. Pass `--anthropic` to override. ## What the Script Does 1. **Connects** via SSH (key auth — required, password auth often disabled on Hostinger) 2. **Installs Node.js 22** via nodesource apt repo 3. **Installs OpenClaw** — either `npm install -g <package>` or git clone + `pnpm build` 4. **Writes `openclaw.json`** with correct schema (see critical notes below) 5. **Installs systemd service** — auto-starts on reboot, restarts on crash 6. **Opens UFW ports** — SSH (22) + gateway port 7. **Saves auth token** to local vault as `OPENCLAW_VPS_<IP>_TOKEN` ## SSH Key on WSL2 The user's Windows SSH key is accessible at `/mnt/c/Users/<username>/.ssh/id_ed25519` in WSL2. Copy it with correct permissions before use: ```bash cp /mnt/c/Users/charl/.ssh/id_ed25519 /tmp/vps_key chmod 600 /tmp/vps_key python3 scripts/deploy.py --ip 1.2.3.4 --key /tmp/vps_key ``` ## Getting the Server IP From the Hostinger tab in the Control dashboard (VPS Servers section), or via API: ```bash mcporter call hostinger-api.VPS_getVirtualMachinesV1 2>&1 | \ python3 -c "import json,sys; [print(v['id'], v['ipv4'][0]['address'], v['hostname']) for v in json.load(sys.stdin) if v.get('ipv4')]" ``` ## Critical Notes (Read Before Manually Deploying) See `references/gotchas.md` for full details on every failure mode. Key points: 1. **Never use `openclaw gateway start`** as ExecStart — use `openclaw gateway --bind lan --auth token --allow-unconfigured` 2. **Config schema**: use `agents.list[]` not `agents.default` (causes schema error) 3. **Set `gateway.mode`** — required, use `"remote"` for cloud access 4. **Set `XDG_RUNTIME_DIR=/run/user/0`** in systemd + `mkdir -p /run/user/0` 5. **Add public IP to `allowedOrigins`** — otherwise the UI blocks non-localhost connections 6. **Performance**: always set `OPENCLAW_NO_RESPAWN=1` and `NODE_COMPILE_CACHE` ## Correct openclaw.json Schema ```json { "agents": { "defaults": { "model": { "primary": "anthropic/claude-sonnet-4-6" } }, "list": [ { "id": "main", "default": true, "name": "Koda" } ] }, "env": { "ANTHROPIC_API_KEY": "sk-ant-..." }, "gateway": { "port": 18789, "bind": "lan", "mode": "remote", "auth": { "mode": "token", "token": "your-token" }, "controlUi": { "allowedOrigins": [ "http://localhost:18789", "http://127.0.0.1:18789", "http://YOUR.SERVER.IP:18789" ] } } } ``` ## Correct systemd Unit ```ini [Unit] Description=OpenClaw Gateway After=network.target [Service] Type=simple User=root WorkingDirectory=/root ExecStart=/usr/bin/openclaw gateway --bind lan --port 18789 --auth token --allow-unconfigured Restart=always RestartSec=5 Environment=HOME=/root Environment=OPENCLAW_NO_RESPAWN=1 Environment=NODE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache Environment=XDG_RUNTIME_DIR=/run/user/0 [Install] WantedBy=multi-user.target ``` ## Managing a Live Instance ```bash # Status ssh root@SERVER_IP "systemctl status openclaw" # Logs (live) ssh root@SERVER_IP "journalctl -u openclaw -f" # Restart ssh root@SERVER_IP "systemctl restart openclaw" # Update OpenClaw (npm) ssh root@SERVER_IP "npm install -g openclaw && systemctl restart openclaw" # Update custom fork (git) ssh root@SERVER_IP "cd /opt/openclaw && git pull && pnpm install && pnpm build && systemctl restart openclaw" ``` ## Accessing the UI Navigate to `http://SERVER_IP:18789`. When prompted for an auth token, use the value stored in: - Local vault: `OPENCLAW_VPS_<IP_WITH_UNDERSCORES>_TOKEN` - Or retrieve: `python3 -c "import json; v=json.load(open('/home/charl/.openclaw/secrets.json')); [print(k,v[k]) for k in v if 'VPS' in k and 'TOKEN' in k]"` ## Multi-Agent VPS Setup One VPS can run multiple isolated agents on separate ports, each with its own Cloudflare URL. No core OpenClaw changes needed — use `--config-dir` per agent. See `references/multi-agent.md` for: port allocation, per-agent systemd services, provisioning script, resource planning, and Cloudflare tunnel integration. ```bash # Each agent = separate gateway on a separate port openclaw gateway --config-dir /root/agents/koda --port 18789 openclaw gateway --config-dir /root/agents/alex --port 18790 ``` ## Dependencies - `paramiko` Python package (auto-installed by deploy.py if missing) - SSH private key with access to the target VPS - `ANTHROPIC_API_KEY` in vault or environment

path: references/gotchas.md
content: # Known Gotchas — OpenClaw VPS Deployment

Hard-won lessons from real deployments. Each one caused a real failure.

## 1. `openclaw gateway start` ≠ `openclaw gateway`

| Command | What it does |
|---|---|
| `openclaw gateway start` | Starts the **user-level systemd service** — requires D-Bus session |
| `openclaw gateway` | Runs the gateway **directly in foreground** — what you want in a system service |

**Symptom:** `Gateway service check failed: systemctl is-enabled unavailable: $DBUS_SESSION_BUS_ADDRESS not defined`

**Fix:** Use `openclaw gateway --bind lan --port 18789 --auth token --allow-unconfigured` as ExecStart.

---

## 2. Wrong agents config schema

**Wrong (causes "Unrecognized key: default" error):**
```json
{
  "agents": {
    "default": { "name": "Koda" }
  }
}
```

**Correct:**
```json
{
  "agents": {
    "defaults": {
      "model": { "primary": "anthropic/claude-sonnet-4-6" }
    },
    "list": [
      { "id": "main", "default": true, "name": "Koda" }
    ]
  }
}
```

---

## 3. `gateway.mode` must be set

Without it: `Gateway mode is unset; gateway start will be blocked.`

Set `"mode": "remote"` in `gateway` config, or pass `--allow-unconfigured` flag.

---

## 4. `XDG_RUNTIME_DIR` missing in systemd

**Symptom:** Service starts, immediately exits with D-Bus error.

**Fix:** Add to systemd unit and create the directory:
```ini
Environment=XDG_RUNTIME_DIR=/run/user/0
```
```bash
mkdir -p /run/user/0 && chmod 700 /run/user/0
```

---

## 5. `allowedOrigins` blocks the UI

When `bind=lan`, OpenClaw seeds `allowedOrigins` to only `localhost`. Accessing via public IP returns a CORS/origin error.

**Fix:** Add public IP to config:
```json
"gateway": {
  "controlUi": {
    "allowedOrigins": [
      "http://localhost:18789",
      "http://127.0.0.1:18789",
      "http://YOUR.IP.HERE:18789"
    ]
  }
}
```

---

## 6. mcporter config path is CWD-relative

When the gateway calls `mcporter` via `execAsync`, it looks for `./config/mcporter.json` relative to the **current working directory** of the process — not `$HOME`. This means if the gateway's CWD differs from the user's shell CWD, mcporter won't find its servers.

**Fix:** Always pass `--config /absolute/path/to/mcporter.json` when calling mcporter from a subprocess.

---

## 7. paramiko auth on Hostinger VPS

Hostinger VPS instances typically disable password auth after initial setup (or set a specific root password during creation that may differ from what you specified in a failed API purchase attempt).

**Fix:** Always use key-based auth. In WSL2, the Windows SSH key is at `/mnt/c/Users/<username>/.ssh/id_ed25519`.

```python
client.connect(ip, port=port, username=user, key_filename="/tmp/vps_key")
```

Copy the key to `/tmp/vps_key` with `chmod 600` before using.

---

## 8. Performance on small VPS (KVM 1)

KVM 1 = 1 vCPU, 4GB RAM. OpenClaw runs fine but Node.js startup is slower without these:

```ini
Environment=OPENCLAW_NO_RESPAWN=1
Environment=NODE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache
```

Always set both in the systemd unit.

---

## 9. Gateway auth token configuration

`--auth token` sets the **mode** but the token itself must be in config:
```json
"gateway": {
  "auth": {
    "mode": "token",
    "token": "your-token-here"
  }
}
```

Or generate one: `openssl rand -hex 24`

---

## 10. Build from git repo (custom fork)

If deploying a custom fork instead of the npm package:
1. Install `pnpm` first: `npm install -g pnpm`
2. Clone: `git clone --depth 1 <repo> /opt/openclaw`
3. Build: `cd /opt/openclaw && pnpm install --frozen-lockfile && pnpm build`
4. Binary is at `/opt/openclaw/dist/index.js` — symlink to `/usr/local/bin/openclaw`


path: references/multi-agent.md
content: # Multi-Agent VPS Deployment

Run multiple isolated OpenClaw agents on a single VPS, each with its own port, workspace, identity, and public URL.

## Table of Contents
1. [Architecture](#architecture)
2. [Port Allocation](#port-allocation)
3. [Provisioning a New Agent](#provisioning-a-new-agent)
4. [Systemd Services](#systemd-services)
5. [Connecting Cloudflare Tunnels](#connecting-cloudflare-tunnels)
6. [Managing All Agents](#managing-all-agents)

---

## Architecture

Each agent is a fully isolated OpenClaw gateway process:

```
VPS (187.124.84.25)
├── /root/agents/
│   ├── koda/         ← config, workspace, sessions
│   ├── alex/
│   └── jordan/
│
├── openclaw-koda.service     → port 18789
├── openclaw-alex.service     → port 18790
├── openclaw-jordan.service   → port 18791
│
├── cloudflared-koda.service  → https://koda.yourdomain.com
├── cloudflared-alex.service  → https://alex.yourdomain.com
└── cloudflared-jordan.service → https://jordan.yourdomain.com
```

No core OpenClaw modifications required. Multi-gateway is a first-class feature via `--config-dir`.

---

## Port Allocation

Reserve a port range and track it:

| Port  | Agent   | Tunnel URL                    |
|-------|---------|-------------------------------|
| 18789 | koda    | koda.yourdomain.com           |
| 18790 | alex    | alex.yourdomain.com           |
| 18791 | jordan  | jordan.yourdomain.com         |
| 18792 | (next)  | ...                           |

Open all agent ports on UFW at once:
```bash
for port in 18789 18790 18791; do ufw allow $port/tcp; done
```

Or keep ports **closed** to the public and only expose them through Cloudflare Tunnel (recommended — Cloudflare handles the HTTPS, the raw port stays private).

---

## Provisioning a New Agent

Run this on the VPS (replace `alex` and `18790` with your values):

```bash
AGENT_NAME="alex"
AGENT_PORT=18790
AGENT_MODEL="anthropic/claude-sonnet-4-6"
AGENT_TOKEN=$(openssl rand -hex 24)
SERVER_IP=$(curl -s ifconfig.me)

# 1. Create config directory
mkdir -p /root/agents/${AGENT_NAME}/.openclaw/agents/main/sessions
chmod 700 /root/agents/${AGENT_NAME}/.openclaw

# 2. Write openclaw.json
cat > /root/agents/${AGENT_NAME}/.openclaw/openclaw.json << EOF
{
  "agents": {
    "defaults": {
      "model": { "primary": "${AGENT_MODEL}" }
    },
    "list": [
      {
        "id": "main",
        "default": true,
        "name": "${AGENT_NAME^}",
        "model": { "primary": "${AGENT_MODEL}" }
      }
    ]
  },
  "env": {
    "ANTHROPIC_API_KEY": "${ANTHROPIC_API_KEY}"
  },
  "gateway": {
    "port": ${AGENT_PORT},
    "bind": "lan",
    "mode": "remote",
    "auth": {
      "mode": "token",
      "token": "${AGENT_TOKEN}"
    },
    "controlUi": {
      "allowedOrigins": [
        "http://localhost:${AGENT_PORT}",
        "http://127.0.0.1:${AGENT_PORT}",
        "http://${SERVER_IP}:${AGENT_PORT}"
      ]
    }
  }
}
EOF

chmod 600 /root/agents/${AGENT_NAME}/.openclaw/openclaw.json

echo "Agent token for ${AGENT_NAME}: ${AGENT_TOKEN}"
```

---

## Systemd Services

### OpenClaw service per agent

Create `/etc/systemd/system/openclaw-${AGENT_NAME}.service`:

```ini
[Unit]
Description=OpenClaw Agent - alex
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/root/agents/alex
ExecStart=/usr/bin/openclaw gateway \
  --config-dir /root/agents/alex/.openclaw \
  --bind lan \
  --port 18790 \
  --auth token \
  --allow-unconfigured
Restart=always
RestartSec=5
Environment=HOME=/root/agents/alex
Environment=OPENCLAW_NO_RESPAWN=1
Environment=NODE_COMPILE_CACHE=/var/tmp/openclaw-compile-cache-alex
Environment=XDG_RUNTIME_DIR=/run/user/0

[Install]
WantedBy=multi-user.target
```

**Note:** Set `HOME` to the agent dir so OpenClaw resolves workspace paths correctly. Set `NODE_COMPILE_CACHE` to a unique path per agent to avoid cache collisions.

Enable and start:
```bash
systemctl daemon-reload
systemctl enable openclaw-alex
systemctl start openclaw-alex
systemctl status openclaw-alex
```

---

## Connecting Cloudflare Tunnels

See the `cloudflare-agent-tunnel` skill for full setup. Short version:

### Quick tunnel (no account — temporary URL)
```bash
cloudflared tunnel --url http://localhost:18790
# Prints a random https://*.trycloudflare.com URL
# URL resets on restart — use for testing only
```

### Named tunnel (permanent URL, requires Cloudflare account)
```bash
# One-time: login + create tunnel
cloudflared login
cloudflared tunnel create alex

# Route to your domain
cloudflared tunnel route dns alex alex.yourdomain.com

# Config at /etc/cloudflared/alex.yml
# Service at /etc/systemd/system/cloudflared-alex.service
```

Full walkthrough in: `cloudflare-agent-tunnel` skill.

---

## Managing All Agents

```bash
# Status of all openclaw agents
systemctl list-units "openclaw-*" --no-pager

# Status of all tunnels
systemctl list-units "cloudflared-*" --no-pager

# Restart all agents
systemctl restart openclaw-koda openclaw-alex openclaw-jordan

# Logs for a specific agent
journalctl -u openclaw-alex -f

# List all agent ports in use
ss -tlnp | grep openclaw

# Add a new agent (next available port)
NEXT_PORT=$(ss -tlnp | grep -oP ':\K1878\d' | sort -n | tail -1 | xargs -I{} expr {} + 1)
echo "Next port: ${NEXT_PORT:-18789}"
```

---

## Resource Considerations (KVM 1 — 4GB RAM)

Each OpenClaw agent uses ~300-400MB RAM at idle. On a KVM 1 (4GB):

| Agents | Est. RAM | Headroom |
|--------|----------|----------|
| 1      | ~400MB   | ✅ Plenty |
| 2      | ~800MB   | ✅ Good   |
| 4      | ~1.6GB   | ✅ OK     |
| 8      | ~3.2GB   | ⚠️ Tight  |

For 4+ agents, upgrade to KVM 2 (8GB RAM). CPU is rarely the bottleneck — agents spend most time waiting on API responses.


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)