#!/bin/sh # RapidGTO core one-line installer, served by the rapidgto-api Worker at # https://get.rapidgto.com (root; /install.sh is an alias). Usage: # # curl -fsSL https://get.rapidgto.com | sudo sh # # Installs a single RapidGTO solver core on this host: validates your # account token, downloads the core image from R2 (entitlement-checked # presigned URL — no registry), writes /opt/rapidgto/{compose.yaml,.env}, # and brings the core up via Docker Compose. The core dials the relay # OUTBOUND (no inbound ports) and pairs with your panel by the account # token you enter below. Re-running on a host that already has a core # offers update / reconfigure / uninstall — the same one-liner is the # whole lifecycle, so there is no hand-typed `docker rm` to get wrong (a # bare rm would kill a running solve before it checkpoints). # # Names match the manual flow (container rapidgto-core, volume # rapidgto-data), so a machine that installed by hand upgrades in place # and keeps its solves + core identity. set -eu API_URL="https://api.rapidgto.com" PANEL_URL="https://app.rapidgto.com" INSTALL_DIR="/opt/rapidgto" err() { echo "Error: $*" >&2; exit 1; } # --- prerequisites ----------------------------------------------------------- [ "$(id -u)" -eq 0 ] || err "run as root, e.g. curl -fsSL https://get.rapidgto.com | sudo sh" # We prompt on the terminal (token, consents) since stdin is the curl # pipe. No tty = no way to ask, so bail early with guidance. [ -r /dev/tty ] || err "no terminal to prompt on; download the script and run it directly instead of piping." # The core image is x86_64-only. Fail fast in plain language before the # user installs Docker or types a token (ARM is what the cheapest cloud # tiers hand out, and the docker error for it is cryptic). arch=$(uname -m) case "$arch" in x86_64 | amd64) : ;; *) err "RapidGTO needs an x86_64 (Intel/AMD) server; yours is $arch (e.g. ARM). It cannot run here." ;; esac # --- existing install → update / reconfigure / uninstall --------------------- # Default is a non-destructive update: the token and the core's paired # identity (in the data volume) are never silently rewritten. MODE=install if [ -e "$INSTALL_DIR/.env" ]; then printf 'Found an existing RapidGTO core at %s.\n [U] Update — get the latest core, keep your current token (default)\n [R] Reconfigure — enter a new account token or core name\n [X] Uninstall — stop and remove this core\n [Q] Quit\nChoose [U/r/x/q]: ' "$INSTALL_DIR" > /dev/tty IFS= read -r reply < /dev/tty || err "could not read a reply." case "$reply" in [Rr]*) MODE=reconfigure ;; [Xx]*) MODE=uninstall ;; [Qq]*) err "left the existing install untouched." ;; *) MODE=update ;; esac fi # --- uninstall --------------------------------------------------------------- # Before the Docker/token machinery: an uninstall must work even with an # expired trial, and needs nothing from the API. if [ "$MODE" = uninstall ]; then command -v docker >/dev/null 2>&1 || err "Docker is gone, so nothing can be removed cleanly. Delete $INSTALL_DIR by hand." [ -f "$INSTALL_DIR/compose.yaml" ] || \ err "$INSTALL_DIR/compose.yaml is missing, so the core cannot be stopped from here. Re-run and choose Update to restore it, then uninstall." cd "$INSTALL_DIR" core_image=$(docker compose images -q core 2>/dev/null || true) # A running solve checkpoints on shutdown; compose down honours the # compose file's stop_grace_period (5m) so that completes and the # data volume stays resumable. Never shorten this. echo "Stopping the core (waiting for it to checkpoint a running solve, this can take a few minutes)..." docker compose down --remove-orphans # The data volume is the irreversible part: artifacts, packs/tiers, # checkpoints, the core's paired identity. Keeping it is the default. if docker volume inspect rapidgto-data >/dev/null 2>&1; then mount=$(docker volume inspect rapidgto-data --format '{{.Mountpoint}}' 2>/dev/null || true) size=$([ -n "$mount" ] && [ -d "$mount" ] && du -sh "$mount" 2>/dev/null | cut -f1 || true) printf 'Also delete the solver data volume "rapidgto-data"%s? Solved games, packs/tiers, checkpoints and this core'\''s paired identity. This cannot be undone. [y/N] ' \ "${size:+ ($size)}" > /dev/tty IFS= read -r reply < /dev/tty || err "could not read a reply." case "$reply" in [Yy]*) if docker volume rm rapidgto-data >/dev/null 2>&1; then echo "Deleted the data volume." else echo "Could not delete the data volume 'rapidgto-data' (something else may still use it)."; fi ;; *) KEPT_VOLUME=1 ;; esac fi [ -n "$core_image" ] && docker image rm "$core_image" >/dev/null 2>&1 || true cd / rm -rf "$INSTALL_DIR" echo echo "Done. The RapidGTO core is uninstalled; its card in your panel goes offline (remove it there if you like)." if [ "${KEPT_VOLUME:-}" = 1 ]; then echo "Kept your solver data in the Docker volume 'rapidgto-data'. Reinstalling picks it" echo "up again (same core identity); to delete it later: docker volume rm rapidgto-data" fi echo "Reinstall any time with the same command from $PANEL_URL/install" exit 0 fi # --- Docker ------------------------------------------------------------------ # Docker Engine + Compose v2. If missing, offer Docker's official # convenience script — it covers the mainstream server distros and # installs the compose plugin too. Ask first (it adds a repo + a root # daemon) but default to yes so a plain `curl … | sh` stays one Enter. if ! command -v docker >/dev/null 2>&1; then printf 'Docker is not installed. Install it now via get.docker.com? [Y/n] ' > /dev/tty IFS= read -r reply < /dev/tty || err "could not read a reply." case "$reply" in [Nn]*) err "Docker is required. Install it (https://docs.docker.com/engine/install/) and re-run." ;; *) echo "Installing Docker via get.docker.com..." curl -fsSL https://get.docker.com | sh || err "Docker install failed; install it manually and re-run." ;; esac fi docker compose version >/dev/null 2>&1 || \ err "Docker Compose v2 plugin is missing; install it (https://docs.docker.com/compose/install/) and re-run." # --- account token ----------------------------------------------------------- if [ "$MODE" = update ]; then # An update reuses the stored credentials — never re-prompts. ACCOUNT_TOKEN=$(sed -n 's/^CORE_TOKEN=//p' "$INSTALL_DIR/.env") [ -n "$ACCOUNT_TOKEN" ] || err "no CORE_TOKEN in $INSTALL_DIR/.env; re-run and choose Reconfigure." else # The token pairs this core with your panel (shown once on the # install page; regenerate there if it ever leaks). printf 'Enter your RapidGTO account token (rgto_..., from %s/install): ' "$PANEL_URL" > /dev/tty IFS= read -r ACCOUNT_TOKEN < /dev/tty || err "could not read the account token." ACCOUNT_TOKEN=$(printf '%s' "$ACCOUNT_TOKEN" | tr -d '[:space:]') [ -n "$ACCOUNT_TOKEN" ] || err "account token must not be empty." case "$ACCOUNT_TOKEN" in rgto_?*) : ;; *) err "that does not look like a RapidGTO account token (expected rgto_...)." ;; esac # Core name shown in the panel. Default to the host's real name. default_core_name=$(hostname 2>/dev/null || uname -n) printf 'Name for this core [%s]: ' "$default_core_name" > /dev/tty IFS= read -r CORE_NAME < /dev/tty || err "could not read a reply." CORE_NAME=$(printf '%s' "$CORE_NAME" | tr -d '[:space:]') [ -n "$CORE_NAME" ] || CORE_NAME=$default_core_name fi # --- validate the token + get the image URL, THEN write files ---------------- # The API checks the token against your account and entitlement and # returns a short-lived presigned download for the core image. A bad or # expired token aborts HERE — before anything on the host is touched. echo "Checking your token and entitlement..." resp=$(curl -fsSL -H "authorization: Bearer $ACCOUNT_TOKEN" "$API_URL/dist/image-url" 2>/dev/null) \ || err "token check failed — is the token current (regenerate on $PANEL_URL/install) and your trial/plan active?" image_url=$(printf '%s' "$resp" | sed -n 's/.*"url":"\([^"]*\)".*/\1/p') [ -n "$image_url" ] || err "unexpected API response: $resp" # --- download + load the image ----------------------------------------------- # Bulk bytes come straight from R2 (the presigned URL), never through # the API or relay. docker load handles the gzip stream directly. echo "Downloading the core image (~140 MB unpacked)..." curl -fsSL "$image_url" | docker load || err "image download/load failed (the link is good for an hour — re-run to mint a fresh one)." # A core installed by hand (docker run, the manual flow) uses the same # container name but is not compose-managed; compose cannot adopt it. # Migrate it: graceful stop (5 min for a checkpoint), remove the # container, keep the volume — the compose core picks the identity up. if docker inspect rapidgto-core >/dev/null 2>&1 \ && [ -z "$(docker inspect rapidgto-core --format '{{index .Config.Labels "com.docker.compose.project"}}' 2>/dev/null)" ]; then printf 'Found a manually-run rapidgto-core container. Replace it with the managed install (its data and pairing are kept)? [Y/n] ' > /dev/tty IFS= read -r reply < /dev/tty || err "could not read a reply." case "$reply" in [Nn]*) err "left the manual container in place. Stop it yourself and re-run." ;; *) echo "Stopping the manual container (up to 5 minutes for a clean checkpoint)..." docker stop -t 300 rapidgto-core >/dev/null 2>&1 || true docker rm rapidgto-core >/dev/null 2>&1 || true ;; esac fi mkdir -p "$INSTALL_DIR" # compose.yaml is (re)written in every mode so updates pick up compose # changes, not just a newer image. .env is untouched on update. cat > "$INSTALL_DIR/compose.yaml" <<'COMPOSE' name: rapidgto services: core: image: rapidgto-core:latest container_name: rapidgto-core pull_policy: never environment: CORE_TOKEN: "${CORE_TOKEN:?CORE_TOKEN is missing}" CORE_NAME: "${CORE_NAME:-}" volumes: - rapidgto-data:/data restart: unless-stopped stop_grace_period: 5m volumes: rapidgto-data: name: rapidgto-data COMPOSE if [ "$MODE" != update ]; then cat > "$INSTALL_DIR/.env" </dev/null 2>&1 || true # --- demo artifacts (optional) ----------------------------------------------- # Two solved games to browse immediately (NLHE HU 25bb with EVs, PLO6 HU # 25bb preflop). Unpacked into the data volume through the core image # itself — no extra image pull. Skipped silently on update if declined. if [ "$MODE" != update ]; then printf 'Download the demo solves to browse right away (~360 MB)? [Y/n] ' > /dev/tty IFS= read -r reply < /dev/tty || err "could not read a reply." case "$reply" in [Nn]*) : ;; *) demo_resp=$(curl -fsSL -H "authorization: Bearer $ACCOUNT_TOKEN" "$API_URL/dist/demo-url" 2>/dev/null || true) demo_url=$(printf '%s' "$demo_resp" | sed -n 's/.*"url":"\([^"]*\)".*/\1/p') if [ -n "$demo_url" ]; then echo "Downloading + unpacking the demo bundle..." if curl -fsSL "$demo_url" | docker run --rm -i -v rapidgto-data:/data --entrypoint /bin/sh rapidgto-core:latest -c 'tar xzf - -C /data'; then echo "Demo artifacts installed — they appear in Browse on the next visit." else echo "Demo download failed — get it later from $PANEL_URL/install (the core itself is fine)." fi else echo "Demo bundle not available right now — get it later from $PANEL_URL/install." fi ;; esac fi echo if [ "$MODE" = update ]; then echo "Done. The core is updated and running." else echo "Done. The core '$CORE_NAME' is starting: it pairs with your account and runs a" echo "short self-benchmark — its card in the panel goes green and fills in live" echo "(usually well under a minute)." fi echo "Logs: cd $INSTALL_DIR && docker compose logs -f" echo "Update: re-run the same install command and choose Update." echo "Uninstall: re-run the same install command and choose Uninstall." echo "Your panel: $PANEL_URL"