From c78e6320a3a1b09343b09174c0914e970e13a3bc Mon Sep 17 00:00:00 2001 From: david-hummingbot Date: Fri, 8 May 2026 00:56:38 +0800 Subject: [PATCH 1/6] feat: add Tailscale integration and related changes --- Makefile | 40 +++++++++++++++++++-- README.md | 70 ++++++++++++++++++++++++++++++++++++ docker-compose.tailscale.yml | 30 ++++++++++++++++ setup.sh | 68 ++++++++++++++++++++++++++++++++++- 4 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 docker-compose.tailscale.yml diff --git a/Makefile b/Makefile index 8c4a43aa..849819ba 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: setup run deploy stop install uninstall build install-pre-commit +.PHONY: setup run deploy stop install uninstall build install-pre-commit tailscale-status SETUP_SENTINEL := .setup-complete @@ -9,13 +9,47 @@ $(SETUP_SENTINEL): ./setup.sh # Run locally (dev mode) +# When TAILSCALE_ENABLED=true: installs Tailscale if needed, connects, configures tailscale serve, +# then binds uvicorn to 127.0.0.1 only (tailscale serve exposes port 8000 on the tailnet) run: docker compose up emqx postgres -d - conda run --no-capture-output -n hummingbot-api uvicorn main:app --reload + @set -a; [ -f .env ] && . ./.env; set +a; \ + if [ "$${TAILSCALE_ENABLED:-false}" = "true" ]; then \ + echo "[INFO] Tailscale mode: setting up Tailscale for source install..."; \ + if ! command -v tailscale >/dev/null 2>&1; then \ + echo "[INFO] Installing Tailscale..."; \ + curl -fsSL https://tailscale.com/install.sh | sh; \ + fi; \ + if ! tailscale status >/dev/null 2>&1; then \ + echo "[INFO] Connecting to Tailscale network..."; \ + sudo tailscale up --authkey="$${TAILSCALE_AUTH_KEY}" --hostname="$${TAILSCALE_HOSTNAME:-hummingbot-api}" --accept-dns=true; \ + fi; \ + tailscale serve status 2>/dev/null | grep -q ":8000" || \ + sudo tailscale serve --bg http:8000 http://localhost:8000; \ + echo "[INFO] Binding uvicorn to 127.0.0.1 (tailscale serve exposes port 8000 on tailnet)"; \ + conda run --no-capture-output -n hummingbot-api uvicorn main:app --reload --host 127.0.0.1 --port 8000; \ + else \ + conda run --no-capture-output -n hummingbot-api uvicorn main:app --reload; \ + fi # Deploy with Docker +# When TAILSCALE_ENABLED=true: adds the Tailscale sidecar compose override deploy: $(SETUP_SENTINEL) - docker compose up -d + @set -a; [ -f .env ] && . ./.env; set +a; \ + if [ "$${TAILSCALE_ENABLED:-false}" = "true" ]; then \ + echo "[INFO] Deploying with Tailscale sidecar..."; \ + docker compose -f docker-compose.yml -f docker-compose.tailscale.yml up -d; \ + else \ + docker compose up -d; \ + fi + +# Show Tailscale connection status +tailscale-status: + @if command -v tailscale >/dev/null 2>&1; then \ + tailscale status; \ + else \ + echo "Tailscale is not installed or not on PATH."; \ + fi # Stop all services stop: diff --git a/README.md b/README.md index 9486a201..eaaec815 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ That's it! The API is now running at http://localhost:8000 | `make run` | Run API locally in dev mode | | `make install` | Install conda environment for development | | `make build` | Build Docker image | +| `make tailscale-status` | Show Tailscale connection status | ## Services @@ -96,6 +97,69 @@ GATEWAY_URL=... # Gateway URL (for DEX) Edit `.env` and restart with `make deploy` to apply changes. +## Secure Connection via Tailscale + +[Tailscale](https://tailscale.com) creates a private WireGuard network (tailnet) that makes the API accessible only to devices on your tailnet — no open ports, no firewall rules needed. + +Use this when running on a VPS or cloud server and want to access the API privately from another machine (e.g. Condor or MCP tools). + +### Prerequisites: Get a Tailscale auth key + +1. Create a free account at [tailscale.com](https://tailscale.com) +2. Go to **Settings → Keys**: [tailscale.com/admin/settings/keys](https://tailscale.com/admin/settings/keys) +3. Click **Generate auth key** — check **Reusable** for multiple deployments +4. Copy the key (starts with `tskey-auth-`) + +### Setup + +Run `make setup` and answer `y` when prompted: + +> Use Tailscale for secure private networking? [y/N] + +This adds the following to `.env`: + +```bash +TAILSCALE_ENABLED=true +TAILSCALE_AUTH_KEY=tskey-auth-... +TAILSCALE_HOSTNAME=hummingbot-api # MagicDNS hostname on your tailnet +``` + +### Deploy + +```bash +make deploy +``` + +When `TAILSCALE_ENABLED=true`, this automatically runs: + +```bash +docker compose -f docker-compose.yml -f docker-compose.tailscale.yml up -d +``` + +A Tailscale sidecar container joins your tailnet using `network_mode: host`. The API is then reachable at `http://hummingbot-api:8000` from any device on the same tailnet — port 8000 is not exposed publicly. + +### Connecting MCP tools via Tailscale + +Once on the same tailnet, use the MagicDNS hostname instead of `localhost`: + +```bash +claude mcp add --transport stdio hummingbot -- \ + docker run --rm -i \ + -e HUMMINGBOT_API_URL=http://hummingbot-api:8000 \ + -v hummingbot_mcp:/root/.hummingbot_mcp \ + hummingbot/hummingbot-mcp:latest +``` + +### Dev mode + +When `TAILSCALE_ENABLED=true`, `make run` will automatically install Tailscale if needed, connect to your tailnet, and bind uvicorn to `127.0.0.1` only (Tailscale handles external access). + +### Check status + +```bash +make tailscale-status +``` + ## API Features - **Portfolio**: Balances, positions, P&L across all exchanges @@ -132,6 +196,12 @@ make deploy # Fresh start docker ps | grep hummingbot ``` +**Tailscale not connecting?** +```bash +make tailscale-status # Check tailnet peers +``` +Confirm the node appears in `tailscale status` and that MagicDNS is enabled in your Tailscale admin console. + ## Support - **API Docs**: http://localhost:8000/docs diff --git a/docker-compose.tailscale.yml b/docker-compose.tailscale.yml new file mode 100644 index 00000000..0222b7bc --- /dev/null +++ b/docker-compose.tailscale.yml @@ -0,0 +1,30 @@ +# Tailscale sidecar overlay — use with: +# docker compose -f docker-compose.yml -f docker-compose.tailscale.yml up -d +# +# network_mode: host lets the Tailscale daemon create a real tailscale0 interface +# on the host, making hummingbot-api's port 8000 reachable at the Tailscale IP +# without exposing it on any public interface. +# +# The base docker-compose.yml emqx-bridge network is unaffected — emqx and +# postgres still communicate with hummingbot-api normally via that bridge. + +services: + tailscale: + image: tailscale/tailscale:latest + container_name: hummingbot-tailscale + network_mode: host + environment: + - TS_AUTHKEY=${TAILSCALE_AUTH_KEY} + - TS_STATE_DIR=/var/lib/tailscale + - TS_USERSPACE=false + - TS_HOSTNAME=${TAILSCALE_HOSTNAME:-hummingbot-api} + volumes: + - tailscale_state:/var/lib/tailscale + - /dev/net/tun:/dev/net/tun + cap_add: + - NET_ADMIN + - NET_RAW + restart: unless-stopped + +volumes: + tailscale_state: diff --git a/setup.sh b/setup.sh index 95128ed2..e7c57cca 100755 --- a/setup.sh +++ b/setup.sh @@ -20,6 +20,26 @@ COMPOSE_ALREADY_PRESENT=false has_cmd() { command -v "$1" >/dev/null 2>&1; } +prompt_tty() { + local message="$1" + local default_value="${2:-}" + local value="" + if [[ -c /dev/tty ]] && [[ -r /dev/tty ]]; then + read -p "$message" value < /dev/tty + else + read -p "$message" value + fi + echo "${value:-$default_value}" +} + +prompt_yes_no() { + local message="$1" + local default_value="${2:-n}" + local value + value="$(prompt_tty "$message" "$default_value")" + [[ "$value" =~ ^[Yy]$ ]] +} + resolve_script_dir() { local src="${BASH_SOURCE[0]}" while [ -h "$src" ]; do @@ -379,6 +399,39 @@ else fi CONFIG_PASSWORD=${CONFIG_PASSWORD:-admin} +# -------------------------- +# Tailscale Configuration +# -------------------------- +TAILSCALE_ENABLED=false +TAILSCALE_AUTH_KEY="" +TAILSCALE_HOSTNAME="hummingbot-api" + +if prompt_yes_no "Use Tailscale for secure private networking? [y/N]: " "n"; then + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " How to get a Tailscale auth key:" + echo " 1. Create a free account at https://tailscale.com" + echo " 2. Go to: https://tailscale.com/admin/settings/keys" + echo " 3. Click 'Generate auth key'" + echo " 4. Check 'Reusable' for multiple server deployments" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + while true; do + TAILSCALE_AUTH_KEY="$(prompt_tty "Tailscale auth key (tskey-auth-...): " "")" + if [[ -z "$TAILSCALE_AUTH_KEY" ]]; then + echo "[WARN] Auth key cannot be empty" + continue + fi + if [[ ! "$TAILSCALE_AUTH_KEY" =~ ^tskey-auth- ]]; then + echo "[WARN] Auth key must start with 'tskey-auth-'" + continue + fi + break + done + # Hostname defaults to "hummingbot-api" — override via TAILSCALE_HOSTNAME in .env if needed + TAILSCALE_ENABLED=true +fi + cat > .env << EOF # Hummingbot API Configuration USERNAME=$USERNAME @@ -401,6 +454,11 @@ GATEWAY_PASSPHRASE=admin # Paths BOTS_PATH=$(pwd) + +# Tailscale +TAILSCALE_ENABLED=$TAILSCALE_ENABLED +TAILSCALE_AUTH_KEY=$TAILSCALE_AUTH_KEY +TAILSCALE_HOSTNAME=$TAILSCALE_HOSTNAME EOF touch .setup-complete @@ -415,5 +473,13 @@ echo " make deploy" echo "" echo "Option 2: Run API locally (dev mode)" echo " make install # Creates the conda environment - Note: Please install the latest Anaconda version manually" -echo " make run # Run API" +echo " make run # Run API (installs and connects Tailscale automatically if TAILSCALE_ENABLED=true)" +if [ "$TAILSCALE_ENABLED" = true ]; then + echo "" + echo "Tailscale:" + echo " Docker deploy: Tailscale sidecar starts automatically with 'make deploy'" + echo " Source run: Tailscale installs and connects automatically with 'make run'" + echo " Condor URL: http://$TAILSCALE_HOSTNAME:8000" + echo " Status: make tailscale-status" +fi echo "" From f281e74136154151679248d19a7e20e30d7e62eb Mon Sep 17 00:00:00 2001 From: david-hummingbot <85695272+david-hummingbot@users.noreply.github.com> Date: Sun, 10 May 2026 01:42:52 +0800 Subject: [PATCH 2/6] Update Tailscale installation and connection process --- Makefile | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 849819ba..4cd59ad0 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,16 @@ run: fi; \ if ! tailscale status >/dev/null 2>&1; then \ echo "[INFO] Connecting to Tailscale network..."; \ + if grep -qi microsoft /proc/version 2>/dev/null; then \ + if ! pgrep -x tailscaled >/dev/null 2>&1; then \ + echo "[INFO] Starting Tailscale daemon (WSL2)..."; \ + sudo mkdir -p /var/run/tailscale /var/lib/tailscale; \ + sudo tailscaled --state=/var/lib/tailscale/tailscaled.state \ + --socket=/var/run/tailscale/tailscaled.sock \ + >/dev/null 2>&1 & \ + sleep 2; \ + fi; \ + fi; \ sudo tailscale up --authkey="$${TAILSCALE_AUTH_KEY}" --hostname="$${TAILSCALE_HOSTNAME:-hummingbot-api}" --accept-dns=true; \ fi; \ tailscale serve status 2>/dev/null | grep -q ":8000" || \ @@ -79,4 +89,4 @@ install-pre-commit: # Build Docker image build: - docker build -t hummingbot/hummingbot-api:latest . \ No newline at end of file + docker build -t hummingbot/hummingbot-api:latest . From ff80c309f1001a0ea9d3b276275ca70c36edd90f Mon Sep 17 00:00:00 2001 From: david-hummingbot <85695272+david-hummingbot@users.noreply.github.com> Date: Sun, 10 May 2026 01:43:16 +0800 Subject: [PATCH 3/6] Refactor Hummingbot API setup script for clarity --- setup.sh | 577 +++++++++++++++++++------------------------------------ 1 file changed, 194 insertions(+), 383 deletions(-) diff --git a/setup.sh b/setup.sh index e7c57cca..bd0a8957 100755 --- a/setup.sh +++ b/setup.sh @@ -1,437 +1,237 @@ #!/bin/bash -# Hummingbot API Setup - Creates .env with sensible defaults (Mac/Linux/WSL2) -# - On Linux (apt-based): installs build deps (gcc, build-essential) -# - Ensures Docker + Docker Compose are available (auto-installs on Linux via get.docker.com) -# - Idempotent: safe to run multiple times, skips already-completed steps -# - Verbose output: shows all installation progress directly -# - Fixed: Removed apt-get upgrade, uses /dev/tty for prompts +# Hummingbot API Setup +# - Installs Docker on Linux (apt-based) +# - Creates .env with credentials and optional Tailscale config +# - Works for both Docker deployment and source/conda install +# - Idempotent: safe to re-run set -euo pipefail -echo "Hummingbot API Setup" -echo "" - -# -------------------------- -# State Tracking Variables -# -------------------------- -APT_CACHE_UPDATED=false -DOCKER_ALREADY_PRESENT=false -COMPOSE_ALREADY_PRESENT=false - -has_cmd() { command -v "$1" >/dev/null 2>&1; } - -prompt_tty() { - local message="$1" - local default_value="${2:-}" - local value="" - if [[ -c /dev/tty ]] && [[ -r /dev/tty ]]; then - read -p "$message" value < /dev/tty - else - read -p "$message" value - fi - echo "${value:-$default_value}" -} - -prompt_yes_no() { - local message="$1" - local default_value="${2:-n}" - local value - value="$(prompt_tty "$message" "$default_value")" - [[ "$value" =~ ^[Yy]$ ]] +# ── Styling ──────────────────────────────────────────────────────────────────── +BOLD='\033[1m' +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +RESET='\033[0m' + +msg_ok() { echo -e " ${GREEN}✓${RESET} $1"; } +msg_info() { echo -e " ${CYAN}→${RESET} $1"; } +msg_warn() { echo -e " ${YELLOW}⚠${RESET} $1"; } +msg_error() { echo -e " ${RED}✗${RESET} $1"; exit 1; } + +prompt_visible() { + local message="$1" default="${2:-}" var_name="$3" value="" + if [[ -n "$default" ]]; then + read -p " $message [$default]: " value < /dev/tty 2>/dev/null || read -p " $message [$default]: " value + else + read -p " $message: " value < /dev/tty 2>/dev/null || read -p " $message: " value + fi + printf -v "$var_name" '%s' "${value:-$default}" } -resolve_script_dir() { - local src="${BASH_SOURCE[0]}" - while [ -h "$src" ]; do - local dir - dir="$(cd -P "$(dirname "$src")" >/dev/null 2>&1 && pwd)" - src="$(readlink "$src")" - [[ "$src" != /* ]] && src="$dir/$src" - done - cd -P "$(dirname "$src")" >/dev/null 2>&1 && pwd +prompt_secret() { + local message="$1" default="${2:-}" var_name="$3" value="" + if [[ -n "$default" ]]; then + read -s -p " $message [$default]: " value < /dev/tty 2>/dev/null || read -s -p " $message [$default]: " value + else + read -s -p " $message: " value < /dev/tty 2>/dev/null || read -s -p " $message: " value + fi + echo + printf -v "$var_name" '%s' "${value:-$default}" } -SCRIPT_DIR="$(resolve_script_dir)" - -# -------------------------- -# OS / Environment Detection -# -------------------------- -OS="$(uname -s || true)" -ARCH="$(uname -m || true)" - -is_linux() { [[ "${OS}" == "Linux" ]]; } -is_macos() { [[ "${OS}" == "Darwin" ]]; } +# ── State tracking ───────────────────────────────────────────────────────────── +APT_CACHE_UPDATED=false +DOCKER_ALREADY_PRESENT=false +COMPOSE_ALREADY_PRESENT=false +has_cmd() { command -v "$1" >/dev/null 2>&1; } +is_linux() { [[ "$(uname -s)" == "Linux" ]]; } +is_macos() { [[ "$(uname -s)" == "Darwin" ]]; } +is_wsl2() { grep -qi microsoft /proc/version 2>/dev/null; } docker_ok() { has_cmd docker; } docker_compose_ok() { - if has_cmd docker && docker compose version >/dev/null 2>&1; then - return 0 - fi - if has_cmd docker-compose && docker-compose version >/dev/null 2>&1; then - return 0 - fi - return 1 + has_cmd docker && docker compose version >/dev/null 2>&1 && return 0 + has_cmd docker-compose && docker-compose version >/dev/null 2>&1 && return 0 + return 1 } need_sudo_or_die() { - if ! has_cmd sudo; then - echo "ERROR: 'sudo' is required for dependency installation on this system." - echo "Please install sudo (or run as root) and re-run this script." - exit 1 - fi + has_cmd sudo && return 0 + msg_error "'sudo' is required. Install it or run as root." } -# -------------------------- -# APT Cache Management (Linux) -# -------------------------- safe_apt_update() { - # Only run apt-get update once per script execution - if [ "$APT_CACHE_UPDATED" = false ]; then - echo "[INFO] Updating apt cache..." - sudo env DEBIAN_FRONTEND=noninteractive apt-get update + [ "$APT_CACHE_UPDATED" = true ] && return 0 + msg_info "Updating apt cache..." + sudo env DEBIAN_FRONTEND=noninteractive apt-get update -q APT_CACHE_UPDATED=true - fi } -# -------------------------- -# Package Check Utilities -# -------------------------- -is_package_installed() { - # Check if a Debian package is installed - # Usage: is_package_installed package-name - dpkg -l "$1" 2>/dev/null | grep -q "^ii" -} +is_pkg_installed() { dpkg -l "$1" 2>/dev/null | grep -q "^ii"; } -# -------------------------- -# Linux Dependencies -# -------------------------- -install_linux_build_deps() { - if has_cmd apt-get; then - # Check if build dependencies are already installed - if is_package_installed build-essential && has_cmd gcc; then - echo "[OK] Build dependencies (gcc, build-essential) already installed. Skipping." - return 0 - fi - - need_sudo_or_die - echo "[INFO] Installing build dependencies (gcc, build-essential)..." - - safe_apt_update - - # REMOVED: apt-get upgrade -y - # This was causing failures due to system-wide package upgrades - # apt-get install will get the latest available versions anyway - - sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y gcc build-essential - - echo "[OK] Build dependencies installed." - else - echo "[WARN] Detected Linux, but 'apt-get' is not available. Skipping build dependency install." - fi +check_user_in_docker_group() { + [[ "${EUID}" -eq 0 ]] && return 0 + has_cmd getent && getent group docker >/dev/null 2>&1 && \ + id -nG "$USER" 2>/dev/null | grep -qw docker } -ensure_curl_on_linux() { - if has_cmd curl; then - echo "[OK] curl is already installed." - return 0 - fi - - if has_cmd apt-get; then - need_sudo_or_die - echo "[INFO] Installing curl (required for Docker install script)..." - safe_apt_update - sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates - echo "[OK] curl installed." - return 0 - fi - - echo "[WARN] curl is not installed and apt-get is unavailable. Please install curl and re-run." - return 1 +add_user_to_docker_group() { + check_user_in_docker_group && { msg_ok "User '$USER' already in docker group"; return 0; } + has_cmd getent && getent group docker >/dev/null 2>&1 && [[ "${EUID}" -ne 0 ]] && \ + sudo usermod -aG docker "$USER" >/dev/null 2>&1 || true + msg_ok "User added to docker group (re-login may be required)" } -# -------------------------- -# Docker Install / Validation -# -------------------------- -check_user_in_docker_group() { - # Check if current user is already in docker group - if [[ "${EUID}" -eq 0 ]]; then - # Running as root, no need for docker group - return 0 - fi - - if has_cmd getent && getent group docker >/dev/null 2>&1; then - if id -nG "$USER" 2>/dev/null | grep -qw docker; then - return 0 - fi - fi - - return 1 -} +# ── Banner ───────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════════════${RESET}" +echo -e " ${BOLD}Hummingbot API Setup${RESET}" +echo -e "${BOLD}══════════════════════════════════════════════${RESET}" +echo "" +msg_info "OS: $(uname -s) ARCH: $(uname -m)" +echo "" -add_user_to_docker_group() { - # Only add user to docker group if not already a member - if check_user_in_docker_group; then - echo "[OK] User '$USER' is already in the 'docker' group." - return 0 - fi - - if has_cmd getent && getent group docker >/dev/null 2>&1; then - if [[ "${EUID}" -ne 0 ]]; then - echo "[INFO] Adding current user to 'docker' group (may require re-login)..." - sudo usermod -aG docker "$USER" >/dev/null 2>&1 || true - echo "[OK] User added to docker group. You may need to log out and back in for this to take effect." +# ── Linux build deps ─────────────────────────────────────────────────────────── +if is_linux && has_cmd apt-get; then + if is_pkg_installed build-essential && has_cmd gcc; then + msg_ok "Build dependencies already installed" + else + need_sudo_or_die + msg_info "Installing build dependencies (gcc, build-essential)..." + safe_apt_update + sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y gcc build-essential + msg_ok "Build dependencies installed" fi - fi -} +fi +# ── Docker ───────────────────────────────────────────────────────────────────── install_docker_linux() { - need_sudo_or_die - ensure_curl_on_linux - - echo "[INFO] Docker not found. Installing Docker using get.docker.com script..." - curl -fsSL https://get.docker.com -o get-docker.sh - sudo sh get-docker.sh - rm -f get-docker.sh - - if has_cmd systemctl; then - if systemctl is-system-running >/dev/null 2>&1; then - echo "[INFO] Enabling and starting Docker service..." - sudo systemctl enable docker 2>/dev/null || true - sudo systemctl start docker 2>/dev/null || true + need_sudo_or_die + if ! has_cmd curl; then + msg_info "Installing curl..." + safe_apt_update + sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates fi - fi - - add_user_to_docker_group + msg_info "Installing Docker via get.docker.com..." + curl -fsSL https://get.docker.com -o get-docker.sh + sudo sh get-docker.sh + rm -f get-docker.sh + if has_cmd systemctl && systemctl is-system-running >/dev/null 2>&1; then + sudo systemctl enable docker 2>/dev/null || true + sudo systemctl start docker 2>/dev/null || true + fi + add_user_to_docker_group } -ensure_docker_and_compose() { - if is_linux; then - # Check Docker installation - if docker_ok; then - echo "[OK] Docker already installed: $(docker --version 2>/dev/null || echo 'version unknown')" - DOCKER_ALREADY_PRESENT=true - - # Even if Docker is installed, ensure user is in docker group - add_user_to_docker_group - else - # Check if Docker binary exists but isn't in PATH - if [ -x "/usr/bin/docker" ] || [ -x "/usr/local/bin/docker" ]; then - echo "[INFO] Docker found but not in current PATH. Adding to PATH..." - export PATH="/usr/bin:/usr/local/bin:$PATH" - +ensure_docker() { + if is_linux; then if docker_ok; then - echo "[OK] Docker is now accessible: $(docker --version 2>/dev/null || echo 'version unknown')" - DOCKER_ALREADY_PRESENT=true - add_user_to_docker_group + msg_ok "Docker $(docker --version 2>/dev/null | head -1)" + DOCKER_ALREADY_PRESENT=true + add_user_to_docker_group else - install_docker_linux + install_docker_linux fi - else - install_docker_linux - fi - fi - - # Verify Docker is actually working - if ! docker_ok; then - echo "ERROR: Docker installation did not succeed or 'docker' is still not on PATH." - echo " Try opening a new shell and re-running, or verify Docker installation." - exit 1 - fi + docker_ok || msg_error "Docker installation failed. Open a new shell and re-run." - # Check Docker Compose installation - if docker_compose_ok; then - echo "[OK] Docker Compose already available" - COMPOSE_ALREADY_PRESENT=true - - # Show which version we detected - if docker compose version >/dev/null 2>&1; then - echo "[OK] Using Docker Compose plugin: $(docker compose version 2>/dev/null || echo 'version unknown')" - else - echo "[OK] Using standalone docker-compose: $(docker-compose version 2>/dev/null || echo 'version unknown')" - fi - else - # Try to install docker-compose-plugin - if has_cmd apt-get; then - # Check if plugin package is already installed but not working - if is_package_installed docker-compose-plugin; then - echo "[WARN] docker-compose-plugin package is installed but not functioning properly." - echo "[INFO] Attempting to reinstall docker-compose-plugin..." - need_sudo_or_die - safe_apt_update - sudo env DEBIAN_FRONTEND=noninteractive apt-get install --reinstall -y docker-compose-plugin || true + if docker_compose_ok; then + msg_ok "Docker Compose available" + COMPOSE_ALREADY_PRESENT=true else - need_sudo_or_die - echo "[INFO] Docker Compose not found. Attempting to install docker-compose-plugin..." - safe_apt_update - sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y docker-compose-plugin || true + if has_cmd apt-get; then + need_sudo_or_die + msg_info "Installing docker-compose-plugin..." + safe_apt_update + sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y docker-compose-plugin || true + fi fi - fi - fi + docker_compose_ok || msg_error "Docker Compose not available. Try: sudo apt-get install -y docker-compose-plugin" - # Final verification of Docker Compose - if ! docker_compose_ok; then - echo "ERROR: Docker Compose is not available." - echo " Expected either 'docker compose' (v2) or 'docker-compose' (v1)." - echo " On Ubuntu/Debian, try: sudo apt-get install -y docker-compose-plugin" - exit 1 - fi - - elif is_macos; then - if ! docker_ok || ! docker_compose_ok; then - echo "ERROR: Docker and/or Docker Compose not found on macOS." - echo " Install Docker Desktop for Mac (Apple Silicon or Intel) and re-run this script." - echo " After installation, ensure 'docker' works in this terminal (you may need a new shell)." - exit 1 - fi - - echo "[OK] Docker detected: $(docker --version 2>/dev/null || echo 'version unknown')" - if docker compose version >/dev/null 2>&1; then - echo "[OK] Docker Compose detected: $(docker compose version 2>/dev/null || echo 'version unknown')" - else - echo "[OK] Docker Compose detected: $(docker-compose version 2>/dev/null || echo 'version unknown')" - fi - - else - echo "[WARN] Unsupported/unknown OS '${OS}'. Proceeding without installing OS-level dependencies." - if ! docker_ok || ! docker_compose_ok; then - echo "ERROR: Docker and/or Docker Compose not found." - exit 1 - fi - - echo "[OK] Docker detected: $(docker --version 2>/dev/null || echo 'version unknown')" - if docker compose version >/dev/null 2>&1; then - echo "[OK] Docker Compose detected: $(docker compose version 2>/dev/null || echo 'version unknown')" + elif is_macos; then + docker_ok && docker_compose_ok || \ + msg_error "Docker Desktop not found. Install it from https://www.docker.com/products/docker-desktop and re-run." + msg_ok "Docker $(docker --version 2>/dev/null | head -1)" + msg_ok "Docker Compose available" else - echo "[OK] Docker Compose detected: $(docker-compose version 2>/dev/null || echo 'version unknown')" + docker_ok && docker_compose_ok || msg_error "Docker and Docker Compose are required." fi - fi } -# -------------------------- -# Pull Hummingbot Docker Image -# -------------------------- -pull_hummingbot_image() { - echo "[INFO] Pulling latest Hummingbot image (hummingbot/hummingbot:latest)..." - if docker pull hummingbot/hummingbot:latest; then - echo "[OK] Hummingbot image pulled successfully." - else - echo "[WARN] Could not pull hummingbot/hummingbot:latest (network issue?). You may need to run 'docker pull hummingbot/hummingbot:latest' manually." - fi -} - -# -------------------------- -# Pre-flight (deps + docker) -# -------------------------- -echo "[INFO] OS=${OS} ARCH=${ARCH}" - -if is_linux; then - install_linux_build_deps -fi - -ensure_docker_and_compose +ensure_docker -# Show summary of what was done echo "" -if [ "$DOCKER_ALREADY_PRESENT" = true ] && [ "$COMPOSE_ALREADY_PRESENT" = true ]; then - echo "[OK] All dependencies were already installed. No changes made." -elif [ "$DOCKER_ALREADY_PRESENT" = true ]; then - echo "[OK] Docker was already installed. Docker Compose has been set up." -elif [ "$COMPOSE_ALREADY_PRESENT" = true ]; then - echo "[OK] Docker has been installed. Docker Compose was already available." +msg_info "Pulling latest Hummingbot image..." +if docker pull hummingbot/hummingbot:latest 2>/dev/null; then + msg_ok "hummingbot/hummingbot:latest pulled" else - echo "[OK] Docker and Docker Compose have been installed." + msg_warn "Could not pull hummingbot image — run 'docker pull hummingbot/hummingbot:latest' later" fi - -echo "" - -# Always pull latest Hummingbot image (first install and upgrade) -pull_hummingbot_image - echo "" -# -------------------------- -# Existing .env creation flow -# -------------------------- +# ── Idempotency check ────────────────────────────────────────────────────────── if [ -f ".env" ]; then - echo ".env file already exists. Skipping setup." - echo "" - - # Ensure sentinel file exists - if [ ! -f ".setup-complete" ]; then + msg_ok ".env already exists — skipping setup" touch .setup-complete - fi - - exit 0 + echo "" + exit 0 fi -# Clear screen before prompting user (only if running interactively) -if [[ -t 0 ]] && [[ -c /dev/tty ]]; then - if has_cmd clear; then - clear - else - printf "\033c" - fi -fi - -echo "Hummingbot API Setup" +# ── Credentials ──────────────────────────────────────────────────────────────── +echo -e "${BOLD}══════════════════════════════════════════════${RESET}" +echo -e " ${BOLD}API Credentials${RESET}" +echo -e "${BOLD}══════════════════════════════════════════════${RESET}" echo "" -# Use /dev/tty for prompts to work correctly when called from parent scripts -if [[ -c /dev/tty ]] && [[ -r /dev/tty ]]; then - read -p "API username [default: admin]: " USERNAME < /dev/tty -else - read -p "API username [default: admin]: " USERNAME -fi -USERNAME=${USERNAME:-admin} +prompt_visible "API username" "admin" USERNAME +prompt_secret "API password" "admin" PASSWORD +prompt_secret "Config password" "admin" CONFIG_PASSWORD -if [[ -c /dev/tty ]] && [[ -r /dev/tty ]]; then - read -p "API password [default: admin]: " PASSWORD < /dev/tty -else - read -p "API password [default: admin]: " PASSWORD -fi -PASSWORD=${PASSWORD:-admin} +# ── Tailscale ────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}══════════════════════════════════════════════${RESET}" +echo -e " ${BOLD}Tailscale (optional)${RESET}" +echo -e "${BOLD}══════════════════════════════════════════════${RESET}" +echo "" +echo -e " Use Tailscale to make this API securely accessible from Condor" +echo -e " without exposing port 8000 to the public internet." +echo "" -if [[ -c /dev/tty ]] && [[ -r /dev/tty ]]; then - read -p "Config password [default: admin]: " CONFIG_PASSWORD < /dev/tty -else - read -p "Config password [default: admin]: " CONFIG_PASSWORD -fi -CONFIG_PASSWORD=${CONFIG_PASSWORD:-admin} +prompt_visible "Enable Tailscale? [y/N]" "N" _use_tailscale -# -------------------------- -# Tailscale Configuration -# -------------------------- TAILSCALE_ENABLED=false TAILSCALE_AUTH_KEY="" TAILSCALE_HOSTNAME="hummingbot-api" -if prompt_yes_no "Use Tailscale for secure private networking? [y/N]: " "n"; then - echo "" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo " How to get a Tailscale auth key:" - echo " 1. Create a free account at https://tailscale.com" - echo " 2. Go to: https://tailscale.com/admin/settings/keys" - echo " 3. Click 'Generate auth key'" - echo " 4. Check 'Reusable' for multiple server deployments" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "" - while true; do - TAILSCALE_AUTH_KEY="$(prompt_tty "Tailscale auth key (tskey-auth-...): " "")" - if [[ -z "$TAILSCALE_AUTH_KEY" ]]; then - echo "[WARN] Auth key cannot be empty" - continue - fi - if [[ ! "$TAILSCALE_AUTH_KEY" =~ ^tskey-auth- ]]; then - echo "[WARN] Auth key must start with 'tskey-auth-'" - continue - fi - break - done - # Hostname defaults to "hummingbot-api" — override via TAILSCALE_HOSTNAME in .env if needed - TAILSCALE_ENABLED=true +if [[ "${_use_tailscale:-}" =~ ^[Yy]$ ]]; then + echo "" + echo -e " ${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" + echo -e " ${CYAN} How to get a Tailscale auth key:${RESET}" + echo -e " ${CYAN} 1. Create a free account at https://tailscale.com${RESET}" + echo -e " ${CYAN} 2. Go to: https://tailscale.com/admin/settings/keys${RESET}" + echo -e " ${CYAN} 3. Click 'Generate auth key'${RESET}" + echo -e " ${CYAN} 4. Check 'Reusable' for multiple server deployments${RESET}" + echo -e " ${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" + echo "" + while true; do + prompt_visible "Tailscale auth key (tskey-auth-...)" "" TAILSCALE_AUTH_KEY + if [[ -z "${TAILSCALE_AUTH_KEY:-}" ]]; then + msg_warn "Auth key cannot be empty" + continue + fi + if [[ ! "$TAILSCALE_AUTH_KEY" =~ ^tskey-auth- ]]; then + msg_warn "Auth key must start with 'tskey-auth-'" + continue + fi + break + done + TAILSCALE_ENABLED=true + msg_ok "Tailscale will be enabled — hostname: $TAILSCALE_HOSTNAME" fi +# ── Write .env ───────────────────────────────────────────────────────────────── cat > .env << EOF # Hummingbot API Configuration USERNAME=$USERNAME @@ -462,24 +262,35 @@ TAILSCALE_HOSTNAME=$TAILSCALE_HOSTNAME EOF touch .setup-complete +msg_ok ".env created" +# ── Summary ──────────────────────────────────────────────────────────────────── echo "" -echo ".env created successfully!" -echo "" -echo "Next steps:" +echo -e "${BOLD}══════════════════════════════════════════════${RESET}" +echo -e " ${GREEN}Setup complete!${RESET}" echo "" -echo "Option 1: Start all services with Docker (recommended)" -echo " make deploy" +echo -e " ${BOLD}Docker deployment (recommended for VPS):${RESET}" +echo -e " make deploy" echo "" -echo "Option 2: Run API locally (dev mode)" -echo " make install # Creates the conda environment - Note: Please install the latest Anaconda version manually" -echo " make run # Run API (installs and connects Tailscale automatically if TAILSCALE_ENABLED=true)" +echo -e " ${BOLD}Source / dev mode (requires conda):${RESET}" +echo -e " make install ${CYAN}# create conda environment${RESET}" +echo -e " make run ${CYAN}# start API${RESET}" if [ "$TAILSCALE_ENABLED" = true ]; then - echo "" - echo "Tailscale:" - echo " Docker deploy: Tailscale sidecar starts automatically with 'make deploy'" - echo " Source run: Tailscale installs and connects automatically with 'make run'" - echo " Condor URL: http://$TAILSCALE_HOSTNAME:8000" - echo " Status: make tailscale-status" + echo "" + echo -e " ${BOLD}Tailscale:${RESET}" + echo -e " Docker: Tailscale sidecar starts automatically with 'make deploy'" + echo -e " Source: Tailscale installs and connects automatically with 'make run'" + echo -e " API URL: http://$TAILSCALE_HOSTNAME:8000 ${CYAN}(Tailscale access)${RESET}" + echo -e " Status: make tailscale-status" + echo "" + echo -e " ${BOLD}Accessing from Condor:${RESET}" + echo -e " ${CYAN} Install Tailscale on the Condor machine and connect with the same key:${RESET}" + if is_wsl2; then + echo -e " curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up --authkey=$TAILSCALE_AUTH_KEY" + else + echo -e " Linux / WSL: curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up --authkey=$TAILSCALE_AUTH_KEY" + echo -e " macOS / Win: https://tailscale.com/download — then run: sudo tailscale up --authkey=$TAILSCALE_AUTH_KEY" + fi fi +echo -e "${BOLD}══════════════════════════════════════════════${RESET}" echo "" From f7110fe3a5e5e7f8ea7863dab9aa4aca25c082a0 Mon Sep 17 00:00:00 2001 From: david-hummingbot <85695272+david-hummingbot@users.noreply.github.com> Date: Mon, 11 May 2026 13:27:31 +0800 Subject: [PATCH 4/6] Refactor Makefile for Tailscale integration --- Makefile | 60 ++++++++++++++++++++++++-------------------------------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/Makefile b/Makefile index 4cd59ad0..cf25ff98 100644 --- a/Makefile +++ b/Makefile @@ -8,42 +8,35 @@ $(SETUP_SENTINEL): chmod +x setup.sh ./setup.sh -# Run locally (dev mode) -# When TAILSCALE_ENABLED=true: installs Tailscale if needed, connects, configures tailscale serve, -# then binds uvicorn to 127.0.0.1 only (tailscale serve exposes port 8000 on the tailnet) +# Run locally (dev mode) — Tailscale-aware: reads TAILSCALE_ENABLED from .env run: - docker compose up emqx postgres -d @set -a; [ -f .env ] && . ./.env; set +a; \ if [ "$${TAILSCALE_ENABLED:-false}" = "true" ]; then \ - echo "[INFO] Tailscale mode: setting up Tailscale for source install..."; \ + echo "[INFO] Tailscale enabled — checking connection..."; \ if ! command -v tailscale >/dev/null 2>&1; then \ - echo "[INFO] Installing Tailscale..."; \ - curl -fsSL https://tailscale.com/install.sh | sh; \ + echo "[ERROR] Tailscale is not installed. Install it or set TAILSCALE_ENABLED=false in .env."; \ + exit 1; \ + fi; \ + if grep -qi microsoft /proc/version 2>/dev/null && ! pgrep -x tailscaled >/dev/null 2>&1; then \ + echo "[INFO] Starting Tailscale daemon (WSL2)..."; \ + sudo mkdir -p /var/run/tailscale /var/lib/tailscale; \ + sudo tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/var/run/tailscale/tailscaled.sock >/dev/null 2>&1 & sleep 2; \ fi; \ if ! tailscale status >/dev/null 2>&1; then \ - echo "[INFO] Connecting to Tailscale network..."; \ - if grep -qi microsoft /proc/version 2>/dev/null; then \ - if ! pgrep -x tailscaled >/dev/null 2>&1; then \ - echo "[INFO] Starting Tailscale daemon (WSL2)..."; \ - sudo mkdir -p /var/run/tailscale /var/lib/tailscale; \ - sudo tailscaled --state=/var/lib/tailscale/tailscaled.state \ - --socket=/var/run/tailscale/tailscaled.sock \ - >/dev/null 2>&1 & \ - sleep 2; \ - fi; \ + echo "[INFO] Connecting to Tailscale..."; \ + if [ -n "$${TAILSCALE_AUTH_KEY:-}" ]; then \ + sudo tailscale up --authkey="$${TAILSCALE_AUTH_KEY}" --hostname=$${TAILSCALE_HOSTNAME:-hummingbot-api} --accept-dns=true; \ + else \ + sudo tailscale up --hostname=$${TAILSCALE_HOSTNAME:-hummingbot-api} --accept-dns=true; \ fi; \ - sudo tailscale up --authkey="$${TAILSCALE_AUTH_KEY}" --hostname="$${TAILSCALE_HOSTNAME:-hummingbot-api}" --accept-dns=true; \ + else \ + echo "[INFO] Tailscale already connected."; \ fi; \ - tailscale serve status 2>/dev/null | grep -q ":8000" || \ - sudo tailscale serve --bg http:8000 http://localhost:8000; \ - echo "[INFO] Binding uvicorn to 127.0.0.1 (tailscale serve exposes port 8000 on tailnet)"; \ - conda run --no-capture-output -n hummingbot-api uvicorn main:app --reload --host 127.0.0.1 --port 8000; \ - else \ - conda run --no-capture-output -n hummingbot-api uvicorn main:app --reload; \ fi + docker compose up emqx postgres -d + conda run --no-capture-output -n hummingbot-api uvicorn main:app --reload -# Deploy with Docker -# When TAILSCALE_ENABLED=true: adds the Tailscale sidecar compose override +# Deploy with Docker (Tailscale-aware: reads TAILSCALE_ENABLED from .env) deploy: $(SETUP_SENTINEL) @set -a; [ -f .env ] && . ./.env; set +a; \ if [ "$${TAILSCALE_ENABLED:-false}" = "true" ]; then \ @@ -53,14 +46,6 @@ deploy: $(SETUP_SENTINEL) docker compose up -d; \ fi -# Show Tailscale connection status -tailscale-status: - @if command -v tailscale >/dev/null 2>&1; then \ - tailscale status; \ - else \ - echo "Tailscale is not installed or not on PATH."; \ - fi - # Stop all services stop: docker compose down @@ -90,3 +75,10 @@ install-pre-commit: # Build Docker image build: docker build -t hummingbot/hummingbot-api:latest . +# Show Tailscale connection status +tailscale-status: + @if command -v tailscale >/dev/null 2>&1; then \ + tailscale status; \ + else \ + echo "Tailscale is not installed or not on PATH."; \ + fi From 5395c89a2358ba2ebe300ae6280394cfb91ed116 Mon Sep 17 00:00:00 2001 From: david-hummingbot <85695272+david-hummingbot@users.noreply.github.com> Date: Tue, 19 May 2026 22:01:40 +0800 Subject: [PATCH 5/6] Remove default suggested username and password Updated Hummingbot API setup script to increase security --- setup.sh | 579 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 386 insertions(+), 193 deletions(-) diff --git a/setup.sh b/setup.sh index bd0a8957..76620de5 100755 --- a/setup.sh +++ b/setup.sh @@ -1,237 +1,441 @@ #!/bin/bash -# Hummingbot API Setup -# - Installs Docker on Linux (apt-based) -# - Creates .env with credentials and optional Tailscale config -# - Works for both Docker deployment and source/conda install -# - Idempotent: safe to re-run +# Hummingbot API Setup - Creates .env with sensible defaults (Mac/Linux/WSL2) +# - On Linux (apt-based): installs build deps (gcc, build-essential) +# - Ensures Docker + Docker Compose are available (auto-installs on Linux via get.docker.com) +# - Idempotent: safe to run multiple times, skips already-completed steps +# - Verbose output: shows all installation progress directly +# - Fixed: Removed apt-get upgrade, uses /dev/tty for prompts set -euo pipefail -# ── Styling ──────────────────────────────────────────────────────────────────── -BOLD='\033[1m' -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -RESET='\033[0m' - -msg_ok() { echo -e " ${GREEN}✓${RESET} $1"; } -msg_info() { echo -e " ${CYAN}→${RESET} $1"; } -msg_warn() { echo -e " ${YELLOW}⚠${RESET} $1"; } -msg_error() { echo -e " ${RED}✗${RESET} $1"; exit 1; } - -prompt_visible() { - local message="$1" default="${2:-}" var_name="$3" value="" - if [[ -n "$default" ]]; then - read -p " $message [$default]: " value < /dev/tty 2>/dev/null || read -p " $message [$default]: " value - else - read -p " $message: " value < /dev/tty 2>/dev/null || read -p " $message: " value - fi - printf -v "$var_name" '%s' "${value:-$default}" +echo "Hummingbot API Setup" +echo "" + +# -------------------------- +# State Tracking Variables +# -------------------------- +APT_CACHE_UPDATED=false +DOCKER_ALREADY_PRESENT=false +COMPOSE_ALREADY_PRESENT=false + +has_cmd() { command -v "$1" >/dev/null 2>&1; } + +prompt_tty() { + local message="$1" + local default_value="${2:-}" + local value="" + local fd + if [[ -t 0 ]]; then + read -r -p "$message" value + elif { exec {fd}<>/dev/tty; } 2>/dev/null; then + printf '%s' "$message" >&${fd} + read -r value <&${fd} + exec {fd}>&- + elif IFS= read -r value; then + : + else + value="" + fi + echo "${value:-$default_value}" } -prompt_secret() { - local message="$1" default="${2:-}" var_name="$3" value="" - if [[ -n "$default" ]]; then - read -s -p " $message [$default]: " value < /dev/tty 2>/dev/null || read -s -p " $message [$default]: " value - else - read -s -p " $message: " value < /dev/tty 2>/dev/null || read -s -p " $message: " value +prompt_yes_no() { + local message="$1" + local default_value="${2:-n}" + local value + value="$(prompt_tty "$message" "$default_value")" + [[ "$value" =~ ^[Yy]$ ]] +} + +prompt_required_tty() { + local message="$1" + local value="" + while true; do + value="$(prompt_tty "$message" "")" + if [[ -n "$value" ]]; then + echo "$value" + return 0 fi - echo - printf -v "$var_name" '%s' "${value:-$default}" + echo "[WARN] This value cannot be empty" + done } -# ── State tracking ───────────────────────────────────────────────────────────── -APT_CACHE_UPDATED=false -DOCKER_ALREADY_PRESENT=false -COMPOSE_ALREADY_PRESENT=false +resolve_script_dir() { + local src="${BASH_SOURCE[0]}" + while [ -h "$src" ]; do + local dir + dir="$(cd -P "$(dirname "$src")" >/dev/null 2>&1 && pwd)" + src="$(readlink "$src")" + [[ "$src" != /* ]] && src="$dir/$src" + done + cd -P "$(dirname "$src")" >/dev/null 2>&1 && pwd +} + +SCRIPT_DIR="$(resolve_script_dir)" + +# -------------------------- +# OS / Environment Detection +# -------------------------- +OS="$(uname -s || true)" +ARCH="$(uname -m || true)" + +is_linux() { [[ "${OS}" == "Linux" ]]; } +is_macos() { [[ "${OS}" == "Darwin" ]]; } -has_cmd() { command -v "$1" >/dev/null 2>&1; } -is_linux() { [[ "$(uname -s)" == "Linux" ]]; } -is_macos() { [[ "$(uname -s)" == "Darwin" ]]; } -is_wsl2() { grep -qi microsoft /proc/version 2>/dev/null; } docker_ok() { has_cmd docker; } docker_compose_ok() { - has_cmd docker && docker compose version >/dev/null 2>&1 && return 0 - has_cmd docker-compose && docker-compose version >/dev/null 2>&1 && return 0 - return 1 + if has_cmd docker && docker compose version >/dev/null 2>&1; then + return 0 + fi + if has_cmd docker-compose && docker-compose version >/dev/null 2>&1; then + return 0 + fi + return 1 } need_sudo_or_die() { - has_cmd sudo && return 0 - msg_error "'sudo' is required. Install it or run as root." + if ! has_cmd sudo; then + echo "ERROR: 'sudo' is required for dependency installation on this system." + echo "Please install sudo (or run as root) and re-run this script." + exit 1 + fi } +# -------------------------- +# APT Cache Management (Linux) +# -------------------------- safe_apt_update() { - [ "$APT_CACHE_UPDATED" = true ] && return 0 - msg_info "Updating apt cache..." - sudo env DEBIAN_FRONTEND=noninteractive apt-get update -q + # Only run apt-get update once per script execution + if [ "$APT_CACHE_UPDATED" = false ]; then + echo "[INFO] Updating apt cache..." + sudo env DEBIAN_FRONTEND=noninteractive apt-get update APT_CACHE_UPDATED=true + fi } -is_pkg_installed() { dpkg -l "$1" 2>/dev/null | grep -q "^ii"; } +# -------------------------- +# Package Check Utilities +# -------------------------- +is_package_installed() { + # Check if a Debian package is installed + # Usage: is_package_installed package-name + dpkg -l "$1" 2>/dev/null | grep -q "^ii" +} -check_user_in_docker_group() { - [[ "${EUID}" -eq 0 ]] && return 0 - has_cmd getent && getent group docker >/dev/null 2>&1 && \ - id -nG "$USER" 2>/dev/null | grep -qw docker +# -------------------------- +# Linux Dependencies +# -------------------------- +install_linux_build_deps() { + if has_cmd apt-get; then + # Check if build dependencies are already installed + if is_package_installed build-essential && has_cmd gcc; then + echo "[OK] Build dependencies (gcc, build-essential) already installed. Skipping." + return 0 + fi + + need_sudo_or_die + echo "[INFO] Installing build dependencies (gcc, build-essential)..." + + safe_apt_update + + # REMOVED: apt-get upgrade -y + # This was causing failures due to system-wide package upgrades + # apt-get install will get the latest available versions anyway + + sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y gcc build-essential + + echo "[OK] Build dependencies installed." + else + echo "[WARN] Detected Linux, but 'apt-get' is not available. Skipping build dependency install." + fi } -add_user_to_docker_group() { - check_user_in_docker_group && { msg_ok "User '$USER' already in docker group"; return 0; } - has_cmd getent && getent group docker >/dev/null 2>&1 && [[ "${EUID}" -ne 0 ]] && \ - sudo usermod -aG docker "$USER" >/dev/null 2>&1 || true - msg_ok "User added to docker group (re-login may be required)" +ensure_curl_on_linux() { + if has_cmd curl; then + echo "[OK] curl is already installed." + return 0 + fi + + if has_cmd apt-get; then + need_sudo_or_die + echo "[INFO] Installing curl (required for Docker install script)..." + safe_apt_update + sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates + echo "[OK] curl installed." + return 0 + fi + + echo "[WARN] curl is not installed and apt-get is unavailable. Please install curl and re-run." + return 1 } -# ── Banner ───────────────────────────────────────────────────────────────────── -echo "" -echo -e "${BOLD}══════════════════════════════════════════════${RESET}" -echo -e " ${BOLD}Hummingbot API Setup${RESET}" -echo -e "${BOLD}══════════════════════════════════════════════${RESET}" -echo "" -msg_info "OS: $(uname -s) ARCH: $(uname -m)" -echo "" +# -------------------------- +# Docker Install / Validation +# -------------------------- +check_user_in_docker_group() { + # Check if current user is already in docker group + if [[ "${EUID}" -eq 0 ]]; then + # Running as root, no need for docker group + return 0 + fi + + if has_cmd getent && getent group docker >/dev/null 2>&1; then + if id -nG "$USER" 2>/dev/null | grep -qw docker; then + return 0 + fi + fi + + return 1 +} -# ── Linux build deps ─────────────────────────────────────────────────────────── -if is_linux && has_cmd apt-get; then - if is_pkg_installed build-essential && has_cmd gcc; then - msg_ok "Build dependencies already installed" - else - need_sudo_or_die - msg_info "Installing build dependencies (gcc, build-essential)..." - safe_apt_update - sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y gcc build-essential - msg_ok "Build dependencies installed" +add_user_to_docker_group() { + # Only add user to docker group if not already a member + if check_user_in_docker_group; then + echo "[OK] User '$USER' is already in the 'docker' group." + return 0 + fi + + if has_cmd getent && getent group docker >/dev/null 2>&1; then + if [[ "${EUID}" -ne 0 ]]; then + echo "[INFO] Adding current user to 'docker' group (may require re-login)..." + sudo usermod -aG docker "$USER" >/dev/null 2>&1 || true + echo "[OK] User added to docker group. You may need to log out and back in for this to take effect." fi -fi + fi +} -# ── Docker ───────────────────────────────────────────────────────────────────── install_docker_linux() { - need_sudo_or_die - if ! has_cmd curl; then - msg_info "Installing curl..." - safe_apt_update - sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates - fi - msg_info "Installing Docker via get.docker.com..." - curl -fsSL https://get.docker.com -o get-docker.sh - sudo sh get-docker.sh - rm -f get-docker.sh - if has_cmd systemctl && systemctl is-system-running >/dev/null 2>&1; then - sudo systemctl enable docker 2>/dev/null || true - sudo systemctl start docker 2>/dev/null || true + need_sudo_or_die + ensure_curl_on_linux + + echo "[INFO] Docker not found. Installing Docker using get.docker.com script..." + curl -fsSL https://get.docker.com -o get-docker.sh + sudo sh get-docker.sh + rm -f get-docker.sh + + if has_cmd systemctl; then + if systemctl is-system-running >/dev/null 2>&1; then + echo "[INFO] Enabling and starting Docker service..." + sudo systemctl enable docker 2>/dev/null || true + sudo systemctl start docker 2>/dev/null || true fi - add_user_to_docker_group + fi + + add_user_to_docker_group } -ensure_docker() { - if is_linux; then +ensure_docker_and_compose() { + if is_linux; then + # Check Docker installation + if docker_ok; then + echo "[OK] Docker already installed: $(docker --version 2>/dev/null || echo 'version unknown')" + DOCKER_ALREADY_PRESENT=true + + # Even if Docker is installed, ensure user is in docker group + add_user_to_docker_group + else + # Check if Docker binary exists but isn't in PATH + if [ -x "/usr/bin/docker" ] || [ -x "/usr/local/bin/docker" ]; then + echo "[INFO] Docker found but not in current PATH. Adding to PATH..." + export PATH="/usr/bin:/usr/local/bin:$PATH" + if docker_ok; then - msg_ok "Docker $(docker --version 2>/dev/null | head -1)" - DOCKER_ALREADY_PRESENT=true - add_user_to_docker_group + echo "[OK] Docker is now accessible: $(docker --version 2>/dev/null || echo 'version unknown')" + DOCKER_ALREADY_PRESENT=true + add_user_to_docker_group else - install_docker_linux + install_docker_linux fi - docker_ok || msg_error "Docker installation failed. Open a new shell and re-run." + else + install_docker_linux + fi + fi - if docker_compose_ok; then - msg_ok "Docker Compose available" - COMPOSE_ALREADY_PRESENT=true + # Verify Docker is actually working + if ! docker_ok; then + echo "ERROR: Docker installation did not succeed or 'docker' is still not on PATH." + echo " Try opening a new shell and re-running, or verify Docker installation." + exit 1 + fi + + # Check Docker Compose installation + if docker_compose_ok; then + echo "[OK] Docker Compose already available" + COMPOSE_ALREADY_PRESENT=true + + # Show which version we detected + if docker compose version >/dev/null 2>&1; then + echo "[OK] Using Docker Compose plugin: $(docker compose version 2>/dev/null || echo 'version unknown')" + else + echo "[OK] Using standalone docker-compose: $(docker-compose version 2>/dev/null || echo 'version unknown')" + fi + else + # Try to install docker-compose-plugin + if has_cmd apt-get; then + # Check if plugin package is already installed but not working + if is_package_installed docker-compose-plugin; then + echo "[WARN] docker-compose-plugin package is installed but not functioning properly." + echo "[INFO] Attempting to reinstall docker-compose-plugin..." + need_sudo_or_die + safe_apt_update + sudo env DEBIAN_FRONTEND=noninteractive apt-get install --reinstall -y docker-compose-plugin || true else - if has_cmd apt-get; then - need_sudo_or_die - msg_info "Installing docker-compose-plugin..." - safe_apt_update - sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y docker-compose-plugin || true - fi + need_sudo_or_die + echo "[INFO] Docker Compose not found. Attempting to install docker-compose-plugin..." + safe_apt_update + sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y docker-compose-plugin || true fi - docker_compose_ok || msg_error "Docker Compose not available. Try: sudo apt-get install -y docker-compose-plugin" + fi + fi - elif is_macos; then - docker_ok && docker_compose_ok || \ - msg_error "Docker Desktop not found. Install it from https://www.docker.com/products/docker-desktop and re-run." - msg_ok "Docker $(docker --version 2>/dev/null | head -1)" - msg_ok "Docker Compose available" + # Final verification of Docker Compose + if ! docker_compose_ok; then + echo "ERROR: Docker Compose is not available." + echo " Expected either 'docker compose' (v2) or 'docker-compose' (v1)." + echo " On Ubuntu/Debian, try: sudo apt-get install -y docker-compose-plugin" + exit 1 + fi + + elif is_macos; then + if ! docker_ok || ! docker_compose_ok; then + echo "ERROR: Docker and/or Docker Compose not found on macOS." + echo " Install Docker Desktop for Mac (Apple Silicon or Intel) and re-run this script." + echo " After installation, ensure 'docker' works in this terminal (you may need a new shell)." + exit 1 + fi + + echo "[OK] Docker detected: $(docker --version 2>/dev/null || echo 'version unknown')" + if docker compose version >/dev/null 2>&1; then + echo "[OK] Docker Compose detected: $(docker compose version 2>/dev/null || echo 'version unknown')" + else + echo "[OK] Docker Compose detected: $(docker-compose version 2>/dev/null || echo 'version unknown')" + fi + + else + echo "[WARN] Unsupported/unknown OS '${OS}'. Proceeding without installing OS-level dependencies." + if ! docker_ok || ! docker_compose_ok; then + echo "ERROR: Docker and/or Docker Compose not found." + exit 1 + fi + + echo "[OK] Docker detected: $(docker --version 2>/dev/null || echo 'version unknown')" + if docker compose version >/dev/null 2>&1; then + echo "[OK] Docker Compose detected: $(docker compose version 2>/dev/null || echo 'version unknown')" else - docker_ok && docker_compose_ok || msg_error "Docker and Docker Compose are required." + echo "[OK] Docker Compose detected: $(docker-compose version 2>/dev/null || echo 'version unknown')" fi + fi +} + +# -------------------------- +# Pull Hummingbot Docker Image +# -------------------------- +pull_hummingbot_image() { + echo "[INFO] Pulling latest Hummingbot image (hummingbot/hummingbot:latest)..." + if docker pull hummingbot/hummingbot:latest; then + echo "[OK] Hummingbot image pulled successfully." + else + echo "[WARN] Could not pull hummingbot/hummingbot:latest (network issue?). You may need to run 'docker pull hummingbot/hummingbot:latest' manually." + fi } -ensure_docker +# -------------------------- +# Pre-flight (deps + docker) +# -------------------------- +echo "[INFO] OS=${OS} ARCH=${ARCH}" + +if is_linux; then + install_linux_build_deps +fi + +ensure_docker_and_compose +# Show summary of what was done echo "" -msg_info "Pulling latest Hummingbot image..." -if docker pull hummingbot/hummingbot:latest 2>/dev/null; then - msg_ok "hummingbot/hummingbot:latest pulled" +if [ "$DOCKER_ALREADY_PRESENT" = true ] && [ "$COMPOSE_ALREADY_PRESENT" = true ]; then + echo "[OK] All dependencies were already installed. No changes made." +elif [ "$DOCKER_ALREADY_PRESENT" = true ]; then + echo "[OK] Docker was already installed. Docker Compose has been set up." +elif [ "$COMPOSE_ALREADY_PRESENT" = true ]; then + echo "[OK] Docker has been installed. Docker Compose was already available." else - msg_warn "Could not pull hummingbot image — run 'docker pull hummingbot/hummingbot:latest' later" + echo "[OK] Docker and Docker Compose have been installed." fi + echo "" -# ── Idempotency check ────────────────────────────────────────────────────────── +# Always pull latest Hummingbot image (first install and upgrade) +pull_hummingbot_image + +echo "" + +# -------------------------- +# Existing .env creation flow +# -------------------------- if [ -f ".env" ]; then - msg_ok ".env already exists — skipping setup" + echo ".env file already exists. Skipping setup." + echo "" + + # Ensure sentinel file exists + if [ ! -f ".setup-complete" ]; then touch .setup-complete - echo "" - exit 0 + fi + + exit 0 fi -# ── Credentials ──────────────────────────────────────────────────────────────── -echo -e "${BOLD}══════════════════════════════════════════════${RESET}" -echo -e " ${BOLD}API Credentials${RESET}" -echo -e "${BOLD}══════════════════════════════════════════════${RESET}" -echo "" - -prompt_visible "API username" "admin" USERNAME -prompt_secret "API password" "admin" PASSWORD -prompt_secret "Config password" "admin" CONFIG_PASSWORD +# Clear screen before prompting user (only if running interactively) +if [[ -t 0 ]] && [[ -c /dev/tty ]]; then + if has_cmd clear; then + clear + else + printf "\033c" + fi +fi -# ── Tailscale ────────────────────────────────────────────────────────────────── +echo "Hummingbot API Setup" echo "" -echo -e "${BOLD}══════════════════════════════════════════════${RESET}" -echo -e " ${BOLD}Tailscale (optional)${RESET}" -echo -e "${BOLD}══════════════════════════════════════════════${RESET}" -echo "" -echo -e " Use Tailscale to make this API securely accessible from Condor" -echo -e " without exposing port 8000 to the public internet." +echo "Set API credentials (use a strong username, password, and config password):" echo "" -prompt_visible "Enable Tailscale? [y/N]" "N" _use_tailscale +USERNAME="$(prompt_required_tty "API username: ")" +PASSWORD="$(prompt_required_tty "API password: ")" +CONFIG_PASSWORD="$(prompt_required_tty "Config password: ")" +# -------------------------- +# Tailscale Configuration +# -------------------------- TAILSCALE_ENABLED=false TAILSCALE_AUTH_KEY="" TAILSCALE_HOSTNAME="hummingbot-api" -if [[ "${_use_tailscale:-}" =~ ^[Yy]$ ]]; then - echo "" - echo -e " ${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" - echo -e " ${CYAN} How to get a Tailscale auth key:${RESET}" - echo -e " ${CYAN} 1. Create a free account at https://tailscale.com${RESET}" - echo -e " ${CYAN} 2. Go to: https://tailscale.com/admin/settings/keys${RESET}" - echo -e " ${CYAN} 3. Click 'Generate auth key'${RESET}" - echo -e " ${CYAN} 4. Check 'Reusable' for multiple server deployments${RESET}" - echo -e " ${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" - echo "" - while true; do - prompt_visible "Tailscale auth key (tskey-auth-...)" "" TAILSCALE_AUTH_KEY - if [[ -z "${TAILSCALE_AUTH_KEY:-}" ]]; then - msg_warn "Auth key cannot be empty" - continue - fi - if [[ ! "$TAILSCALE_AUTH_KEY" =~ ^tskey-auth- ]]; then - msg_warn "Auth key must start with 'tskey-auth-'" - continue - fi - break - done - TAILSCALE_ENABLED=true - msg_ok "Tailscale will be enabled — hostname: $TAILSCALE_HOSTNAME" +if prompt_yes_no "Use Tailscale for secure private networking? [y/N]: " "n"; then + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " How to get a Tailscale auth key:" + echo " 1. Create a free account at https://tailscale.com" + echo " 2. Go to: https://tailscale.com/admin/settings/keys" + echo " 3. Click 'Generate auth key'" + echo " 4. Check 'Reusable' for multiple server deployments" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + while true; do + TAILSCALE_AUTH_KEY="$(prompt_tty "Tailscale auth key (tskey-auth-...): " "")" + if [[ -z "$TAILSCALE_AUTH_KEY" ]]; then + echo "[WARN] Auth key cannot be empty" + continue + fi + if [[ ! "$TAILSCALE_AUTH_KEY" =~ ^tskey-auth- ]]; then + echo "[WARN] Auth key must start with 'tskey-auth-'" + continue + fi + break + done + # Hostname defaults to "hummingbot-api" — override via TAILSCALE_HOSTNAME in .env if needed + TAILSCALE_ENABLED=true fi -# ── Write .env ───────────────────────────────────────────────────────────────── cat > .env << EOF # Hummingbot API Configuration USERNAME=$USERNAME @@ -262,35 +466,24 @@ TAILSCALE_HOSTNAME=$TAILSCALE_HOSTNAME EOF touch .setup-complete -msg_ok ".env created" -# ── Summary ──────────────────────────────────────────────────────────────────── echo "" -echo -e "${BOLD}══════════════════════════════════════════════${RESET}" -echo -e " ${GREEN}Setup complete!${RESET}" +echo ".env created successfully!" +echo "" +echo "Next steps:" echo "" -echo -e " ${BOLD}Docker deployment (recommended for VPS):${RESET}" -echo -e " make deploy" +echo "Option 1: Start all services with Docker (recommended)" +echo " make deploy" echo "" -echo -e " ${BOLD}Source / dev mode (requires conda):${RESET}" -echo -e " make install ${CYAN}# create conda environment${RESET}" -echo -e " make run ${CYAN}# start API${RESET}" +echo "Option 2: Run API locally (dev mode)" +echo " make install # Creates the conda environment - Note: Please install the latest Anaconda version manually" +echo " make run # Run API (installs and connects Tailscale automatically if TAILSCALE_ENABLED=true)" if [ "$TAILSCALE_ENABLED" = true ]; then - echo "" - echo -e " ${BOLD}Tailscale:${RESET}" - echo -e " Docker: Tailscale sidecar starts automatically with 'make deploy'" - echo -e " Source: Tailscale installs and connects automatically with 'make run'" - echo -e " API URL: http://$TAILSCALE_HOSTNAME:8000 ${CYAN}(Tailscale access)${RESET}" - echo -e " Status: make tailscale-status" - echo "" - echo -e " ${BOLD}Accessing from Condor:${RESET}" - echo -e " ${CYAN} Install Tailscale on the Condor machine and connect with the same key:${RESET}" - if is_wsl2; then - echo -e " curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up --authkey=$TAILSCALE_AUTH_KEY" - else - echo -e " Linux / WSL: curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up --authkey=$TAILSCALE_AUTH_KEY" - echo -e " macOS / Win: https://tailscale.com/download — then run: sudo tailscale up --authkey=$TAILSCALE_AUTH_KEY" - fi + echo "" + echo "Tailscale:" + echo " Docker deploy: Tailscale sidecar starts automatically with 'make deploy'" + echo " Source run: Tailscale installs and connects automatically with 'make run'" + echo " Condor URL: http://$TAILSCALE_HOSTNAME:8000" + echo " Status: make tailscale-status" fi -echo -e "${BOLD}══════════════════════════════════════════════${RESET}" echo "" From 10d9052c793de42e008b2e0e3d935d340836caa3 Mon Sep 17 00:00:00 2001 From: david-hummingbot <85695272+david-hummingbot@users.noreply.github.com> Date: Wed, 20 May 2026 00:53:27 +0800 Subject: [PATCH 6/6] Enhance Tailscale setup and command structure Updated the Makefile to improve Tailscale integration and streamline run and deploy commands. --- Makefile | 61 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/Makefile b/Makefile index cf25ff98..8e6c0e6b 100644 --- a/Makefile +++ b/Makefile @@ -8,35 +8,32 @@ $(SETUP_SENTINEL): chmod +x setup.sh ./setup.sh -# Run locally (dev mode) — Tailscale-aware: reads TAILSCALE_ENABLED from .env +# Run locally (dev mode) +# When TAILSCALE_ENABLED=true: installs Tailscale if needed, connects, configures tailscale serve, +# then binds uvicorn to 127.0.0.1 only (tailscale serve exposes port 8000 on the tailnet) run: + docker compose up emqx postgres -d @set -a; [ -f .env ] && . ./.env; set +a; \ if [ "$${TAILSCALE_ENABLED:-false}" = "true" ]; then \ - echo "[INFO] Tailscale enabled — checking connection..."; \ + echo "[INFO] Tailscale mode: setting up Tailscale for source install..."; \ if ! command -v tailscale >/dev/null 2>&1; then \ - echo "[ERROR] Tailscale is not installed. Install it or set TAILSCALE_ENABLED=false in .env."; \ - exit 1; \ - fi; \ - if grep -qi microsoft /proc/version 2>/dev/null && ! pgrep -x tailscaled >/dev/null 2>&1; then \ - echo "[INFO] Starting Tailscale daemon (WSL2)..."; \ - sudo mkdir -p /var/run/tailscale /var/lib/tailscale; \ - sudo tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/var/run/tailscale/tailscaled.sock >/dev/null 2>&1 & sleep 2; \ + echo "[INFO] Installing Tailscale..."; \ + curl -fsSL https://tailscale.com/install.sh | sh; \ fi; \ if ! tailscale status >/dev/null 2>&1; then \ - echo "[INFO] Connecting to Tailscale..."; \ - if [ -n "$${TAILSCALE_AUTH_KEY:-}" ]; then \ - sudo tailscale up --authkey="$${TAILSCALE_AUTH_KEY}" --hostname=$${TAILSCALE_HOSTNAME:-hummingbot-api} --accept-dns=true; \ - else \ - sudo tailscale up --hostname=$${TAILSCALE_HOSTNAME:-hummingbot-api} --accept-dns=true; \ - fi; \ - else \ - echo "[INFO] Tailscale already connected."; \ + echo "[INFO] Connecting to Tailscale network..."; \ + sudo tailscale up --authkey="$${TAILSCALE_AUTH_KEY}" --hostname="$${TAILSCALE_HOSTNAME:-hummingbot-api}" --accept-dns=true; \ fi; \ + tailscale serve status 2>/dev/null | grep -q ":8000" || \ + sudo tailscale serve --bg http:8000 http://localhost:8000; \ + echo "[INFO] Binding uvicorn to 127.0.0.1 (tailscale serve exposes port 8000 on tailnet)"; \ + conda run --no-capture-output -n hummingbot-api uvicorn main:app --reload --host 127.0.0.1 --port 8000; \ + else \ + conda run --no-capture-output -n hummingbot-api uvicorn main:app --reload; \ fi - docker compose up emqx postgres -d - conda run --no-capture-output -n hummingbot-api uvicorn main:app --reload -# Deploy with Docker (Tailscale-aware: reads TAILSCALE_ENABLED from .env) +# Deploy with Docker +# When TAILSCALE_ENABLED=true: adds the Tailscale sidecar compose override deploy: $(SETUP_SENTINEL) @set -a; [ -f .env ] && . ./.env; set +a; \ if [ "$${TAILSCALE_ENABLED:-false}" = "true" ]; then \ @@ -46,6 +43,23 @@ deploy: $(SETUP_SENTINEL) docker compose up -d; \ fi +TAILSCALE_CONTAINER := hummingbot-tailscale + +# Show Tailscale connection status (Docker sidecar or local install) +tailscale-status: + @if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx '$(TAILSCALE_CONTAINER)'; then \ + echo "[INFO] Tailscale sidecar (Docker)"; \ + docker exec $(TAILSCALE_CONTAINER) tailscale status; \ + elif command -v tailscale >/dev/null 2>&1; then \ + echo "[INFO] Tailscale (local)"; \ + tailscale status; \ + else \ + echo "Tailscale is not available."; \ + echo " Docker deploy: ensure TAILSCALE_ENABLED=true and run 'make deploy'"; \ + echo " Source run: use 'make run' with Tailscale enabled (installs locally)"; \ + exit 1; \ + fi + # Stop all services stop: docker compose down @@ -75,10 +89,3 @@ install-pre-commit: # Build Docker image build: docker build -t hummingbot/hummingbot-api:latest . -# Show Tailscale connection status -tailscale-status: - @if command -v tailscale >/dev/null 2>&1; then \ - tailscale status; \ - else \ - echo "Tailscale is not installed or not on PATH."; \ - fi