feat: single-node Zopu runtime deployment for Debian
Lane D deliverable: reproducible execution-plane deployment on a fresh Debian dedicated server (~12 cores, 40 GB RAM, single-node). Artifacts: - bootstrap.sh: Docker Engine, Bun, repo clone, build, systemd install, firewall (deny-by-default), optional Tailscale, disk monitor cron - .env.template: all 8 environment groups documented (Convex, Gitea, model gateway, AgentOS/RivetKit, Zopu agent, daemon, Docker sandbox, service auth) - systemd units: zopu-daemon, zopu-agent (both Restart=always), zopu-health timer (60s), zopu-docker-cleanup timer (daily) - scripts: health-check, update, rollback, docker-cleanup, disk-monitor - Caddyfile: optional reverse proxy (documentation-only by default) - README.md: full runbook with start/stop/log/update/rollback/cleanup procedures, topology documentation, and boundary notes Verified topology (from installed RivetKit source): - registry.start() boots an in-process RivetKit engine (envoy mode) with a native Rust sidecar at http://localhost:6420 (DEFAULT_ENDPOINT in chunk-YDUQHING.js line 4751). createClient() connects back to it. - No separate Rivet Engine process required for single-node operation. - Linux x64 sidecar packages (@rivet-dev/agentos-sidecar-linux-x64-gnu) are available as optional dependencies. Docker / Orb boundary: - Docker Engine installed and zopu user in docker group. - No Docker-backed sandbox code wired; Orb lane contract not yet landed. - Volume pruning omitted from docker-cleanup.sh to protect durable data. Validated: bash -n on all 6 shell scripts; mocked health-check smoke (nc-based TCP probes, mocked systemctl/docker); systemd unit structural checks. No JS/TS/agent files changed.
This commit is contained in:
63
deploy/zopu-runtime/scripts/disk-monitor.sh
Executable file
63
deploy/zopu-runtime/scripts/disk-monitor.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# disk-monitor.sh — Check disk usage and alert when above threshold.
|
||||
#
|
||||
# Usage:
|
||||
# disk-monitor.sh # print usage, warn at 80%
|
||||
# disk-monitor.sh --warn-percent 90 # custom threshold
|
||||
#
|
||||
# Requires GNU coreutils df (standard on Debian). Uses --output for
|
||||
# deterministic column ordering regardless of locale.
|
||||
#
|
||||
# Exit code 0 if under threshold, 1 if at or above.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
WARN_PERCENT=80
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--warn-percent)
|
||||
WARN_PERCENT="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
EXIT_CODE=0
|
||||
|
||||
# Partitions to check: install root and /var (Docker data-root is often here)
|
||||
PARTITIONS="${PARTITIONS:-/ /var}"
|
||||
|
||||
for partition in $PARTITIONS; do
|
||||
if [[ ! -d "$partition" ]]; then
|
||||
continue
|
||||
fi
|
||||
# GNU df --output columns: pcent (Use%), size, avail, target (Mounted on)
|
||||
read -r USAGE_PCT SIZE AVAIL MOUNT <<< "$(df -h --output=pcent,size,avail,target "$partition" | awk 'NR==2 {gsub(/%/,"",$1); print $1, $2, $3, $4}')"
|
||||
|
||||
if [[ -z "${USAGE_PCT:-}" || ! "${USAGE_PCT:-}" =~ ^[0-9]+$ ]]; then
|
||||
echo " [skip] Could not read usage for ${partition}"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$USAGE_PCT" -ge "$WARN_PERCENT" ]]; then
|
||||
echo -e " ${RED}WARN${NC} ${MOUNT}: ${USAGE_PCT}% used (${AVAIL} avail of ${SIZE}) — threshold ${WARN_PERCENT}%"
|
||||
EXIT_CODE=1
|
||||
elif [[ "$USAGE_PCT" -ge $((WARN_PERCENT - 10)) ]]; then
|
||||
echo -e " ${YELLOW}NOTE${NC} ${MOUNT}: ${USAGE_PCT}% used (${AVAIL} avail of ${SIZE}) — approaching threshold"
|
||||
else
|
||||
echo -e " ${GREEN}OK${NC} ${MOUNT}: ${USAGE_PCT}% used (${AVAIL} avail of ${SIZE})"
|
||||
fi
|
||||
done
|
||||
|
||||
exit "$EXIT_CODE"
|
||||
36
deploy/zopu-runtime/scripts/docker-cleanup.sh
Executable file
36
deploy/zopu-runtime/scripts/docker-cleanup.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# docker-cleanup.sh — Remove stopped containers, dangling images, and unused networks.
|
||||
#
|
||||
# Safe to run via systemd timer (daily). Uses Docker's built-in pruning
|
||||
# commands with conservative scope.
|
||||
#
|
||||
# Does NOT prune volumes. Named volumes may hold durable data and cannot be
|
||||
# safely auto-pruned in a deployment that mixes stateful workloads. When the
|
||||
# Orb lane lands with labeled resources, volume pruning can be scoped to
|
||||
# Orb-managed labels (e.g. --filter label=org.openputer.orb). Until then,
|
||||
# manage volumes manually.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "[docker-cleanup] $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
# Remove stopped containers older than 24 hours
|
||||
echo "[docker-cleanup] Pruning stopped containers (>24h old)..."
|
||||
docker container prune -f --filter "until=24h"
|
||||
|
||||
# Remove dangling images (untagged intermediate layers only)
|
||||
echo "[docker-cleanup] Pruning dangling images..."
|
||||
docker image prune -f
|
||||
|
||||
# Remove unused networks
|
||||
echo "[docker-cleanup] Pruning unused networks..."
|
||||
docker network prune -f
|
||||
|
||||
# Volumes are intentionally NOT pruned. See header comment.
|
||||
|
||||
# Show remaining disk usage
|
||||
echo "[docker-cleanup] Docker disk usage:"
|
||||
docker system df
|
||||
|
||||
echo "[docker-cleanup] Done."
|
||||
122
deploy/zopu-runtime/scripts/health-check.sh
Executable file
122
deploy/zopu-runtime/scripts/health-check.sh
Executable file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# health-check.sh — Probe the Zopu execution plane.
|
||||
#
|
||||
# Checks (TCP/process-level; no assumed HTTP health routes):
|
||||
# 1. zopu-daemon systemd unit is active
|
||||
# 2. zopu-agent systemd unit is active
|
||||
# 3. RivetKit engine TCP port (RIVET_ENDPOINT, default 6420) accepts connections
|
||||
# 4. Flue agent TCP port (PORT, default 3583) accepts connections
|
||||
# 5. Docker daemon is reachable
|
||||
#
|
||||
# Flue does not expose a health endpoint by design. RivetKit's health route
|
||||
# is internal to the registry runtime. We use TCP connection checks only.
|
||||
#
|
||||
# Usage:
|
||||
# health-check.sh # print results
|
||||
# health-check.sh --quiet # suppress output, exit 0 only if all healthy
|
||||
#
|
||||
# Exit codes: 0 = all healthy, 1 = one or more unhealthy
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
QUIET=false
|
||||
[[ "${1:-}" == "--quiet" ]] && QUIET=true
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
pass() { $QUIET || echo -e " ${GREEN}PASS${NC} $*"; }
|
||||
fail() { echo -e " ${RED}FAIL${NC} $*" >&2; FAILURES=$((FAILURES + 1)); }
|
||||
|
||||
FAILURES=0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Load environment
|
||||
# ---------------------------------------------------------------------------
|
||||
ENV_FILE="${ENV_FILE:-/opt/zopu/.env}"
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
set -a
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
fi
|
||||
|
||||
RIVET_PORT="6420"
|
||||
if [[ -n "${RIVET_ENDPOINT:-}" ]]; then
|
||||
RIVET_PORT=$(echo "$RIVET_ENDPOINT" | sed -n 's|.*://[^:]*:\([0-9]*\).*|\1|p')
|
||||
[[ -z "$RIVET_PORT" ]] && RIVET_PORT="6420"
|
||||
fi
|
||||
|
||||
AGENT_PORT="${PORT:-3583}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TCP probe helper: works on Debian (nc from netcat-openbsd) and macOS.
|
||||
# Falls back to bash /dev/tcp if nc is unavailable.
|
||||
# ---------------------------------------------------------------------------
|
||||
tcp_probe() {
|
||||
local host="$1" port="$2"
|
||||
if command -v nc &>/dev/null; then
|
||||
nc -z -w 5 "$host" "$port" 2>/dev/null
|
||||
else
|
||||
timeout 5 bash -c "echo > /dev/tcp/${host}/${port}" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Daemon systemd unit
|
||||
# ---------------------------------------------------------------------------
|
||||
if systemctl is-active --quiet zopu-daemon 2>/dev/null; then
|
||||
pass "zopu-daemon service is active"
|
||||
else
|
||||
fail "zopu-daemon service is not active"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Agent systemd unit
|
||||
# ---------------------------------------------------------------------------
|
||||
if systemctl is-active --quiet zopu-agent 2>/dev/null; then
|
||||
pass "zopu-agent service is active"
|
||||
else
|
||||
fail "zopu-agent service is not active"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. RivetKit engine TCP port
|
||||
# ---------------------------------------------------------------------------
|
||||
if tcp_probe localhost "$RIVET_PORT"; then
|
||||
pass "RivetKit engine port ${RIVET_PORT} is accepting connections"
|
||||
else
|
||||
fail "RivetKit engine port ${RIVET_PORT} is not accepting connections"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Flue agent TCP port
|
||||
# ---------------------------------------------------------------------------
|
||||
if tcp_probe localhost "$AGENT_PORT"; then
|
||||
pass "Flue agent port ${AGENT_PORT} is accepting connections"
|
||||
else
|
||||
fail "Flue agent port ${AGENT_PORT} is not accepting connections"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Docker daemon
|
||||
# ---------------------------------------------------------------------------
|
||||
if docker info &>/dev/null; then
|
||||
pass "Docker daemon is reachable"
|
||||
else
|
||||
fail "Docker daemon is not reachable"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
$QUIET || echo ""
|
||||
if [[ "$FAILURES" -eq 0 ]]; then
|
||||
$QUIET || echo -e "${GREEN}All checks passed.${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}${FAILURES} check(s) failed.${NC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
88
deploy/zopu-runtime/scripts/rollback.sh
Executable file
88
deploy/zopu-runtime/scripts/rollback.sh
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# rollback.sh — Roll back to the previously deployed commit.
|
||||
#
|
||||
# Usage:
|
||||
# rollback.sh # roll back to .last-deployed-sha
|
||||
# rollback.sh <sha> # roll back to a specific commit
|
||||
#
|
||||
# .env is gitignored and is never touched by git operations. It survives
|
||||
# updates and rollbacks unchanged.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_DIR="${ZOPU_INSTALL_DIR:-/opt/zopu}"
|
||||
SERVICE_USER="${ZOPU_SERVICE_USER:-zopu}"
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[rollback]${NC} $*"; }
|
||||
err() { echo -e "${RED}[rollback]${NC} $*" >&2; }
|
||||
|
||||
cd "$INSTALL_DIR"
|
||||
|
||||
# Determine target
|
||||
ROLLBACK_SHA="${1:-}"
|
||||
if [[ -z "$ROLLBACK_SHA" ]]; then
|
||||
LAST_SHA_FILE="${INSTALL_DIR}/.last-deployed-sha"
|
||||
if [[ ! -f "$LAST_SHA_FILE" ]]; then
|
||||
err "No previous deployment recorded in ${LAST_SHA_FILE}."
|
||||
err "Pass a commit SHA explicitly: rollback.sh <sha>"
|
||||
exit 1
|
||||
fi
|
||||
ROLLBACK_SHA=$(cat "$LAST_SHA_FILE")
|
||||
fi
|
||||
|
||||
# Validate commit exists
|
||||
if ! git rev-parse --verify "${ROLLBACK_SHA}^{commit}" &>/dev/null; then
|
||||
err "Commit ${ROLLBACK_SHA} does not exist in the local repository."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT_SHA=$(git rev-parse HEAD)
|
||||
log "Current: ${CURRENT_SHA:0:12}"
|
||||
log "Rolling back to: ${ROLLBACK_SHA:0:12}"
|
||||
|
||||
# Save current state before rolling back (enables re-rollback)
|
||||
echo "$CURRENT_SHA" > "${INSTALL_DIR}/.pre-rollback-sha"
|
||||
|
||||
# Checkout target
|
||||
git checkout "$ROLLBACK_SHA"
|
||||
|
||||
# Confirm .env is intact
|
||||
if [[ -f "${INSTALL_DIR}/.env" ]]; then
|
||||
log ".env preserved."
|
||||
else
|
||||
err ".env is missing! Restore it from backup before starting services."
|
||||
fi
|
||||
|
||||
log "Running bun install..."
|
||||
sudo -u "$SERVICE_USER" bun install
|
||||
|
||||
log "Building daemon..."
|
||||
sudo -u "$SERVICE_USER" bun run build:daemon
|
||||
|
||||
log "Building agent service..."
|
||||
sudo -u "$SERVICE_USER" bun run build:agents
|
||||
|
||||
log "Restarting services..."
|
||||
systemctl restart zopu-daemon
|
||||
sleep 3
|
||||
systemctl restart zopu-agent
|
||||
sleep 5
|
||||
|
||||
# Health check
|
||||
HEALTH_SCRIPT="${INSTALL_DIR}/deploy/zopu-runtime/scripts/health-check.sh"
|
||||
if [[ -x "$HEALTH_SCRIPT" ]]; then
|
||||
log "Running health check..."
|
||||
if "$HEALTH_SCRIPT"; then
|
||||
log "Rollback complete and healthy."
|
||||
else
|
||||
err "Health check failed after rollback!"
|
||||
err " journalctl -u zopu-daemon -n 50"
|
||||
err " journalctl -u zopu-agent -n 50"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
97
deploy/zopu-runtime/scripts/update.sh
Executable file
97
deploy/zopu-runtime/scripts/update.sh
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# update.sh — Update the Zopu runtime to a specific branch or commit.
|
||||
#
|
||||
# Usage:
|
||||
# update.sh # update to latest dogfood/v0
|
||||
# update.sh dogfood/v0 # update to latest of branch dogfood/v0
|
||||
# update.sh <commit-sha> # checkout and build a specific commit
|
||||
#
|
||||
# The argument is treated as a branch name first; if no matching remote
|
||||
# tracking branch exists it is treated as a commit SHA.
|
||||
#
|
||||
# .env is gitignored and is never touched by git operations. It survives
|
||||
# updates and rollbacks unchanged.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_DIR="${ZOPU_INSTALL_DIR:-/opt/zopu}"
|
||||
SERVICE_USER="${ZOPU_SERVICE_USER:-zopu}"
|
||||
TARGET="${1:-dogfood/v0}"
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[update]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[update]${NC} $*"; }
|
||||
err() { echo -e "${RED}[update]${NC} $*" >&2; }
|
||||
|
||||
cd "$INSTALL_DIR"
|
||||
|
||||
# Record current commit for rollback
|
||||
CURRENT_SHA=$(git rev-parse HEAD)
|
||||
log "Current HEAD: ${CURRENT_SHA:0:12}"
|
||||
echo "$CURRENT_SHA" > "${INSTALL_DIR}/.last-deployed-sha"
|
||||
|
||||
# Fetch all remotes
|
||||
log "Fetching from origin..."
|
||||
git fetch origin
|
||||
|
||||
# Resolve target: branch first, then commit
|
||||
if git show-ref --verify --quiet "refs/remotes/origin/${TARGET}"; then
|
||||
log "Target is a branch: ${TARGET}"
|
||||
git checkout -B "$TARGET" "origin/${TARGET}"
|
||||
elif git rev-parse --verify "${TARGET}^{commit}" &>/dev/null; then
|
||||
log "Target is a commit: ${TARGET:0:12}"
|
||||
git checkout "$TARGET"
|
||||
else
|
||||
err "'${TARGET}' is not a known branch or valid commit."
|
||||
err "Available branches: $(git branch -r | tr '\n' ' ')"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NEW_SHA=$(git rev-parse HEAD)
|
||||
log "Now at: ${NEW_SHA:0:12}"
|
||||
|
||||
# Confirm .env is intact
|
||||
if [[ -f "${INSTALL_DIR}/.env" ]]; then
|
||||
log ".env preserved."
|
||||
else
|
||||
err ".env is missing! Restore it from backup before starting services."
|
||||
fi
|
||||
|
||||
# Install and build
|
||||
log "Running bun install..."
|
||||
sudo -u "$SERVICE_USER" bun install
|
||||
|
||||
log "Building daemon binary..."
|
||||
sudo -u "$SERVICE_USER" bun run build:daemon
|
||||
|
||||
log "Building agent service..."
|
||||
sudo -u "$SERVICE_USER" bun run build:agents
|
||||
|
||||
# Graceful restart: daemon first (owns RivetKit engine), then agent
|
||||
log "Restarting services..."
|
||||
systemctl restart zopu-daemon
|
||||
sleep 3
|
||||
systemctl restart zopu-agent
|
||||
sleep 5
|
||||
|
||||
# Health check
|
||||
HEALTH_SCRIPT="${INSTALL_DIR}/deploy/zopu-runtime/scripts/health-check.sh"
|
||||
if [[ -x "$HEALTH_SCRIPT" ]]; then
|
||||
log "Running health check..."
|
||||
if "$HEALTH_SCRIPT"; then
|
||||
log "Update complete and healthy."
|
||||
else
|
||||
err "Health check failed after update!"
|
||||
err " journalctl -u zopu-daemon -n 50"
|
||||
err " journalctl -u zopu-agent -n 50"
|
||||
err "To rollback: ${INSTALL_DIR}/deploy/zopu-runtime/scripts/rollback.sh"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log "Update complete. Verify manually."
|
||||
fi
|
||||
Reference in New Issue
Block a user