跳转至

部署到生产

一套"上线清单",把前面所有能力串起来。推荐:Docker + systemd + Prometheus

架构

flowchart LR
    LLM[LLM 客户端<br/>Claude/GPT] -->|HTTPS<br/>Bearer| MCP[futu-mcp<br/>HTTP :38765]
    Bot[交易 bot<br/>Python/Go] -->|gRPC<br/>Bearer metadata| OpenD
    Web[网页前端] -->|WS<br/>?token=| OpenD
    Script[运维脚本] -->|REST<br/>Bearer| OpenD[futu-opend<br/>TCP :11111<br/>REST :22222<br/>gRPC :33333]

    OpenD -->|FTAPI TCP| Futu[Futu 后端]
    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. 机器准备

  • OS:Ubuntu 22.04 / Debian 12 / RHEL 9 都行
  • 资源:2 vCPU + 2 GB RAM 够单实例用;高并发推 4 vCPU + 4 GB
  • 网络:能出访 Futu 后端(*.futunn.com),入访只需开你决定暴露的端口
  • 用户futu:futu 非 root,/var/lib/futu /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. 放配置

/etc/futu-opend/envchmod 0640 root:futu):

/etc/futu-opend/env
FUTU_ACCOUNT=12345678
FUTU_PWD=your_login_password
FUTU_MCP_API_KEY=fc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

/etc/futu-opend/keys.jsonchmod 0640 root:futu):用 futucli gen-key 生成。

3. 装二进制

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/

参考下面"docker-compose 版本"前的 Dockerfile 小节,用那份自己 build 就好。目前不发布公开 Docker image。

4. systemd unit

futu-opend.service

/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

# 凭证从 EnvironmentFile 读,避免命令行 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

# 加固
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

/etc/systemd/system/futu-mcp.service(HTTP transport 模式,多 LLM 共享):

[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

启用

sudo systemctl daemon-reload
sudo systemctl enable --now futu-opend.service
sudo systemctl enable --now futu-mcp.service

# 看日志
sudo journalctl -u futu-opend -f

关键加固字段

  • EnvironmentFile=/etc/futu-opend/env — 凭证不进 ps 输出
  • User=futu / Group=futu — 非 root 运行
  • NoNewPrivileges / ProtectSystem=strict / ReadWritePaths=... — systemd 加固
  • Restart=on-failure — 崩了自动重启

5. Dockerfile + docker-compose

目前不发布公开 image,自己打。把 tarball 下下来后,放一个 Dockerfile(或者用解压后的 binary 直接进镜像):

Dockerfile
# runtime-only:假设你已经从 futuapi.com/releases/... 下好 linux-x86_64 tarball
# 并解压到构建目录
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

docker-compose.yml
version: "3.9"
services:
  opend:
    image: futu-opend-rs:1.2        # 本地 build 的
    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 只允许本机 / 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. 反向代理(nginx / Caddy)

对外暴露 HTTPS 时必须加反代。推荐 Caddy 自动 Let's Encrypt:

/etc/caddy/Caddyfile
api.your-domain.com {
    reverse_proxy localhost:22222
}

mcp.your-domain.com {
    reverse_proxy localhost:38765
}
sudo systemctl reload caddy

Caddy 会自动申请证书、处理 renewal。

7. 监控 + 告警

  • Prometheus<gateway>:22222/metrics<mcp>:38765/metrics
  • Grafana dashboard审计与可观察性 里的 Grafana 示例
  • 告警futu_auth_events_total{outcome="reject"} 突增 → 攻击 / 配置错;futu_auth_limit_rejects_total{reason="rate"} 长期非零 → key 被 spray

8. 日志 + 审计

  • journalctl -u futu-opend 看常规日志
  • /var/log/futu/futu-audit.log.* 每日滚动的审计 JSONL(--audit-log /var/log/futu/
  • 用 logrotate 做归档压缩:/etc/logrotate.d/futu
/var/log/futu/*.log {
    daily
    rotate 30
    compress
    missingok
    notifempty
    su futu futu
}

9. 升级

# 新版 binary 覆盖到 /usr/local/bin(同名)
sudo cp futu-opend-new /usr/local/bin/futu-opend

# 重启
sudo systemctl restart futu-opend
sudo systemctl restart futu-mcp

SIGHUP 热重载 keys.json(不重启):

sudo systemctl kill -s HUP futu-opend
sudo systemctl kill -s HUP futu-mcp
# 或
sudo pkill -HUP futu-opend

10. 备份

  • /etc/futu-opend/keys.json —— 有 key 的 SHA-256 hash,丢了所有 key 全失效。定期 encrypted backup
  • /var/log/futu/ —— 审计日志,合规场景要长期保存
  • ~futu/.config/futu/ —— Futu 后端的 session 缓存,丢了首次启动要重走 SMS

上线检查单

  • keys.json 里没有 wildcard 权限的 key,每把都有明确 scope + 限额
  • --audit-log 配了,日志在轮转
  • /metrics 被 Prometheus 抓了,Grafana dashboard 能看到数据
  • /health 打得通,LB / k8s probe 指向它
  • FUTU_TRADE_PWD 的交易密码只在 futucli unlock-trade 里用,进 systemd env
  • 对外端口只开反代后的 443,网关端口 bind 127.0.0.1
  • 对接方有 runbook:key 丢了怎么办、怎么吊销、怎么 SIGHUP
  • allowed_machines 给关键 key 绑机器
  • 新 binary 上线前跑 scripts/run_test.sh 回归

搞定。

下一步