Production deploy¶
A "go-live checklist" that ties everything together. Recommended stack: Docker + systemd + Prometheus.
Architecture¶
flowchart LR
LLM[LLM client<br/>Claude/GPT] -->|HTTPS<br/>Bearer| MCP[futu-mcp<br/>HTTP :38765]
Bot[trading bot<br/>Python/Go] -->|gRPC<br/>Bearer metadata| OpenD
Web[web frontend] -->|WS<br/>?token=| OpenD
Script[ops scripts] -->|REST<br/>Bearer| OpenD[futu-opend<br/>TCP :11111<br/>REST :22222<br/>gRPC :33333]
OpenD -->|FTAPI TCP| Futu[Futu backend]
MCP -->|FTAPI TCP| OpenD
OpenD --> Audit[(/var/log/futu/<br/>audit JSONL)]
MCP --> Audit
Prom[Prometheus] -->|scrape| OpenD
Prom -->|scrape| MCP
Graf[Grafana] --> Prom
1. Host prep¶
- OS: Ubuntu 22.04 / Debian 12 / RHEL 9 all work.
- Resources: 2 vCPU + 2 GB RAM fits a single instance; use 4 vCPU + 4 GB for higher concurrency.
- Network: egress to the Futu backend (
*.futunn.com); ingress only on the ports you expose. - User: non-root
futu:futu, owns/var/lib/futuand/var/log/futu.
sudo useradd --system --shell /usr/sbin/nologin futu
sudo install -d -o futu -g futu -m 0750 /var/lib/futu /var/log/futu
sudo install -d -o root -g futu -m 0750 /etc/futu-opend
2. Configuration files¶
/etc/futu-opend/env (chmod 0640 root:futu):
FUTU_ACCOUNT=12345678
FUTU_PWD=your_login_password
FUTU_MCP_API_KEY=fc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
/etc/futu-opend/keys.json (chmod 0640 root:futu): generate via
futucli gen-key.
3. Install the binary¶
cd /tmp
curl -LO https://futuapi.com/releases/rs-v1.4.26/futu-opend-rs-1.4.26-linux-x86_64.tar.gz
curl -LO https://futuapi.com/releases/rs-v1.4.26/futu-opend-rs-1.4.26-linux-x86_64.tar.gz.sha256
sha256sum -c futu-opend-rs-1.4.26-linux-x86_64.tar.gz.sha256
tar xf futu-opend-rs-1.4.26-linux-x86_64.tar.gz
sudo install -m 0755 futu-opend /usr/local/bin/
sudo install -m 0755 futu-mcp /usr/local/bin/
sudo install -m 0755 futucli /usr/local/bin/
See the Dockerfile snippet in the docker-compose section below.
No public Docker image is published.
4. systemd units¶
futu-opend.service¶
Save as /etc/systemd/system/futu-opend.service:
[Unit]
Description=FutuOpenD-rs Gateway (TCP/REST/gRPC/WS)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=futu
Group=futu
# Credentials from EnvironmentFile to avoid leaking via `ps`:
EnvironmentFile=/etc/futu-opend/env
ExecStart=/usr/local/bin/futu-opend \
--login-account ${FUTU_ACCOUNT} \
--login-pwd ${FUTU_PWD} \
--rest-port 22222 \
--grpc-port 33333 \
--rest-keys-file /etc/futu-opend/keys.json \
--grpc-keys-file /etc/futu-opend/keys.json \
--audit-log /var/log/futu/
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=futu-opend
# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/log/futu /var/lib/futu
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
RestrictRealtime=true
SystemCallArchitectures=native
CapabilityBoundingSet=
AmbientCapabilities=
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
futu-mcp.service¶
Save as /etc/systemd/system/futu-mcp.service (HTTP transport mode —
multiple LLMs share one server):
[Unit]
Description=FutuOpenD-rs MCP server (HTTP transport)
After=futu-opend.service network-online.target
Wants=futu-opend.service network-online.target
[Service]
Type=simple
User=futu
Group=futu
EnvironmentFile=/etc/futu-opend/env
ExecStart=/usr/local/bin/futu-mcp \
--gateway 127.0.0.1:11111 \
--http-listen 127.0.0.1:38765 \
--keys-file /etc/futu-opend/keys.json \
--audit-log /var/log/futu/mcp/
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=futu-mcp
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/log/futu /var/lib/futu
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
Enable¶
sudo systemctl daemon-reload
sudo systemctl enable --now futu-opend.service
sudo systemctl enable --now futu-mcp.service
# Logs
sudo journalctl -u futu-opend -f
Key hardening fields:
EnvironmentFile=/etc/futu-opend/env— credentials not inpsoutput.User=futu / Group=futu— non-root.NoNewPrivileges / ProtectSystem=strict / ReadWritePaths=...— systemd hardening.Restart=on-failure— auto-restart on crash.
5. Dockerfile + docker-compose¶
No public image yet; build your own. Grab the tarball, drop a
Dockerfile:
# runtime-only: assumes you have the linux-x86_64 tarball extracted
# into the build context
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates tzdata \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --system --gid 10001 futu \
&& useradd --system --uid 10001 --gid futu --no-create-home --shell /usr/sbin/nologin futu
COPY futu-opend /usr/local/bin/
COPY futu-mcp /usr/local/bin/
COPY futucli /usr/local/bin/
RUN install -d -o futu -g futu -m 0750 /var/lib/futu /var/log/futu
USER futu:futu
EXPOSE 11111 22222 33333 38765
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -fsS http://127.0.0.1:22222/health || exit 1
ENTRYPOINT ["futu-opend"]
docker-compose.yml:
version: "3.9"
services:
opend:
image: futu-opend-rs:1.2 # built locally
build: .
restart: unless-stopped
env_file: /etc/futu-opend/env
command:
- futu-opend
- --login-account=${FUTU_ACCOUNT}
- --login-pwd=${FUTU_PWD}
- --rest-port=22222
- --grpc-port=33333
- --rest-keys-file=/etc/futu/keys.json
- --grpc-keys-file=/etc/futu/keys.json
- --audit-log=/var/log/futu/
volumes:
- /etc/futu-opend/keys.json:/etc/futu/keys.json:ro
- /var/log/futu:/var/log/futu
ports:
- "11111:11111"
- "127.0.0.1:22222:22222" # REST bound to localhost (front with LB)
- "33333:33333"
mcp:
image: futu-opend-rs:1.2
restart: unless-stopped
depends_on: [opend]
env_file: /etc/futu-opend/env
command:
- futu-mcp
- --gateway=opend:11111
- --keys-file=/etc/futu/keys.json
- --http-listen=0.0.0.0:38765
- --audit-log=/var/log/futu/mcp/
volumes:
- /etc/futu-opend/keys.json:/etc/futu/keys.json:ro
- /var/log/futu:/var/log/futu
ports:
- "127.0.0.1:38765:38765"
prometheus:
image: prom/prometheus:latest
restart: unless-stopped
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
ports:
- "127.0.0.1:9090:9090"
prometheus.yml:
scrape_configs:
- job_name: futu-opend
static_configs: [{ targets: ['opend:22222'] }]
- job_name: futu-mcp
static_configs: [{ targets: ['mcp:38765'] }]
6. Reverse proxy (nginx / Caddy)¶
HTTPS termination is mandatory when exposing publicly. Caddy (automatic Let's Encrypt) recommended:
api.your-domain.com {
reverse_proxy localhost:22222
}
mcp.your-domain.com {
reverse_proxy localhost:38765
}
Caddy handles certificate issuance and renewal automatically.
7. Monitoring & alerts¶
- Prometheus scrapes
<gateway>:22222/metricsand<mcp>:38765/metrics. - Grafana dashboard: see Audit & observability.
- Alerts: spike in
futu_auth_events_total{outcome="reject"}→ attack or misconfig; sustained non-zerofutu_auth_limit_rejects_total{reason="rate"}→ a key is being sprayed.
8. Logs & audit¶
journalctl -u futu-opendfor regular logs./var/log/futu/futu-audit.log.*— daily-rotating audit JSONL (with--audit-log /var/log/futu/).- Use logrotate for archival + compression:
/etc/logrotate.d/futu:
9. Upgrade¶
# new binary replaces the old one in /usr/local/bin (same name)
sudo cp futu-opend-new /usr/local/bin/futu-opend
# restart
sudo systemctl restart futu-opend
sudo systemctl restart futu-mcp
SIGHUP hot-reload for keys.json (no restart):
sudo systemctl kill -s HUP futu-opend
sudo systemctl kill -s HUP futu-mcp
# or
sudo pkill -HUP futu-opend
10. Backup¶
/etc/futu-opend/keys.json— SHA-256 hashes of all keys; losing it invalidates every key. Regular encrypted backup./var/log/futu/— audit logs; retain long-term for compliance.~futu/.config/futu/— Futu backend session cache; loss means re-doing SMS verification on first launch.
Go-live checklist¶
- No wildcard-permission keys in
keys.json; every key has explicit scope + limits. -
--audit-logconfigured; logs are rotating. -
/metricsscraped by Prometheus; Grafana dashboard shows data. -
/healthreachable; LB / k8s probes point at it. -
FUTU_TRADE_PWDused only byfutucli unlock-trade, not in the systemd env. - Only port 443 (proxy) exposed externally; gateway ports bound to 127.0.0.1.
- Runbook exists for the ops team: how to handle key loss, revocation, SIGHUP reload.
-
allowed_machinesbound for critical keys. - New binary passes
scripts/run_test.shregression before rollout.
Done.