Skip to content

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/futu and /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):

/etc/futu-opend/env
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 in ps output.
  • 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:

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:

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:

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

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

Caddy handles certificate issuance and renewal automatically.

7. Monitoring & alerts

  • Prometheus scrapes <gateway>:22222/metrics and <mcp>:38765/metrics.
  • Grafana dashboard: see Audit & observability.
  • Alerts: spike in futu_auth_events_total{outcome="reject"} → attack or misconfig; sustained non-zero futu_auth_limit_rejects_total{reason="rate"} → a key is being sprayed.

8. Logs & audit

  • journalctl -u futu-opend for 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:
/var/log/futu/*.log {
    daily
    rotate 30
    compress
    missingok
    notifempty
    su futu 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-log configured; logs are rotating.
  • /metrics scraped by Prometheus; Grafana dashboard shows data.
  • /health reachable; LB / k8s probes point at it.
  • FUTU_TRADE_PWD used only by futucli 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_machines bound for critical keys.
  • New binary passes scripts/run_test.sh regression before rollout.

Done.

Next