#!/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 " 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."