# How to deploy Hermes Agent on Ubuntu

## Phase 1: Server Initialization & Base Setup

1. Purchase a VPS, deploy Ubuntu 24.04 LTS
2. Create system user `tianhe`, add to sudo group
3. Go to [https://hermes-agent.nousresearch.com/](https://hermes-agent.nousresearch.com/), run the install script to complete setup

## Phase 2: Data Migration (from old Railway deployment)

1. Download backup from Cloudflare R2 via AWS CLI
2. Restore `.hermes` directory from backup tarball
3. Rebuild corrupted `state.db` — reimport from sessions/*.json files
4. Honcho was originally on Railway; later migrated to local Docker Compose(PostgreSQL + pgvector + Redis)

## Phase 3: R2 Backup Script Adaptation

1. Update `backup-to-r2.py` paths from old user home to `/home/tianhe`
2. Set up cron: daily at 03:00 UTC, keep 14 most recent backups
3. Optimize backup: exclude venv/node_modules (119 MB → 27.5 MB)

## Phase 4: Security Hardening

1. Bind Dashboard to `0.0.0.0` with `--insecure` flag (due to DNS rebinding protection interfering with reverse proxy)
2. Harden SSH: `PermitRootLogin prohibit-password`, force publickey auth, disable password auth, disable X11 forwarding, limit MaxAuthTries to 3
3. Enable UFW firewall: allow only 22/tcp (SSH) and 443/tcp (HTTPS)
4. Configure fail2ban: 3 failures → 24-hour ban((sshd jail only))
5. Enable unattended-upgrades for automatic security patches

## Phase 5: Dashboard + Caddy Reverse Proxy

1. Build Web UI: npm install → npm run build
2. Create `systemd` service for hermes-dashboard on `127.0.0.1:9119`
3. Configure Caddy reverse proxy: `herma.tianheg.co:443` → localhost:9119
4. Host Header issue: Dashboard's built-in DNS rebinding protection rejects the forwarded hostname. Bind `0.0.0.0` with `--insecure` flag
5. Integrate Cloudflare: SSL mode `Full`, Cloudflare Access auth layer

## Phase 6: Gateway / Telegram Integration

1. Clear stale `gateway_state.json` (draining state from old environment)
2. Install Gateway as user-level systemd service; fix service file path (was written to wrong location by `hermes gateway install`)
3. Configure Telegram Bot Token in `.env`
4. Enable linger (`loginctl enable-linger`) to keep user services alive after SSH logout
5. Verify: send /start to the bot, confirm auto-response and zero errors

## Phase 7: Cleanup

1. Remove Railway leftovers: RAILWAY_TOKEN env var, railway CLI binary(11 MB)
2. Create / update DevOps skills (security audit, Dashboard, Caddy proxy, Gateway service, data maintenance, R2 ops — 6 skills total)

## Phase 8: Hermes Desktop Remote Access

[Hermes Desktop](https://hermes-agent.nousresearch.com/desktop) is an Electron GUI for Hermes Agent. Shares the same =~*.hermes*= config, sessions, skills, and memory as the CLI. Runs on macOS, Windows, Linux.

Desktop defaults to local mode (starts a local `hermes dashboard` backend). Remote mode turns Desktop into a thin client — agent runs on VPS, Desktop renders UI via WebSocket.

**Server-side Setup**

The dashboard must bind to `0.0.0.0` (not `127.0.0.1`) so Tailscale IPs can reach the WebSocket endpoint. `--insecure` disables Host header validation which rejects non-loopback connections.

1. Restart dashboard on 0.0.0.0:
```bash
hermes dashboard --host 0.0.0.0 --port 9120 --no-open --insecure
```

2. Get session token (used by Desktop for auth):
```bash
# from terminal (run on VPS):
curl -s http://127.0.0.1:9120/ | grep -oP '__HERMES_SESSION_TOKEN__="\\K[^"]+'

# or open http://127.0.0.1:9120/ in browser, open console, type:
__HERMES_SESSION_TOKEN__
```

3. Pin the token (optional — avoids token change after restart):
```bash
# Add to ~/.hermes/.env:
HERMES_DASHBOARD_SESSION_TOKEN=<token-from-step-2>
```

Fixed token lets Desktop use a stable `HERMES_DESKTOP_REMOTE_TOKEN` that survives dashboard restarts.

**Network: Tailscale**

SSH tunnel (`ssh -L 9120:localhost:9120`) works but breaks on disconnect. Tailscale is more stable — zero port exposure, auto-reconnection.

Install native Tailscale on VPS:
```bash
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
```

Desktop connects via `http://<tailscale-ip>:9120`. No basic auth needed — port is only reachable inside tailnet.

**tailscale serve trap**: If you previously ran `tailscale serve` on this port, the rule persists in state and intercepts traffic silently, returning 404. Fix:
```bash
sudo tailscale serve --http=9120 off
```

**Clash Verge conflict**: If you use Clash Verge TUN mode on the same machine, it may hijack Tailscale traffic (100.64.0.0/10) and route it through the proxy, making the Tailscale IP unreachable. Fix: add bypass rules at the top of Clash config:
```yaml
IP-CIDR,100.64.0.0/10,DIRECT,no-resolve
DOMAIN-SUFFIX,ts.net,DIRECT
```
If using Fake-IP DNS mode, also add `*.ts.net` to `fake-ip-filter`.

**Desktop Client Setup**

On Arch (CLI already installed):
```bash
hermes desktop
```

On Windows: download .exe from `https://hermes-agent.nousresearch.com/desktop`.

In Desktop settings → Gateway → Remote Gateway:
- Remote URL: `http://<tailscale-ip>:9120`
- No auth (open inside tailnet), or use token from step 2

**Experience Notes**

- First connection: session list loads slowly (initial sync of all history)
- After warm-up: chat is smooth, no perceptible latency
- Both Arch and Windows work
- Thin client — all tools (terminal, file ops) execute on VPS

## Final System State

- 8 services active (Dashboard, Gateway, Caddy, SSH, fail2ban, UFW, Tailscale, Desktop)
- Dashboard: `0.0.0.0:9120` with `--insecure` (Tailscale-only, no public reverse proxy)
- Telegram platform: connected and responding
- Hermes Desktop connected via Tailscale: Arch + Windows clients
- Backup: daily at 03:00 UTC via Hermes internal cron (successful)
- Daily automatic backup to Cloudflare R2 at 03:00 UTC
- Memory provider: Honcho (self-hosted on local Docker)
