Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: setup run deploy stop install uninstall build install-pre-commit tailscale-status reset
.PHONY: setup run deploy stop install uninstall build install-pre-commit tailscale-status doctor reset

SETUP_SENTINEL := .setup-complete

Expand Down Expand Up @@ -43,16 +43,27 @@ deploy: $(SETUP_SENTINEL)
docker compose up -d; \
fi

# Verify dependencies, .env, containers, port exposure and API access.
# Read-only; exits non-zero when a check actually fails.
doctor:
@chmod +x doctor.sh && ./doctor.sh

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; \
echo ""; \
echo "[INFO] Serve status (confirms port 8000 is proxied, not just tailnet-joined):"; \
docker exec $(TAILSCALE_CONTAINER) tailscale serve status; \
elif command -v tailscale >/dev/null 2>&1; then \
echo "[INFO] Tailscale (local)"; \
tailscale status; \
echo ""; \
echo "[INFO] Serve status:"; \
tailscale serve status; \
else \
echo "Tailscale is not available."; \
echo " Docker deploy: ensure TAILSCALE_ENABLED=true and run 'make deploy'"; \
Expand Down
32 changes: 28 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A REST API for managing Hummingbot trading bots across multiple exchanges, with
>
> Hummingbot API controls real trading: orders, balances, bots, and stored exchange keys. That has always required strong passwords and careful configuration—but **the risk surface has grown**. Tools like **MCP**, **Condor agents**, and other AI assistants make powerful API actions easier to trigger, while cloud VPSes are constantly scanned for open ports like **8000**.
>
> **Tailscale is one safeguard you can add**: it puts the API on a private encrypted network so only your devices can reach it, without publishing port 8000 to the internet. It does **not** replace proper security—use strong API and config passwords, keep exchange keys protected, and avoid exposing sensitive services publicly. Tailscale also works when the API and clients run on the **same machine**.
> **Tailscale is one safeguard you can add**: it puts the API on a private encrypted network so only your devices can reach it, without publishing port 8000 to the internet. It does **not** replace proper security—use strong API and config passwords, keep exchange keys protected, and avoid exposing sensitive services publicly. Tailscale also works when the API and clients run on the **same machine**. But if your only client is co-located too (e.g. Condor deploying this API for itself), it already reaches you over `localhost` regardless — this API's own tailnet node is only needed for *other* devices to reach it directly.

## Quick Start

Expand Down Expand Up @@ -60,7 +60,8 @@ Use your API username and password for all requests. **Do not open port 8000 on
| `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 |
| `make tailscale-status` | Show Tailscale connection + serve (proxy) status |
| `make doctor` | Verify dependencies, `.env`, containers, port exposure and API access |

## Services

Expand All @@ -74,6 +75,11 @@ After hummingbot-api is running, these services are available:
| **EMQX** | localhost:1883 | — | MQTT broker |
| **EMQX Dashboard** | http://localhost:18083 | — | Broker admin (admin/public) |

PostgreSQL and EMQX are bound to `127.0.0.1` only — they're never reachable
from another machine, on the tailnet or otherwise. Bot containers still reach
them fine (they run with `network_mode: host`, so `127.0.0.1` is their host's
loopback too).

## Connect AI Assistant (MCP)

> **Production:** use `http://hummingbot-api:8000` (MagicDNS) instead of `localhost` when MCP runs on a different device than the API. Both must be on the same Tailscale account.
Expand Down Expand Up @@ -145,6 +151,11 @@ GATEWAY_URL=... # Gateway URL (for DEX)
TAILSCALE_ENABLED=true
TAILSCALE_AUTH_KEY=tskey-auth-...
TAILSCALE_HOSTNAME=hummingbot-api # MagicDNS hostname on your tailnet

# Docker port binding for hummingbot-api:8000 (set automatically by setup.sh:
# 127.0.0.1 when Tailscale is enabled, 0.0.0.0 otherwise) — see
# docker-compose.yml / docker-compose.tailscale.yml
API_BIND_HOST=127.0.0.1
```

Edit `.env` and restart with `make deploy` to apply changes.
Expand Down Expand Up @@ -189,7 +200,7 @@ When `TAILSCALE_ENABLED=true`, this automatically runs:
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.
A Tailscale sidecar container joins your tailnet using `network_mode: host`. `setup.sh` also writes `API_BIND_HOST=127.0.0.1` to `.env`, so `hummingbot-api`'s port 8000 is bound to loopback only — it does not sit on any public interface. The sidecar's `tailscale serve` config (`tailscale-serve.json`) is what makes it reachable again: it forwards the tailnet IP's `:8000` to `127.0.0.1:8000`, so the API ends up reachable at `http://hummingbot-api:8000` from any device on the same tailnet, and *only* from the tailnet.

### Connecting MCP tools via Tailscale

