feat(web): vercel deployment and vds compose/systemd setup
Add Vercel config with React Router preset, vercel-build script and /api/auth rewrite to the Convex site. Add VDS staging compose for the Rivet engine and runner plus systemd units, deployment plan, and Docker/Vercel ignore rules. Lockfile covers both the web and agents dependency additions.
This commit is contained in:
63
deploy/compose/Caddyfile
Normal file
63
deploy/compose/Caddyfile
Normal file
@@ -0,0 +1,63 @@
|
||||
# Caddy reverse proxy for the Zopu VDS staging agent stack.
|
||||
#
|
||||
# This is the ONLY public ingress to the private worker (see
|
||||
# docs/DEPLOYMENT_PLAN.md §"Public surface"). It terminates TLS for the Flue
|
||||
# callback hostname that Convex reaches, then forwards ONLY the documented agent
|
||||
# worker paths to the internal `agents` service.
|
||||
#
|
||||
# Hard rules this file enforces:
|
||||
# - No generic /api/* proxy and no browser-to-Flue traffic (DEPLOYMENT_PLAN.md
|
||||
# line 86). Browsers talk only to Convex.
|
||||
# - Only the Flue Node worker paths Convex actually calls are routed:
|
||||
# /agents/zopu/<organizationId> conversation admission
|
||||
# /internal/work-attempts/* work attempt execute + cancel
|
||||
# /api/rivet/* RivetKit Actor gateway surface
|
||||
# /workflows/* Flue workflow execution
|
||||
# /health liveness probe (Compose healthcheck/CI)
|
||||
# - Everything else returns 404. The private worker protocol stays narrow.
|
||||
#
|
||||
# Variables are injected by the `caddy` Compose service environment:
|
||||
# {$AGENTS_HOST} public hostname, e.g. agents-staging.example.com
|
||||
# {$AGENTS_UPSTREAM} internal upstream host:port, e.g. agents:3000
|
||||
# {$ACME_EMAIL} email for the Let's Encrypt account
|
||||
#
|
||||
# TLS certs and the ACME account persist in the caddy-data volume. Caddy serves
|
||||
# its own ACME HTTP-01 challenge responses on :80 automatically, so the :80
|
||||
# block below only redirects everything else to HTTPS — worker traffic is never
|
||||
# served over plain HTTP.
|
||||
{
|
||||
email {$ACME_EMAIL}
|
||||
}
|
||||
|
||||
# --- TLS termination for the Flue callback hostname ---------------------------
|
||||
{$AGENTS_HOST} {
|
||||
encode zstd gzip
|
||||
|
||||
# Conversation admission: Convex POSTs to /agents/zopu/<organizationId>.
|
||||
# The conversationRoute middleware requires the FLUE_DB_TOKEN bearer and the
|
||||
# matching x-zopu-organization-id header.
|
||||
reverse_proxy /agents/zopu/* {$AGENTS_UPSTREAM}
|
||||
|
||||
# Internal work-attempt execute/cancel. Requires the internalRoute bearer.
|
||||
reverse_proxy /internal/work-attempts/* {$AGENTS_UPSTREAM}
|
||||
|
||||
# RivetKit Actor gateway surface (app.all("/api/rivet/*")). Routed so the
|
||||
# runtime registry handler is reachable for actor metadata/start.
|
||||
reverse_proxy /api/rivet/* {$AGENTS_UPSTREAM}
|
||||
|
||||
# Flue workflow execution (workflows/plan-work). Requires the workflowRoute
|
||||
# bearer + x-zopu-organization-id header.
|
||||
reverse_proxy /workflows/* {$AGENTS_UPSTREAM}
|
||||
|
||||
# Caddy routes the liveness request to the actual worker; it must not mask a
|
||||
# failed worker process with a synthetic successful response.
|
||||
reverse_proxy /health {$AGENTS_UPSTREAM}
|
||||
|
||||
# Everything else is not part of the private worker protocol.
|
||||
respond 404
|
||||
}
|
||||
|
||||
# --- HTTP → HTTPS redirect (Caddy still answers ACME challenges on :80) -------
|
||||
:80 {
|
||||
redir https://{host}{uri} permanent
|
||||
}
|
||||
56
deploy/compose/docker-compose.yml
Normal file
56
deploy/compose/docker-compose.yml
Normal file
@@ -0,0 +1,56 @@
|
||||
# Zopu VDS staging — Rivet engine + runner on host network.
|
||||
#
|
||||
# The agents registry and Caddy run on the HOST via systemd (not Docker).
|
||||
# This compose only manages the two Rivet services, both on host networking:
|
||||
#
|
||||
# engine — Rivet Engine (RocksDB backend), binds 127.0.0.1:6420 on the host.
|
||||
# UFW (policy DROP, only 22/80/443 open) keeps it private.
|
||||
# runner — AgentOS runner, reaches the engine at 127.0.0.1:6420.
|
||||
#
|
||||
# With network_mode: host there are no Docker bridge networks, no port
|
||||
# forwarding, and no host.docker.internal gymnastics — both services share
|
||||
# the host network namespace directly.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose --env-file .env up -d
|
||||
|
||||
services:
|
||||
engine:
|
||||
image: ${ENGINE_IMAGE:?ENGINE_IMAGE must be set}
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ${ZOPU_DEPLOY_ROOT:?ZOPU_DEPLOY_ROOT is required}/data/rivet
|
||||
target: /data
|
||||
environment:
|
||||
RIVET__FILE_SYSTEM__PATH: /data
|
||||
RIVET__AUTH__ADMIN_TOKEN: ${RIVET_ADMIN_TOKEN:?RIVET_ADMIN_TOKEN is required}
|
||||
RIVET_LOG_LEVEL: ${RIVET_LOG_LEVEL:-info}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:6420/health"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
runner:
|
||||
image: ${RUNNER_IMAGE:?RUNNER_IMAGE must be set}
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
RIVET_RUNNER_VERSION: ${RIVET_RUNNER_VERSION:?RIVET_RUNNER_VERSION is required}
|
||||
RIVET_ENVOY_VERSION: ${RIVET_RUNNER_VERSION:?RIVET_RUNNER_VERSION is required}
|
||||
RIVET_ENDPOINT: http://${RIVET_NAMESPACE:-default}:${RIVET_ADMIN_TOKEN:?RIVET_ADMIN_TOKEN is required}@127.0.0.1:6420
|
||||
RIVET_WORKSPACE_TOKEN: ${RIVET_WORKSPACE_TOKEN:?RIVET_WORKSPACE_TOKEN is required}
|
||||
AGENT_WORKSPACE_ROOT: /var/lib/zopu/workspaces
|
||||
BUN_EXECUTABLE: /usr/local/bin/bun
|
||||
AGENT_MODEL_API_KEY: ${AGENT_MODEL_API_KEY:?AGENT_MODEL_API_KEY is required}
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ${ZOPU_DEPLOY_ROOT:?ZOPU_DEPLOY_ROOT is required}/workspaces
|
||||
target: /var/lib/zopu/workspaces
|
||||
depends_on:
|
||||
engine:
|
||||
condition: service_healthy
|
||||
7
deploy/compose/rivet-config.json
Normal file
7
deploy/compose/rivet-config.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://rivet.dev/engine-config-schema.json",
|
||||
"file_system": {
|
||||
"path": "/data"
|
||||
},
|
||||
"singleplayer": false
|
||||
}
|
||||
18
deploy/systemd/Caddyfile
Normal file
18
deploy/systemd/Caddyfile
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
email ops@zopu.ai
|
||||
}
|
||||
{$AGENTS_HOST} {
|
||||
encode zstd gzip
|
||||
|
||||
route {
|
||||
reverse_proxy /agents/zopu/* 127.0.0.1:3000
|
||||
reverse_proxy /internal/work-attempts/* 127.0.0.1:3000
|
||||
reverse_proxy /api/rivet/* 127.0.0.1:3000
|
||||
reverse_proxy /workflows/* 127.0.0.1:3000
|
||||
reverse_proxy /health 127.0.0.1:3000
|
||||
respond 404
|
||||
}
|
||||
}
|
||||
:80 {
|
||||
redir https://{host}{uri} permanent
|
||||
}
|
||||
23
deploy/systemd/zopu-agents.service
Normal file
23
deploy/systemd/zopu-agents.service
Normal file
@@ -0,0 +1,23 @@
|
||||
[Unit]
|
||||
Description=Zopu agent registry
|
||||
After=docker.service network-online.target
|
||||
Requires=docker.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=zopu
|
||||
Group=zopu
|
||||
WorkingDirectory=/srv/zopu/source/packages/agents
|
||||
EnvironmentFile=/etc/zopu/agents.env
|
||||
ExecStart=/opt/zopu/node/bin/node dist/server.mjs
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/srv/zopu/workspaces
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
4
deploy/systemd/zopu-caddy.service.conf
Normal file
4
deploy/systemd/zopu-caddy.service.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
[Service]
|
||||
EnvironmentFile=/etc/zopu/agents.env
|
||||
ExecStart=
|
||||
ExecStart=/usr/bin/caddy run --config /etc/caddy/Caddyfile
|
||||
Reference in New Issue
Block a user