feat: deploy zopu single-node production
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
# Zopu Single-Node Runtime Deployment
|
||||
|
||||
Deployment artifacts for the Zopu execution plane on a single Debian dedicated
|
||||
server. Convex is already deployed as the control plane; this lane deploys only
|
||||
the execution plane: the daemon (Bun/Effect + AgentOS/RivetKit), the Flue agent
|
||||
service, Docker Engine for future Orb sandboxes, and supporting infrastructure.
|
||||
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
|
||||
|
||||
@@ -13,23 +10,17 @@ service, Docker Engine for future Orb sandboxes, and supporting infrastructure.
|
||||
│ ~12 CPU cores · ~40 GB RAM · single-node │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ zopu-daemon │ │ zopu-agent │ │ Docker │ │
|
||||
│ │ zopu-web │ │ zopu-agent │ │ Docker │ │
|
||||
│ │ (systemd) │ │ (systemd) │ │ Engine │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ Effect daemon│ │ Flue Node │ │ Orb sandboxes│ │
|
||||
│ │ + RivetKit │ │ server.mjs │ │ (future) │ │
|
||||
│ │ in-process │ │ :3583 │ │ │ │
|
||||
│ │ engine │ │ │ │ │ │
|
||||
│ │ + native │ │ │ │ │ │
|
||||
│ │ sidecar │ │ │ │ │ │
|
||||
│ │ :6420 │ │ │ │ │ │
|
||||
│ │ React Router │ │ Flue Node 22 │ │ Convex │ │
|
||||
│ │ :13100 │ │ server.mjs │ │ backend │ │
|
||||
│ │ │ │ :3583 │ │ :3210/:3211 │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │
|
||||
│ │ │ │
|
||||
│ └────────┬───────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────▼─────────┐ │
|
||||
│ │ Convex (cloud) │ ← control plane (pre-deployed)│
|
||||
│ └──────────────────┘ │
|
||||
│ ┌──────▼───────┐ │
|
||||
│ │ zopu-daemon │ Bun + Effect + RivetKit :6420 │
|
||||
│ └──────────────┘ │
|
||||
│ │
|
||||
│ systemd timers: health-check (60s), docker-cleanup (daily) │
|
||||
│ cron: disk-monitor (daily 06:00) │
|
||||
@@ -40,31 +31,15 @@ service, Docker Engine for future Orb sandboxes, and supporting infrastructure.
|
||||
|
||||
### 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.
|
||||
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`.
|
||||
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.
|
||||
**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.
|
||||
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
|
||||
|
||||
@@ -76,6 +51,7 @@ deploy/zopu-runtime/
|
||||
├── 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
|
||||
@@ -108,7 +84,7 @@ bash bootstrap.sh
|
||||
nano /opt/zopu/.env
|
||||
|
||||
# 5. Start services:
|
||||
systemctl start zopu-daemon
|
||||
systemctl start zopu-web zopu-daemon
|
||||
sleep 3
|
||||
systemctl start zopu-agent
|
||||
|
||||
@@ -122,20 +98,20 @@ systemctl enable --now zopu-health.timer zopu-docker-cleanup.timer
|
||||
## Start / stop / restart
|
||||
|
||||
```bash
|
||||
# Start both services
|
||||
systemctl start zopu-daemon zopu-agent
|
||||
# Start all services
|
||||
systemctl start zopu-web zopu-daemon zopu-agent
|
||||
|
||||
# Stop both services
|
||||
systemctl stop zopu-agent zopu-daemon
|
||||
# Stop all services
|
||||
systemctl stop zopu-agent zopu-daemon zopu-web
|
||||
|
||||
# Restart (daemon first — it owns the RivetKit engine)
|
||||
systemctl restart zopu-daemon && sleep 3 && systemctl restart zopu-agent
|
||||
systemctl restart zopu-web zopu-daemon && sleep 3 && systemctl restart zopu-agent
|
||||
|
||||
# Enable on boot
|
||||
systemctl enable zopu-daemon zopu-agent
|
||||
systemctl enable zopu-web zopu-daemon zopu-agent
|
||||
|
||||
# Disable on boot
|
||||
systemctl disable zopu-daemon zopu-agent
|
||||
systemctl disable zopu-web zopu-daemon zopu-agent
|
||||
```
|
||||
|
||||
## Log inspection
|
||||
@@ -183,16 +159,14 @@ 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.
|
||||
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
|
||||
|
||||
@@ -208,6 +182,7 @@ publicly addressable on the engine endpoint.
|
||||
```
|
||||
|
||||
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
|
||||
@@ -224,9 +199,7 @@ The update script:
|
||||
/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.
|
||||
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
|
||||
|
||||
@@ -242,11 +215,11 @@ 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
|
||||
@@ -259,69 +232,71 @@ Named volumes and running containers are never removed.
|
||||
/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%.
|
||||
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:
|
||||
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:
|
||||
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.
|
||||
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:
|
||||
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`, `PORT` |
|
||||
| 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
|
||||
|
||||
- **Web app**: the web frontend is not deployed in this lane.
|
||||
- **Kubernetes**: no container orchestration.
|
||||
- **PostgreSQL for Rivet**: the in-process RivetKit engine uses its own
|
||||
storage; no external PostgreSQL is required.
|
||||
- **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.
|
||||
- **Production generated-app hosting**: only the execution plane runs here.
|
||||
- **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.
|
||||
- **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:
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user