diff --git a/Makefile b/Makefile index 8c4a43aa..8e6c0e6b 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,56 @@ $(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 + +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: @@ -45,4 +88,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 . diff --git a/README.md b/README.md index 7e8c9953..5561149a 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,15 @@ The script clones **`hummingbot-api`**, runs **`make setup`** (creates **`.env`* That's it! The API is now running at http://localhost:8000 -- **API:** http://localhost:8000 -- **Swagger:** http://localhost:8000/docs +| Command | Description | +|---------|-------------| +| `make setup` | Create `.env` file with configuration | +| `make deploy` | Start all services (API, PostgreSQL, EMQX) | +| `make stop` | Stop all services | +| `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 @@ -89,6 +96,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 @@ -125,6 +195,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..76620de5 100755 --- a/setup.sh +++ b/setup.sh @@ -20,6 +20,46 @@ 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_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 "[WARN] This value cannot be empty" + done +} + resolve_script_dir() { local src="${BASH_SOURCE[0]}" while [ -h "$src" ]; do @@ -356,28 +396,45 @@ fi echo "Hummingbot API Setup" echo "" +echo "Set API credentials (use a strong username, password, and config password):" +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} +USERNAME="$(prompt_required_tty "API username: ")" +PASSWORD="$(prompt_required_tty "API password: ")" +CONFIG_PASSWORD="$(prompt_required_tty "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 Configuration +# -------------------------- +TAILSCALE_ENABLED=false +TAILSCALE_AUTH_KEY="" +TAILSCALE_HOSTNAME="hummingbot-api" -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 +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 -CONFIG_PASSWORD=${CONFIG_PASSWORD:-admin} cat > .env << EOF # Hummingbot API Configuration @@ -401,6 +458,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 +477,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 ""