307 lines
12 KiB
Markdown
307 lines
12 KiB
Markdown
# Zopu Single-Node Runtime Deployment
|
|
|
|
Deployment artifacts for the complete Zopu stack on a single Debian dedicated server: the React Router web app, a persistent self-hosted Convex control plane, the daemon (Bun/Effect + AgentOS/RivetKit), the Flue agent service, Docker Engine, and supporting infrastructure.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Debian Dedicated Server │
|
|
│ ~12 CPU cores · ~40 GB RAM · single-node │
|
|
│ │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ zopu-web │ │ zopu-agent │ │ Docker │ │
|
|
│ │ (systemd) │ │ (systemd) │ │ Engine │ │
|
|
│ │ │ │ │ │ │ │
|
|
│ │ React Router │ │ Flue Node 22 │ │ Convex │ │
|
|
│ │ :13100 │ │ server.mjs │ │ backend │ │
|
|
│ │ │ │ :3583 │ │ :3210/:3211 │ │
|
|
│ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │
|
|
│ │ │ │
|
|
│ ┌──────▼───────┐ │
|
|
│ │ zopu-daemon │ Bun + Effect + RivetKit :6420 │
|
|
│ └──────────────┘ │
|
|
│ │
|
|
│ systemd timers: health-check (60s), docker-cleanup (daily) │
|
|
│ cron: disk-monitor (daily 06:00) │
|
|
│ ufw: deny-incoming, SSH + tailscale0 │
|
|
│ Tailscale: optional private overlay │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Single-node RivetKit topology
|
|
|
|
The daemon calls `registry.start()` from `rivetkit`, which boots an **in-process RivetKit engine** (envoy mode) backed by a **native Rust sidecar** binary (`@rivet-dev/agentos-sidecar`, platform-resolved). The engine listens on `RIVET_ENDPOINT` (default `http://localhost:6420`). The daemon then calls `createClient(RIVET_ENDPOINT)` to connect back to its own in-process engine for actor dispatch.
|
|
|
|
Evidence: RivetKit source `chunk-YDUQHING.js` line 4751 — `DEFAULT_ENDPOINT = "http://localhost:6420"`. The `Registry.start()` method calls `#startEnvoy()` → `runtime.serveRegistry()` for serverful mode (Mode A). The `createClient()` function reads `RIVET_ENDPOINT` env or defaults to the same `http://localhost:6420`.
|
|
|
|
**No separate Rivet Engine process is required.** The engine, actor envoy, and sidecar all run inside the daemon process. A future multi-node deployment would externalize the engine, but that is out of scope.
|
|
|
|
### Docker / Orb boundary
|
|
|
|
Docker Engine is installed and the `zopu` service user is in the `docker` group. The daemon's systemd unit includes `SupplementaryGroups=docker`. However, **no Docker-backed sandbox code is currently wired**. The Orb sandbox lane contract has not landed; Docker access is provisioned now so the boundary is ready. The current agent uses the in-process AgentOS VM (Wasm/V8) sandbox, not Docker.
|
|
|
|
## Files
|
|
|
|
```
|
|
deploy/zopu-runtime/
|
|
├── bootstrap.sh # One-shot Debian installer
|
|
├── .env.template # Environment template (all groups documented)
|
|
├── README.md # This file (runbook)
|
|
├── systemd/
|
|
│ ├── zopu-daemon.service # Daemon systemd unit
|
|
│ ├── zopu-agent.service # Agent systemd unit
|
|
│ ├── zopu-web.service # React Router web systemd unit
|
|
│ ├── zopu-health.service # Health check oneshot
|
|
│ ├── zopu-health.timer # Health check every 60s
|
|
│ ├── zopu-docker-cleanup.service
|
|
│ └── zopu-docker-cleanup.timer
|
|
├── scripts/
|
|
│ ├── health-check.sh # TCP/process health probes
|
|
│ ├── update.sh # Update to branch or commit
|
|
│ ├── rollback.sh # Roll back to previous commit
|
|
│ ├── docker-cleanup.sh # Prune stopped containers/images/networks
|
|
│ └── disk-monitor.sh # Disk usage alerting
|
|
└── caddy/
|
|
└── Caddyfile # Optional reverse proxy config (documentation)
|
|
```
|
|
|
|
## Fresh install
|
|
|
|
```bash
|
|
# 1. SSH into the fresh Debian 12 server as root.
|
|
|
|
# 2. Set environment overrides (optional):
|
|
export ZOPU_REPO_URL="ssh://git@git.openputer.com:2222/puter/zopu-code.git"
|
|
export ZOPU_REPO_BRANCH="dogfood/v0"
|
|
# export TAILSCALE_AUTHKEY="tskey-..."
|
|
# export TAILSCALE_HOSTNAME="zopu-runtime"
|
|
|
|
# 3. Run the bootstrap script:
|
|
bash bootstrap.sh
|
|
|
|
# 4. Edit .env with real values:
|
|
nano /opt/zopu/.env
|
|
|
|
# 5. Start services:
|
|
systemctl start zopu-web zopu-daemon
|
|
sleep 3
|
|
systemctl start zopu-agent
|
|
|
|
# 6. Enable timers:
|
|
systemctl enable --now zopu-health.timer zopu-docker-cleanup.timer
|
|
|
|
# 7. Verify:
|
|
/opt/zopu/deploy/zopu-runtime/scripts/health-check.sh
|
|
```
|
|
|
|
## Start / stop / restart
|
|
|
|
```bash
|
|
# Start all services
|
|
systemctl start zopu-web zopu-daemon zopu-agent
|
|
|
|
# Stop all services
|
|
systemctl stop zopu-agent zopu-daemon zopu-web
|
|
|
|
# Restart (daemon first — it owns the RivetKit engine)
|
|
systemctl restart zopu-web zopu-daemon && sleep 3 && systemctl restart zopu-agent
|
|
|
|
# Enable on boot
|
|
systemctl enable zopu-web zopu-daemon zopu-agent
|
|
|
|
# Disable on boot
|
|
systemctl disable zopu-web zopu-daemon zopu-agent
|
|
```
|
|
|
|
## Log inspection
|
|
|
|
All service logs go to journald with `SyslogIdentifier` tags.
|
|
|
|
```bash
|
|
# Daemon logs (live follow)
|
|
journalctl -u zopu-daemon -f
|
|
|
|
# Agent logs (live follow)
|
|
journalctl -u zopu-agent -f
|
|
|
|
# Last 100 lines of daemon
|
|
journalctl -u zopu-daemon -n 100
|
|
|
|
# Logs since boot
|
|
journalctl -u zopu-daemon -b
|
|
|
|
# Health check timer logs
|
|
journalctl -u zopu-health.service -n 50
|
|
|
|
# Docker cleanup logs
|
|
journalctl -u zopu-docker-cleanup.service -n 50
|
|
|
|
# Disk monitor logs (cron → file)
|
|
tail -100 /var/log/zopu/disk-monitor.log
|
|
|
|
# All Zopu syslog identifiers
|
|
journalctl -t zopu-daemon -t zopu-agent --since "1 hour ago"
|
|
```
|
|
|
|
## Health checks
|
|
|
|
```bash
|
|
# Manual health check (prints all probes)
|
|
/opt/zopu/deploy/zopu-runtime/scripts/health-check.sh
|
|
|
|
# Quiet mode (exit code only)
|
|
/opt/zopu/deploy/zopu-runtime/scripts/health-check.sh --quiet
|
|
|
|
# Check systemd timer is running
|
|
systemctl status zopu-health.timer
|
|
systemctl list-timers zopu-health.timer
|
|
```
|
|
|
|
The health check probes:
|
|
|
|
1. `zopu-daemon` systemd unit is active
|
|
2. `zopu-agent` systemd unit is active
|
|
3. RivetKit engine port (default 6420) accepts TCP connections
|
|
4. Flue agent port (default 3583) accepts TCP connections
|
|
5. Docker daemon responds to `docker info`
|
|
|
|
No HTTP health endpoints are assumed. Flue does not expose one by design (per Flue docs: "Flue does not add a health endpoint"). RivetKit's health route is internal to the registry runtime and not documented as publicly addressable on the engine endpoint.
|
|
|
|
## Update to commit
|
|
|
|
```bash
|
|
# Update to latest of dogfood/v0 (default)
|
|
/opt/zopu/deploy/zopu-runtime/scripts/update.sh
|
|
|
|
# Update to a specific branch
|
|
/opt/zopu/deploy/zopu-runtime/scripts/update.sh dogfood/runtime-deploy
|
|
|
|
# Update to a specific commit
|
|
/opt/zopu/deploy/zopu-runtime/scripts/update.sh abc123def456
|
|
```
|
|
|
|
The update script:
|
|
|
|
1. Records current HEAD to `.last-deployed-sha`
|
|
2. Fetches, resolves branch-or-commit, checks out
|
|
3. `bun install`, builds daemon and agent
|
|
4. Restarts daemon, waits, restarts agent
|
|
5. Runs health check; reports failure and rollback instructions
|
|
|
|
## Rollback
|
|
|
|
```bash
|
|
# Roll back to the previously deployed commit
|
|
/opt/zopu/deploy/zopu-runtime/scripts/rollback.sh
|
|
|
|
# Roll back to a specific commit
|
|
/opt/zopu/deploy/zopu-runtime/scripts/rollback.sh abc123def456
|
|
```
|
|
|
|
Rollback reads `.last-deployed-sha` (written by `update.sh`), checks out that commit, rebuilds, and restarts services. The pre-rollback SHA is saved to `.pre-rollback-sha` for re-rollback if needed.
|
|
|
|
## Docker cleanup
|
|
|
|
```bash
|
|
# Manual cleanup
|
|
/opt/zopu/deploy/zopu-runtime/scripts/docker-cleanup.sh
|
|
|
|
# Check Docker disk usage
|
|
docker system df
|
|
|
|
# Timer runs daily; check its schedule
|
|
systemctl list-timers zopu-docker-cleanup.timer
|
|
```
|
|
|
|
Cleanup prunes:
|
|
|
|
- Stopped containers older than 24 hours
|
|
- Dangling (untagged) images
|
|
- Unused networks
|
|
|
|
Named volumes and running containers are never removed.
|
|
|
|
## Disk-space monitoring
|
|
|
|
```bash
|
|
# Manual check
|
|
/opt/zopu/deploy/zopu-runtime/scripts/disk-monitor.sh
|
|
|
|
# Custom threshold (90%)
|
|
/opt/zopu/deploy/zopu-runtime/scripts/disk-monitor.sh --warn-percent 90
|
|
```
|
|
|
|
A cron job runs at 06:00 daily and writes to `/var/log/zopu/disk-monitor.log`. Default alert threshold is 80%.
|
|
|
|
## Firewall and private networking
|
|
|
|
The firewall (`ufw`) is deny-by-default:
|
|
|
|
- SSH (port 22) is allowed on all interfaces
|
|
- All traffic on `tailscale0` is allowed (Tailscale private overlay)
|
|
- All other incoming traffic is denied
|
|
|
|
The RivetKit engine (`:6420`) and Flue agent (`:3583`) ports are **not** exposed on public interfaces. Reachability options:
|
|
|
|
1. **Tailscale** (recommended): bootstrap runs `ufw allow in on tailscale0` so all ports are reachable over the private overlay. Set `TAILSCALE_AUTHKEY` before running bootstrap to configure automatically. Other Tailscale-connected machines can reach the agent at `http://zopu-runtime:3583` and the engine at `http://zopu-runtime:6420`.
|
|
2. **Custom private interface**: if you have a non-Tailscale private network (e.g. a VLAN or wireguard interface), add an explicit rule:
|
|
```bash
|
|
ufw allow in on eth1 # or your private interface name
|
|
```
|
|
Do NOT assume direct private IP access works by default — the deny-incoming policy blocks it until an interface-specific rule is added.
|
|
3. **Caddy** (optional): install Caddy and use the annotated Caddyfile in `caddy/` if you need TLS termination or a public ingress point.
|
|
|
|
## Environment groups
|
|
|
|
See [`.env.template`](./.env.template) for the full annotated template. The eight required groups:
|
|
|
|
| Group | Variables |
|
|
| --- | --- |
|
|
| Convex | `CONVEX_URL`, `CONVEX_SITE_URL`, `SITE_URL` |
|
|
| Gitea | `GITEA_URL`, `GITEA_TOKEN` |
|
|
| Model gateway | `AGENT_MODEL_*` |
|
|
| AgentOS/RivetKit | `RIVET_ENDPOINT` (optional) |
|
|
| Zopu agent | `FLUE_DB_TOKEN` (`zopu-agent.service` sets port 3583) |
|
|
| Daemon | `DAEMON_ID`, `DAEMON_NAME`, `DAEMON_VERSION`, `DAEMON_HEARTBEAT_MS`, `DAEMON_COMMAND_LEASE_MS` |
|
|
| Docker sandbox | group membership (no env vars) |
|
|
| Service auth | `AUTH_SECRET` (if needed) |
|
|
|
|
## Public single-node routes
|
|
|
|
The checked-in Caddy example assumes Cloudflare Tunnel terminates TLS and sends the four Zopu hosts to Caddy on loopback:
|
|
|
|
- `zopu.sai-onchain.me` → React Router web app on `127.0.0.1:13100`
|
|
- `zopu-api.sai-onchain.me` → Convex API on `127.0.0.1:3210`
|
|
- `zopu-site.sai-onchain.me` → Convex HTTP actions on `127.0.0.1:3211`
|
|
- `zopu-agent.sai-onchain.me` → Flue on `127.0.0.1:3583`
|
|
|
|
Self-hosted Convex state lives in the `zopu-convex-data` Docker volume. Generate the CLI admin key after the backend is healthy:
|
|
|
|
```bash
|
|
docker compose \
|
|
--env-file /opt/zopu/.env \
|
|
-f /opt/zopu/deploy/zopu-runtime/convex/docker-compose.yml \
|
|
exec backend ./generate_admin_key.sh
|
|
```
|
|
|
|
## What is NOT deployed
|
|
|
|
- **Kubernetes**: no container orchestration.
|
|
- **PostgreSQL for Rivet**: the in-process RivetKit engine uses its own storage; no external PostgreSQL is required.
|
|
- **Multi-node coordination**: single-node only.
|
|
- **Public administration endpoints**: no admin HTTP surface.
|
|
- **Secrets in source**: `.env` is never committed; `.env.template` contains only placeholder values.
|
|
- **Docker-backed Orb sandboxes**: Docker is installed and access is provisioned, but no Orb sandbox code is wired. This is a boundary prepared for the Orb lane, not a working feature.
|
|
|
|
## Reproducibility
|
|
|
|
The deployment does not require the developer's MacBook to remain online. Once bootstrap completes and `.env` is filled in:
|
|
|
|
1. Services run under systemd with `Restart=always`.
|
|
2. Logs persist in journald.
|
|
3. Health checks run every 60 seconds via systemd timer.
|
|
4. Docker cleanup runs daily.
|
|
5. Disk usage is monitored daily.
|
|
6. Unattended-upgrades handles Debian security patches.
|