#!/usr/bin/env bash # RosDiff on an Oracle Cloud "Always Free" Ubuntu machine (Ampere A1 / ARM, or x86). See deploy/oracle/README.md. # # Run it from the RosDiff folder on the machine: bash deploy/oracle/setup.sh # It is safe to run again (after copying an updated RosDiff over): it only does what is still missing, then # restarts the service. # # What it sets up: # - system packages, Python venv with RosDiff's requirements, Node.js + OpenCode (free models) # - the documentation search index (python -m rag.ingest), built once # - a systemd service that keeps RosDiff running and restarts it after crashes and reboots # - Caddy in front of it with a free HTTPS certificate: https://.sslip.io # (or your own domain: DOMAIN=rosdiff.example.com bash deploy/oracle/setup.sh) # - the machine's firewall opened for ports 80/443 (Oracle's Ubuntu images block them by default) # - a daily backup of the database (testers' work, budgets, feedback), 14 days kept set -euo pipefail APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" RUN_USER="$(id -un)" cd "$APP_DIR" say() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; } warn() { printf '\033[1;33m!! %s\033[0m\n' "$*"; } [[ $EUID -eq 0 ]] && { echo "Run as your user, not root (the script uses sudo where needed)"; exit 1; } [[ -f api/main.py ]] || { echo "Run this from the RosDiff folder (api/main.py not found)"; exit 1; } [[ -f .env ]] || { echo "Copy your .env into $APP_DIR first (see deploy/oracle/README.md, step 5)"; exit 1; } [[ -f testers.yaml ]] || warn "testers.yaml not found: nobody can sign in until you add testers (python -m api.keys add )" say "System packages" sudo apt-get update -qq sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ python3 python3-venv python3-pip python3-dev build-essential git curl sqlite3 nodejs npm iptables-persistent libgl1 \ debian-keyring debian-archive-keyring apt-transport-https gnupg >/dev/null if ! command -v caddy >/dev/null; then if ! sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq caddy >/dev/null 2>&1; then # not in this Ubuntu release's archive: use Caddy's official repository curl -1sLf https://dl.cloudsmith.io/public/caddy/stable/gpg.key | sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt | sudo tee /etc/apt/sources.list.d/caddy-stable.list >/dev/null sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq caddy >/dev/null fi fi say "Python environment" [[ -d .venv ]] || python3 -m venv .venv .venv/bin/pip install -q --upgrade pip .venv/bin/pip install -q -r requirements.txt say "OpenCode (free prompt enhancement and free debugging)" if ! command -v opencode >/dev/null; then sudo npm install -g --silent opencode-ai || warn "OpenCode did not install; RosDiff still works, on paid models only" fi command -v opencode >/dev/null && opencode --version || true say "Documentation search index" if [[ ! -d data/chroma || ! -f data/ros2_interfaces.json ]]; then set -a; source .env; set +a .venv/bin/python -m rag.ingest # clones MuJoCo/ROS 2 docs and the robot models; a few minutes else echo "already built (delete data/chroma to rebuild)" fi say "Public address" if [[ -z "${DOMAIN:-}" ]]; then IP="$(curl -fsS --max-time 10 https://api.ipify.org || curl -fsS --max-time 10 https://ifconfig.me)" DOMAIN="${IP//./-}.sslip.io" fi URL="https://$DOMAIN" echo "$URL" # the app puts this address in report links if grep -q '^PUBLIC_BASE_URL=' .env; then existing="$(grep '^PUBLIC_BASE_URL=' .env | head -1 | cut -d= -f2-)" if [[ -z "$existing" || "$existing" == *sslip.io* ]]; then sed -i "s|^PUBLIC_BASE_URL=.*|PUBLIC_BASE_URL=$URL|" .env fi else printf '\nPUBLIC_BASE_URL=%s\n' "$URL" >> .env fi grep -q '^REPORT_SECRET=..' .env || { sed -i '/^REPORT_SECRET=/d' .env printf 'REPORT_SECRET=%s\n' "$(openssl rand -hex 32)" >> .env } chmod 600 .env say "RosDiff service" sudo tee /etc/systemd/system/rosdiff.service >/dev/null </dev/null sudo systemctl restart rosdiff say "HTTPS (Caddy)" sudo tee /etc/caddy/Caddyfile >/dev/null </dev/null sudo systemctl reload caddy || sudo systemctl restart caddy say "Firewall: allow 80 and 443" for port in 80 443; do if ! sudo iptables -C INPUT -p tcp --dport "$port" -m state --state NEW -j ACCEPT 2>/dev/null; then # insert above Oracle's catch-all REJECT rule pos="$(sudo iptables -L INPUT --line-numbers -n | awk '/REJECT/ {print $1; exit}')" sudo iptables -I INPUT "${pos:-1}" -p tcp --dport "$port" -m state --state NEW -j ACCEPT fi done sudo netfilter-persistent save >/dev/null say "Daily database backup" mkdir -p "$HOME/backups" DB="$APP_DIR/data/generations.sqlite3" CRON="15 3 * * * [ -f $DB ] && sqlite3 $DB \".backup '$HOME/backups/generations-\$(date +\\%F).sqlite3'\" && find $HOME/backups -name 'generations-*' -mtime +14 -delete" ( { crontab -l 2>/dev/null || true; } | { grep -v 'generations-' || true; } ; echo "$CRON" ) | crontab - say "Checking" for i in $(seq 1 30); do curl -fsS localhost:8000/health >/dev/null 2>&1 && break sleep 2 done if curl -fsS localhost:8000/health >/dev/null 2>&1; then curl -fsS localhost:8000/health | .venv/bin/python -c 'import json,sys; h=json.load(sys.stdin); print(json.dumps({k: h.get(k) for k in ("status","llm_configured","runs_enabled","prompt_enhance","index")}, indent=1))' else warn "RosDiff is not answering yet; see: journalctl -u rosdiff -n 50" fi echo echo "Done. Testers open: $URL" echo "(The HTTPS certificate can take a minute the first time. If the page doesn't load, check step 3 of the guide:" echo " ports 80 and 443 must be open in the Oracle security list.)"