Expand All @@ -213,10 +224,13 @@ When `TAILSCALE_ENABLED=true`, `make run` will automatically install Tailscale i
make tailscale-status
```

This shows both tailnet peer status and `tailscale serve status` — confirm
port 8000 shows up as forwarded, not just that the node joined the tailnet.

From another device on your tailnet:

```bash
curl -u YOUR_USERNAME:YOUR_PASSWORD http://hummingbot-api:8000/health
curl -u YOUR_USERNAME:YOUR_PASSWORD http://hummingbot-api:8000/
```

## API Features
Expand All @@ -239,6 +253,16 @@ make run # Run with hot-reload

## Troubleshooting

**Start here:**
```bash
make doctor
```
Checks dependencies, `.env` (including credentials still left at well-known
defaults), the `hummingbot-api` / `hummingbot-broker` / `hummingbot-postgres`
containers, which ports are on a public interface, Tailscale's tailnet *and*
serve status, and whether the API actually answers an authenticated request.
Read-only, and it names the fix for whatever it finds.

**API won't start?**
```bash
docker compose logs hummingbot-api
Expand Down
11 changes: 9 additions & 2 deletions docker-compose.tailscale.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
# 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.
# on the host. hummingbot-api's port 8000 is bound to 127.0.0.1 only whenever
# this overlay is used (setup.sh writes API_BIND_HOST=127.0.0.1 to .env), so
# TS_SERVE_CONFIG below is what actually makes port 8000 reachable on the
# tailnet — it forwards the tailnet IP's :8000 to 127.0.0.1:8000, which this
# sidecar can reach directly because it shares the host's network namespace.
# Without this, the API would be reachable on the tailnet AND still sitting
# on every other host interface at the same time.
#
# The base docker-compose.yml emqx-bridge network is unaffected — emqx and
# postgres still communicate with hummingbot-api normally via that bridge.
Expand All @@ -18,9 +23,11 @@ services:
- TS_STATE_DIR=/var/lib/tailscale
- TS_USERSPACE=false
- TS_HOSTNAME=${TAILSCALE_HOSTNAME:-hummingbot-api}
- TS_SERVE_CONFIG=/config/tailscale-serve.json
volumes:
- tailscale_state:/var/lib/tailscale
- /dev/net/tun:/dev/net/tun
- ./tailscale-serve.json:/config/tailscale-serve.json:ro
cap_add:
- NET_ADMIN
- NET_RAW
Expand Down
30 changes: 21 additions & 9 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ services:
image: hummingbot/hummingbot-api:latest
restart: unless-stopped
ports:
- "8000:8000"
# Bound to 127.0.0.1 automatically when Tailscale is enabled (setup.sh
# writes API_BIND_HOST=127.0.0.1 to .env) — the tailnet sidecar's
# `tailscale serve` (docker-compose.tailscale.yml) then forwards the
# tailnet's port 8000 to this loopback bind, so the API is never on a
# public interface at the same time. Left as 0.0.0.0 by default so a
# non-Tailscale deploy is unaffected.
- "${API_BIND_HOST:-0.0.0.0}:8000:8000"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
volumes:
- ./bots:/hummingbot-api/bots
- /var/run/docker.sock:/var/run/docker.sock
Expand Down Expand Up @@ -57,14 +63,18 @@ services:
- emqx-data:/opt/emqx/data
- emqx-log:/opt/emqx/log
- emqx-etc:/opt/emqx/etc
# Loopback-only: hummingbot-api reaches emqx over the emqx-bridge docker
# network, and bot containers (launched with network_mode=host) reach
# 127.0.0.1 just as well as 0.0.0.0 — nothing external ever needs these,
# including the admin dashboard on 18083 (default creds admin/public).
ports:
- "1883:1883" # mqtt:tcp
- "8883:8883" # mqtt:tcp:ssl
- "8083:8083" # mqtt:ws
- "8084:8084" # mqtt:ws:ssl
- "8081:8081" # http:management
- "18083:18083" # http:dashboard
- "61613:61613" # web-stomp gateway
- "127.0.0.1:1883:1883" # mqtt:tcp
- "127.0.0.1:8883:8883" # mqtt:tcp:ssl
- "127.0.0.1:8083:8083" # mqtt:ws
- "127.0.0.1:8084:8084" # mqtt:ws:ssl
- "127.0.0.1:8081:8081" # http:management
- "127.0.0.1:18083:18083" # http:dashboard
- "127.0.0.1:61613:61613" # web-stomp gateway
networks:
emqx-bridge:
aliases:
Expand All @@ -89,8 +99,10 @@ services:
- postgres-data:/var/lib/postgresql/data
# Init script as safety net - only runs on first initialization
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql:ro
# Loopback-only: only the hummingbot-api container talks to postgres,
# and it does so over the emqx-bridge docker network, not this port.
ports:
- "5432:5432"
- "127.0.0.1:5432:5432"
networks:
- emqx-bridge
healthcheck:
Expand Down
Loading
Loading