Six fixes addressing operational correctness on a real Debian host:
1. Ownership after git operations: chown -R zopu:zopu on the checkout
after clone/update/rollback before running bun install/build as the
service user. .env kept at 0600 with explicit chmod after each
operation.
2. sudo replaced with runuser: minimal Debian does not include sudo.
runuser is part of util-linux (essential) and always available.
All three scripts (bootstrap, update, rollback) now use runuser -u.
3. cron.d entry fixed: /etc/cron.d format requires a username field.
Added ${SERVICE_USER} between the time fields and the command path.
4. Firewall: added explicit `ufw allow in on tailscale0` rule so the
deny-incoming default does not block Tailscale private-overlay
reachability. Removed inaccurate claim that direct private IP works
by default; documented that an explicit per-interface rule is needed.
5. SupplementaryGroups=docker added to zopu-agent.service: the Orb
sandbox runtime lives in the agent process, not just the daemon.
6. zopu-health.service: added Environment=ENV_FILE=__INSTALL_DIR__/.env
so health-check.sh sources the correct .env at custom install paths.
Verified Flue build output: bun run build:agents produces
packages/agents/dist/server.mjs (confirmed by running the build).
Agent unit ExecStart path is correct.
Validated: bash -n on all 6 shell scripts; mocked health-check smoke
against live RivetKit engine on port 6420; systemd unit structural
checks. No JS/TS/agent files changed.
275 lines
10 KiB
Bash
Executable File
275 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# bootstrap.sh — One-shot installer for the Zopu single-node execution plane.
|
|
#
|
|
# Run as root on a fresh Debian 12 host:
|
|
#
|
|
# bash bootstrap.sh
|
|
#
|
|
# Environment overrides (set before running):
|
|
# ZOPU_REPO_URL — SSH clone URL (default: ssh://git@git.openputer.com:2222/puter/zopu-code.git)
|
|
# ZOPU_REPO_BRANCH — branch to deploy (default: dogfood/v0)
|
|
# ZOPU_INSTALL_DIR — install path (default: /opt/zopu)
|
|
# ZOPU_SERVICE_USER — system user (default: zopu)
|
|
# TAILSCALE_AUTHKEY — if set, configure Tailscale
|
|
# TAILSCALE_HOSTNAME — Tailscale hostname (default: zopu-runtime)
|
|
#
|
|
# Installs: Docker Engine, Bun, clones the repo, runs bun install, builds the
|
|
# daemon and agent, creates a non-root service user, installs systemd units,
|
|
# and configures firewall/Tailscale defaults.
|
|
|
|
set -euo pipefail
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Configuration
|
|
# ---------------------------------------------------------------------------
|
|
REPO_URL="${ZOPU_REPO_URL:-ssh://git@git.openputer.com:2222/puter/zopu-code.git}"
|
|
REPO_BRANCH="${ZOPU_REPO_BRANCH:-dogfood/v0}"
|
|
INSTALL_DIR="${ZOPU_INSTALL_DIR:-/opt/zopu}"
|
|
SERVICE_USER="${ZOPU_SERVICE_USER:-zopu}"
|
|
DEPLOY_DIR="${INSTALL_DIR}/deploy/zopu-runtime"
|
|
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
RED='\033[0;31m'
|
|
NC='\033[0m'
|
|
|
|
log() { echo -e "${GREEN}[bootstrap]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[bootstrap]${NC} $*"; }
|
|
err() { echo -e "${RED}[bootstrap]${NC} $*" >&2; }
|
|
|
|
# runuser is part of util-linux (essential on Debian) and always available.
|
|
# sudo is NOT assumed on minimal Debian installs.
|
|
run_as_service() {
|
|
runuser -u "$SERVICE_USER" -- "$@"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pre-flight
|
|
# ---------------------------------------------------------------------------
|
|
if [[ "$EUID" -ne 0 ]]; then
|
|
err "This script must be run as root."
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -f /etc/debian_version ]]; then
|
|
log "Detected Debian $(cat /etc/debian_version)"
|
|
else
|
|
warn "This script targets Debian 12. Other distributions may need manual adjustments."
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. System packages
|
|
# ---------------------------------------------------------------------------
|
|
log "Updating apt and installing base packages..."
|
|
apt-get update -y
|
|
apt-get install -y \
|
|
ca-certificates \
|
|
curl \
|
|
gnupg \
|
|
ufw \
|
|
git \
|
|
jq \
|
|
netcat-openbsd \
|
|
openssh-client \
|
|
unattended-upgrades \
|
|
rsyslog
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Docker Engine
|
|
# ---------------------------------------------------------------------------
|
|
if ! command -v docker &>/dev/null; then
|
|
log "Installing Docker Engine..."
|
|
install -m 0755 -d /etc/apt/keyrings
|
|
curl -fsSL https://download.docker.com/linux/debian/gpg \
|
|
-o /etc/apt/keyrings/docker.asc
|
|
chmod a+r /etc/apt/keyrings/docker.asc
|
|
|
|
echo \
|
|
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
|
|
https://download.docker.com/linux/debian \
|
|
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
|
|
> /etc/apt/sources.list.d/docker.list
|
|
|
|
apt-get update -y
|
|
apt-get install -y \
|
|
docker-ce \
|
|
docker-ce-cli \
|
|
containerd.io \
|
|
docker-buildx-plugin \
|
|
docker-compose-plugin
|
|
else
|
|
log "Docker Engine already installed: $(docker --version)"
|
|
fi
|
|
|
|
systemctl enable --now docker
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Bun
|
|
# ---------------------------------------------------------------------------
|
|
if ! command -v bun &>/dev/null; then
|
|
log "Installing Bun..."
|
|
curl -fsSL https://bun.sh/install | bash
|
|
ln -sf /root/.bun/bin/bun /usr/local/bin/bun
|
|
else
|
|
log "Bun already installed: $(bun --version)"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Service user
|
|
# ---------------------------------------------------------------------------
|
|
if ! id "$SERVICE_USER" &>/dev/null; then
|
|
log "Creating service user: $SERVICE_USER"
|
|
useradd -r -m -d "/home/$SERVICE_USER" -s /bin/bash "$SERVICE_USER"
|
|
fi
|
|
|
|
if ! id -nG "$SERVICE_USER" | grep -qw docker; then
|
|
usermod -aG docker "$SERVICE_USER"
|
|
log "Added $SERVICE_USER to docker group"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. Clone or update repository
|
|
# ---------------------------------------------------------------------------
|
|
if [[ -d "$INSTALL_DIR/.git" ]]; then
|
|
log "Repository exists at $INSTALL_DIR, fetching latest..."
|
|
cd "$INSTALL_DIR"
|
|
git fetch origin
|
|
git checkout "$REPO_BRANCH"
|
|
git reset --hard "origin/$REPO_BRANCH"
|
|
else
|
|
log "Cloning $REPO_URL (branch $REPO_BRANCH) into $INSTALL_DIR..."
|
|
git clone --branch "$REPO_BRANCH" "$REPO_URL" "$INSTALL_DIR"
|
|
cd "$INSTALL_DIR"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5b. Hand ownership of the checkout to the service user
|
|
# ---------------------------------------------------------------------------
|
|
log "Setting ownership of $INSTALL_DIR to $SERVICE_USER..."
|
|
chown -R "$SERVICE_USER":"$SERVICE_USER" "$INSTALL_DIR"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. Install dependencies and build
|
|
# ---------------------------------------------------------------------------
|
|
log "Running bun install..."
|
|
run_as_service bun install
|
|
|
|
log "Building daemon binary..."
|
|
run_as_service bun run build:daemon
|
|
|
|
log "Building agent service..."
|
|
run_as_service bun run build:agents
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 7. Environment file
|
|
# ---------------------------------------------------------------------------
|
|
ENV_FILE="$INSTALL_DIR/.env"
|
|
if [[ ! -f "$ENV_FILE" ]]; then
|
|
log "Copying .env.template to .env — EDIT BEFORE STARTING SERVICES"
|
|
cp "$DEPLOY_DIR/.env.template" "$ENV_FILE"
|
|
chown "$SERVICE_USER":"$SERVICE_USER" "$ENV_FILE"
|
|
chmod 600 "$ENV_FILE"
|
|
warn "Edit $ENV_FILE with real values before starting services."
|
|
else
|
|
log ".env already exists at $ENV_FILE"
|
|
# Ensure correct ownership and permissions on existing .env
|
|
chown "$SERVICE_USER":"$SERVICE_USER" "$ENV_FILE"
|
|
chmod 600 "$ENV_FILE"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 8. Persistent log directory
|
|
# ---------------------------------------------------------------------------
|
|
LOG_DIR="/var/log/zopu"
|
|
mkdir -p "$LOG_DIR"
|
|
chown "$SERVICE_USER":"$SERVICE_USER" "$LOG_DIR"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 9. Install systemd units (substitute placeholders)
|
|
# ---------------------------------------------------------------------------
|
|
log "Installing systemd units..."
|
|
for unit in zopu-daemon.service zopu-agent.service \
|
|
zopu-health.timer zopu-health.service \
|
|
zopu-docker-cleanup.timer zopu-docker-cleanup.service; do
|
|
SRC="$DEPLOY_DIR/systemd/$unit"
|
|
DST="/etc/systemd/system/$unit"
|
|
if [[ -f "$SRC" ]]; then
|
|
sed \
|
|
-e "s|__INSTALL_DIR__|$INSTALL_DIR|g" \
|
|
-e "s|__SERVICE_USER__|$SERVICE_USER|g" \
|
|
"$SRC" > "$DST"
|
|
log " installed $unit"
|
|
fi
|
|
done
|
|
systemctl daemon-reload
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 10. Firewall (deny-by-default, explicit allow for Tailscale)
|
|
# ---------------------------------------------------------------------------
|
|
log "Configuring firewall..."
|
|
if ! ufw status 2>/dev/null | grep -q "Status: active"; then
|
|
ufw allow 22/tcp
|
|
ufw default deny incoming
|
|
ufw default allow outgoing
|
|
|
|
# Allow all traffic on the Tailscale interface (if present)
|
|
# This lets the agent and engine ports be reached over the private overlay.
|
|
ufw allow in on tailscale0 || warn "tailscale0 not present yet; rule will activate when interface appears"
|
|
|
|
ufw --force enable
|
|
log "Firewall enabled: SSH (22) allowed, tailscale0 allowed."
|
|
warn "Agent and RivetKit ports are NOT exposed on public interfaces."
|
|
warn "Reachability is via Tailscale (tailscale0) only."
|
|
else
|
|
log "Firewall already active. Ensuring tailscale0 rule..."
|
|
ufw allow in on tailscale0 2>/dev/null || true
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 11. Tailscale (optional)
|
|
# ---------------------------------------------------------------------------
|
|
if [[ -n "${TAILSCALE_AUTHKEY:-}" ]]; then
|
|
log "Installing and configuring Tailscale..."
|
|
if ! command -v tailscaled &>/dev/null; then
|
|
curl -fsSL https://tailscale.com/install.sh | sh
|
|
fi
|
|
tailscale up --authkey "$TAILSCALE_AUTHKEY" \
|
|
--hostname "${TAILSCALE_HOSTNAME:-zopu-runtime}" \
|
|
--accept-routes
|
|
log "Tailscale configured: $(tailscale ip -4 2>/dev/null || echo 'waiting for IP')"
|
|
|
|
# Re-apply the tailscale0 firewall rule now that the interface exists
|
|
ufw allow in on tailscale0 2>/dev/null || true
|
|
else
|
|
warn "TAILSCALE_AUTHKEY not set — skipping Tailscale setup."
|
|
warn "Without Tailscale, services are reachable only via localhost."
|
|
warn "To use a private network interface, add an explicit UFW rule:"
|
|
warn " ufw allow in on <interface>"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 12. Disk-space monitoring cron
|
|
# /etc/cron.d format REQUIRES a username field.
|
|
# ---------------------------------------------------------------------------
|
|
log "Installing disk-space monitor (daily at 06:00)..."
|
|
CRON_LINE="0 6 * * * ${SERVICE_USER} ${DEPLOY_DIR}/scripts/disk-monitor.sh --warn-percent 80 >> /var/log/zopu/disk-monitor.log 2>&1"
|
|
echo "$CRON_LINE" > /etc/cron.d/zopu-disk-monitor
|
|
chmod 644 /etc/cron.d/zopu-disk-monitor
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Done
|
|
# ---------------------------------------------------------------------------
|
|
log "Bootstrap complete."
|
|
echo ""
|
|
echo "Next steps:"
|
|
echo " 1. Edit $ENV_FILE with real values"
|
|
echo " 2. Start services:"
|
|
echo " systemctl start zopu-daemon zopu-agent"
|
|
echo " 3. Enable health monitoring:"
|
|
echo " systemctl enable --now zopu-health.timer zopu-docker-cleanup.timer"
|
|
echo " 4. Verify health:"
|
|
echo " $DEPLOY_DIR/scripts/health-check.sh"
|
|
echo ""
|
|
warn "Services are NOT started automatically. Edit .env first."
|