#!/bin/bash # Ryvion Node Linux Installer # Universal install: curl -sSL https://api.ryvion.ai/install.sh | bash # Native-only mode: curl -sSL 'https://api.ryvion.ai/install.sh?mode=native' | bash set -euo pipefail VERSION=${VERSION:-"latest"} HUB_URL=${HUB_URL:-"https://ryvion-hub.fly.dev"} INSTALL_DIR="/opt/ryvion" CONFIG_DIR="$HOME/.ryvion" NODE_KEY_PATH="$CONFIG_DIR/node-key" SERVICE_NAME="ryvion-node" BIND_TOKEN="" UI_PORT="45890" NATIVE_ONLY="0" RUNTIME_CHANNEL="managed_oci_v1" RUNTIME_VERSION="2026.04.29.2" RUNTIME_PROVIDER="oci_linux_adapter" RUNTIME_MODE="host_package" RUNTIME_SOURCE="ryvion_runtime_kit" RUNTIME_ARTIFACT="ryvion-runtime-kit-linux-amd64-2026.04.29.2.tar.gz" RUNTIME_BINARY="/opt/ryvion/runtime/ryvion-runtime" RUNTIME_BACKEND_BINARY="/opt/ryvion/runtime/backend/ryvion-oci" RUNTIME_ENGINE_BINARY="" RUNTIME_ENGINE_KIND="" RUNTIME_MANIFEST_HASH="035b27209cc2b31740d3085c458951fc3d81da95df1f1bafd2af0e1775f3b205" RUNTIME_ARTIFACT_URL="https://ryvion-hub.fly.dev/download/runtime/linux/kit.tar.gz" RUNTIME_CHECKSUM_URL="https://ryvion-hub.fly.dev/download/runtime/linux/kit.tar.gz.sha256" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' HAD_EXISTING_NODE_IDENTITY=0 allow_docker_fallback() { case "${RYV_ALLOW_DOCKER_FALLBACK:-}" in 1|true|TRUE|yes|YES|on|ON) return 0 ;; *) return 1 ;; esac } detect_oci_engine_binary() { if [[ -n "${RYV_RUNTIME_ENGINE_BINARY:-}" && -x "${RYV_RUNTIME_ENGINE_BINARY}" ]]; then printf '%s\n' "${RYV_RUNTIME_ENGINE_BINARY}" return 0 fi for candidate in nerdctl podman; do if command -v "$candidate" >/dev/null 2>&1; then command -v "$candidate" return 0 fi done for candidate in /usr/local/bin/nerdctl /usr/local/bin/podman /usr/bin/nerdctl /usr/bin/podman /snap/bin/nerdctl /snap/bin/podman; do if [[ -x "$candidate" ]]; then printf '%s\n' "$candidate" return 0 fi done if allow_docker_fallback; then if command -v docker >/dev/null 2>&1; then command -v docker return 0 fi for candidate in /usr/local/bin/docker /usr/bin/docker /snap/bin/docker; do if [[ -x "$candidate" ]]; then printf '%s\n' "$candidate" return 0 fi done fi return 1 } detect_oci_engine_kind() { local engine_cli="${1:-}" if [[ -z "$engine_cli" ]]; then engine_cli="$(detect_oci_engine_binary || true)" fi if [[ -z "$engine_cli" ]]; then return 1 fi local engine_name engine_name="$(basename "$engine_cli")" engine_name="${engine_name%.*}" case "$engine_name" in podman*) printf '%s\n' "podman" ;; docker*) printf '%s\n' "docker" ;; nerdctl*) printf '%s\n' "nerdctl" ;; *) printf '%s\n' "unknown" ;; esac } echo -e "${CYAN}🚀 Ryvion Node Linux Installer${NC}" echo -e "${CYAN}====================================${NC}" echo "" ensure_managed_runtime() { echo -e "${YELLOW}ðŸ§ą Provisioning managed execution runtime...${NC}" if [[ -x "$RUNTIME_BACKEND_BINARY" ]] && "$RUNTIME_BACKEND_BINARY" info >/dev/null 2>&1; then echo -e "${GREEN}✅ Managed execution runtime already ready${NC}" else echo -e "${BLUE}â„đïļ Applying runtime kit artifact ${RUNTIME_ARTIFACT}${NC}" TMP_RUNTIME_DIR=$(mktemp -d) if command -v curl >/dev/null 2>&1; then curl -fsSL "${RUNTIME_ARTIFACT_URL}" -o "${TMP_RUNTIME_DIR}/${RUNTIME_ARTIFACT}" curl -fsSL "${RUNTIME_CHECKSUM_URL}" -o "${TMP_RUNTIME_DIR}/${RUNTIME_ARTIFACT}.sha256" elif command -v wget >/dev/null 2>&1; then wget -qO "${TMP_RUNTIME_DIR}/${RUNTIME_ARTIFACT}" "${RUNTIME_ARTIFACT_URL}" wget -qO "${TMP_RUNTIME_DIR}/${RUNTIME_ARTIFACT}.sha256" "${RUNTIME_CHECKSUM_URL}" else rm -rf "${TMP_RUNTIME_DIR}" echo -e "${RED}❌ curl or wget is required to reach the managed runtime kit.${NC}" exit 1 fi EXPECTED_CHECKSUM=$(awk '{print $1}' "${TMP_RUNTIME_DIR}/${RUNTIME_ARTIFACT}.sha256") if command -v sha256sum >/dev/null 2>&1; then ACTUAL_CHECKSUM=$(sha256sum "${TMP_RUNTIME_DIR}/${RUNTIME_ARTIFACT}" | awk '{print $1}') elif command -v shasum >/dev/null 2>&1; then ACTUAL_CHECKSUM=$(shasum -a 256 "${TMP_RUNTIME_DIR}/${RUNTIME_ARTIFACT}" | awk '{print $1}') else rm -rf "${TMP_RUNTIME_DIR}" echo -e "${RED}❌ sha256sum or shasum is required to verify the managed runtime kit.${NC}" exit 1 fi if [[ "${ACTUAL_CHECKSUM}" != "${EXPECTED_CHECKSUM}" ]]; then rm -rf "${TMP_RUNTIME_DIR}" echo -e "${RED}❌ Managed runtime kit checksum mismatch.${NC}" exit 1 fi tar -xzf "${TMP_RUNTIME_DIR}/${RUNTIME_ARTIFACT}" -C "${TMP_RUNTIME_DIR}" bash "${TMP_RUNTIME_DIR}/bootstrap.sh" rm -rf "${TMP_RUNTIME_DIR}" fi RUNTIME_ENGINE_BINARY="$(detect_oci_engine_binary || true)" RUNTIME_ENGINE_KIND="$(detect_oci_engine_kind "$RUNTIME_ENGINE_BINARY" || true)" } # Check if running as root if [[ $EUID -eq 0 ]]; then echo -e "${RED}❌ This script should not be run as root for security reasons${NC}" echo -e "${YELLOW}Please run as a regular user. The script will ask for sudo when needed.${NC}" exit 1 fi echo -e "${YELLOW}🔐 Validating administrator access...${NC}" if ! sudo -v; then echo -e "${RED}❌ Could not acquire sudo access. No changes were applied.${NC}" exit 1 fi # Detect architecture ARCH=$(uname -m) case $ARCH in x86_64) BINARY_ARCH="amd64" ;; aarch64|arm64) BINARY_ARCH="arm64" ;; *) echo -e "${RED}❌ Unsupported architecture: $ARCH${NC}"; exit 1 ;; esac echo -e "${BLUE}â„đïļ Detected architecture: $ARCH${NC}" if [ -f "$NODE_KEY_PATH" ]; then HAD_EXISTING_NODE_IDENTITY=1 echo -e "${BLUE}â„đïļ Existing node identity detected; update will reuse it.${NC}" fi # Check system requirements echo -e "${YELLOW}🔍 Checking system requirements...${NC}" # Check memory MEMORY_KB=$(grep MemTotal /proc/meminfo 2>/dev/null | awk '{print $2}' || echo "4194304") MEMORY_GB=$((MEMORY_KB / 1024 / 1024)) if [[ $MEMORY_GB -lt 4 ]]; then echo -e "${YELLOW}⚠ïļ Low memory detected: ${MEMORY_GB}GB (minimum 4GB recommended)${NC}" else echo -e "${GREEN}✅ Memory check passed: ${MEMORY_GB}GB${NC}" fi # Check for GPU if command -v nvidia-smi &> /dev/null; then GPU_INFO=$(nvidia-smi --query-gpu=name,memory.total --format=csv,noheader,nounits | head -1) echo -e "${GREEN}ðŸŽŪ NVIDIA GPU detected: $GPU_INFO${NC}" elif command -v rocm-smi &> /dev/null; then echo -e "${GREEN}ðŸŽŪ AMD GPU detected${NC}" else echo -e "${BLUE}ðŸ’ŧ No dedicated GPU detected (CPU-only mode)${NC}" fi if [[ "$NATIVE_ONLY" == "1" ]]; then echo -e "${GREEN}ðŸŠķ Native mode: skipping managed container runtime (no Docker/Podman required)${NC}" else ensure_managed_runtime fi # Create directories echo -e "${YELLOW}📁 Creating directories...${NC}" sudo mkdir -p "$INSTALL_DIR" mkdir -p "$CONFIG_DIR" # Download and install binary echo -e "${YELLOW}ðŸ“Ĩ Downloading Ryvion Node binary...${NC}" if [ "$BINARY_ARCH" = "arm64" ]; then DOWNLOAD_URL="${HUB_URL}/download/linux/arm64" else DOWNLOAD_URL="${HUB_URL}/download/linux/binary" fi if command -v curl &> /dev/null; then curl -fSL "$DOWNLOAD_URL" | sudo tar -xz -C "$INSTALL_DIR" elif command -v wget &> /dev/null; then wget -O- "$DOWNLOAD_URL" | sudo tar -xz -C "$INSTALL_DIR" else echo -e "${RED}❌ Neither curl nor wget found. Please install one of them.${NC}" exit 1 fi sudo chmod +x "$INSTALL_DIR/ryvion-node" sudo ln -sf "$INSTALL_DIR/ryvion-node" /usr/local/bin/ryvion-node echo -e "${GREEN}✅ Binary installed successfully${NC}" # Create configuration echo -e "${YELLOW}⚙ïļ Creating configuration...${NC}" CONFIG_FILE="$CONFIG_DIR/config.json" if command -v python3 >/dev/null 2>&1; then python3 - "$CONFIG_FILE" "$HUB_URL" "$CONFIG_DIR/node.log" "$BIND_TOKEN" "$RUNTIME_CHANNEL" "$RUNTIME_VERSION" "$RUNTIME_PROVIDER" "$RUNTIME_MODE" "$RUNTIME_SOURCE" "$RUNTIME_ARTIFACT" "$RUNTIME_BINARY" "$RUNTIME_BACKEND_BINARY" "$RUNTIME_ENGINE_BINARY" "$RUNTIME_ENGINE_KIND" "$RUNTIME_MANIFEST_HASH" <<'PY' import json import os import sys path, hub, log_file, bind_token, runtime_channel, runtime_version, runtime_provider, runtime_mode, runtime_source, runtime_artifact, runtime_binary, runtime_backend_binary, runtime_engine_binary, runtime_engine_kind, runtime_manifest_hash = sys.argv[1:] data = {} if os.path.exists(path): try: with open(path, "r", encoding="utf-8") as handle: loaded = json.load(handle) if isinstance(loaded, dict): data.update(loaded) except Exception: pass data["hub_url"] = hub data["device_type"] = "auto" data["log_level"] = "info" data["log_file"] = log_file data["runtime_channel"] = runtime_channel data["runtime_channel_version"] = runtime_version data["runtime_provider"] = runtime_provider data["runtime_mode"] = runtime_mode data["runtime_source"] = runtime_source data["runtime_artifact"] = runtime_artifact data["runtime_binary"] = runtime_binary data["runtime_backend_binary"] = runtime_backend_binary data["runtime_engine_binary"] = runtime_engine_binary data["runtime_engine_kind"] = runtime_engine_kind data["runtime_manifest_hash"] = runtime_manifest_hash if bind_token: data["bind_token"] = bind_token with open(path, "w", encoding="utf-8") as handle: json.dump(data, handle, indent=2) handle.write("\n") PY else if [[ -n "$BIND_TOKEN" ]]; then cat > "$CONFIG_FILE" << EOF { "hub_url": "$HUB_URL", "device_type": "auto", "log_level": "info", "log_file": "$CONFIG_DIR/node.log", "bind_token": "$BIND_TOKEN", "runtime_channel": "$RUNTIME_CHANNEL", "runtime_channel_version": "$RUNTIME_VERSION", "runtime_provider": "$RUNTIME_PROVIDER", "runtime_mode": "$RUNTIME_MODE", "runtime_source": "$RUNTIME_SOURCE", "runtime_artifact": "$RUNTIME_ARTIFACT", "runtime_binary": "$RUNTIME_BINARY", "runtime_backend_binary": "$RUNTIME_BACKEND_BINARY", "runtime_engine_binary": "$RUNTIME_ENGINE_BINARY", "runtime_engine_kind": "$RUNTIME_ENGINE_KIND", "runtime_manifest_hash": "$RUNTIME_MANIFEST_HASH" } EOF else cat > "$CONFIG_FILE" << EOF { "hub_url": "$HUB_URL", "device_type": "auto", "log_level": "info", "log_file": "$CONFIG_DIR/node.log", "runtime_channel": "$RUNTIME_CHANNEL", "runtime_channel_version": "$RUNTIME_VERSION", "runtime_provider": "$RUNTIME_PROVIDER", "runtime_mode": "$RUNTIME_MODE", "runtime_source": "$RUNTIME_SOURCE", "runtime_artifact": "$RUNTIME_ARTIFACT", "runtime_binary": "$RUNTIME_BINARY", "runtime_backend_binary": "$RUNTIME_BACKEND_BINARY", "runtime_engine_binary": "$RUNTIME_ENGINE_BINARY", "runtime_engine_kind": "$RUNTIME_ENGINE_KIND", "runtime_manifest_hash": "$RUNTIME_MANIFEST_HASH" } EOF fi fi BIND_ENV="" if [[ -n "$BIND_TOKEN" ]]; then BIND_ENV="Environment=RYV_BIND_TOKEN=$BIND_TOKEN" fi NATIVE_ENV="" if [[ "$NATIVE_ONLY" == "1" ]]; then NATIVE_ENV="Environment=RYV_DISABLE_OCI=1" fi RUNTIME_GROUP_LINE="" if [[ "$RUNTIME_ENGINE_KIND" == "docker" ]]; then RUNTIME_GROUP_LINE="SupplementaryGroups=docker" fi # Install systemd service echo -e "${YELLOW}🔧 Installing systemd service...${NC}" sudo tee "/etc/systemd/system/$SERVICE_NAME.service" > /dev/null << EOF [Unit] Description=Ryvion Node After=network.target Wants=network.target [Service] Type=simple User=$USER $RUNTIME_GROUP_LINE WorkingDirectory=$CONFIG_DIR Environment=RYV_HUB_URL=$HUB_URL Environment=RYV_UI_PORT=$UI_PORT Environment=RYV_RUNTIME_CHANNEL=$RUNTIME_CHANNEL Environment=RYV_RUNTIME_CHANNEL_VERSION=$RUNTIME_VERSION Environment=RYV_RUNTIME_PROVIDER=$RUNTIME_PROVIDER Environment=RYV_RUNTIME_MODE=$RUNTIME_MODE Environment=RYV_RUNTIME_SOURCE=$RUNTIME_SOURCE Environment=RYV_RUNTIME_ARTIFACT=$RUNTIME_ARTIFACT Environment=RYV_RUNTIME_BINARY=$RUNTIME_BINARY Environment=RYV_RUNTIME_BACKEND_BINARY=$RUNTIME_BACKEND_BINARY Environment=RYV_RUNTIME_ENGINE_BINARY=$RUNTIME_ENGINE_BINARY Environment=RYV_RUNTIME_ENGINE_KIND=$RUNTIME_ENGINE_KIND Environment=RYV_RUNTIME_MANIFEST_HASH=$RUNTIME_MANIFEST_HASH $BIND_ENV $NATIVE_ENV ExecStart=$INSTALL_DIR/ryvion-node Restart=always RestartSec=10 StandardOutput=journal StandardError=journal SyslogIdentifier=ryvion-node # Resource limits LimitNOFILE=65536 MemoryMax=8G [Install] WantedBy=multi-user.target EOF # Enable and start service sudo systemctl daemon-reload sudo systemctl enable "$SERVICE_NAME" sudo systemctl start "$SERVICE_NAME" sleep 3 if systemctl is-active --quiet "$SERVICE_NAME"; then echo -e "${GREEN}🎉 Ryvion Node started successfully!${NC}" else echo -e "${YELLOW}⚠ïļ Service may have issues. Check logs with: journalctl -u $SERVICE_NAME -f${NC}" fi if command -v curl >/dev/null 2>&1; then for _ in 1 2 3 4 5; do if curl -fsS "http://127.0.0.1:$UI_PORT/healthz" >/dev/null 2>&1; then echo -e "${GREEN}✅ Local operator API is reachable on http://127.0.0.1:$UI_PORT${NC}" break fi sleep 1 done fi NODE_IDENTITY="$("$INSTALL_DIR/ryvion-node" identity --short 16 2>/dev/null || true)" if [ -n "$NODE_IDENTITY" ]; then if [ "$HAD_EXISTING_NODE_IDENTITY" = "1" ]; then echo -e "${GREEN}✅ Reusing node identity: $NODE_IDENTITY${NC}" else echo -e "${GREEN}✅ Created node identity: $NODE_IDENTITY${NC}" fi fi # Create management script echo -e "${YELLOW}📝 Creating management script...${NC}" sudo tee "/usr/local/bin/ryvion-ctl" > /dev/null << 'EOF' #!/bin/bash # Ryvion Node Control Script SERVICE_NAME="ryvion-node" case "$1" in start) echo "🚀 Starting Ryvion node..." sudo systemctl start "$SERVICE_NAME" ;; stop) echo "🛑 Stopping Ryvion node..." sudo systemctl stop "$SERVICE_NAME" ;; restart) echo "🔄 Restarting Ryvion node..." sudo systemctl restart "$SERVICE_NAME" ;; status) systemctl status "$SERVICE_NAME" ;; logs) journalctl -u "$SERVICE_NAME" -f ;; update) echo "⮆ïļ Updating Ryvion node..." curl -sSL https://api.ryvion.ai/install.sh | bash ;; *) echo "Usage: $0 {start|stop|restart|status|logs|update}" exit 1 ;; esac EOF sudo chmod +x "/usr/local/bin/ryvion-ctl" echo "" echo -e "${CYAN}========================================${NC}" echo -e "${CYAN}🎉 Ryvion Node Installation Complete!${NC}" echo -e "${CYAN}========================================${NC}" echo "" echo -e "${YELLOW}Management commands:${NC}" echo -e " ${BLUE}ryvion-ctl start${NC} - Start the node" echo -e " ${BLUE}ryvion-ctl stop${NC} - Stop the node" echo -e " ${BLUE}ryvion-ctl status${NC} - Check node status" echo -e " ${BLUE}ryvion-ctl logs${NC} - View node logs" echo -e " ${BLUE}ryvion-ctl update${NC} - Update to latest version" echo "" echo -e "${YELLOW}Next steps:${NC}" echo -e "1. Node will automatically detect your hardware and start serving work" echo -e "2. Monitor your node: ${BLUE}ryvion-ctl status${NC}" echo -e "3. View earnings at: ${BLUE}https://ryvion.ai/app/operator/nodes${NC}" echo -e "4. Join our community: ${BLUE}https://discord.gg/ryvion${NC}" echo "" echo -e "${GREEN}âœĻ Your Linux machine is now part of the Ryvion operator network!${NC}"