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 --home-dir /var/lib/futu --no-create-home --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_MCP_API_KEY=fc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Store the login password separately at /etc/futu-opend/login-pwd (chmod 0600 root:root); never place it in the EnvironmentFile.

/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.8.0/futu-opend-rs-1.8.0-linux-x86_64.tar.gz
curl -LO https://futuapi.com/releases/rs-v1.8.0/futu-opend-rs-1.8.0-linux-x86_64.tar.gz.sha256
sha256sum -c futu-opend-rs-1.8.0-linux-x86_64.tar.gz.sha256
tar xf futu-opend-rs-1.8.0-linux-x86_64.tar.gz
cd futu-opend-rs-1.8.0
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
StartLimitIntervalSec=15min
StartLimitBurst=3

[Service]
Type=simple
User=futu
Group=futu
Environment=HOME=/var/lib/futu

# EnvironmentFile contains account routing and other non-password settings only.
EnvironmentFile=/etc/futu-opend/env
LoadCredential=login-pwd:/etc/futu-opend/login-pwd

ExecStart=/usr/local/bin/futu-opend \
    --login-account ${FUTU_ACCOUNT} \
    --login-pwd-file ${CREDENTIALS_DIRECTORY}/login-pwd \
    --rest-port 22222 \
    --grpc-port 33333 \
    --rest-keys-file /etc/futu-opend/keys.json \
    --rest-tls-cert /etc/futu-opend/rest-cert.pem \
    --rest-tls-key /etc/futu-opend/rest-key.pem \
    --grpc-keys-file /etc/futu-opend/keys.json \
    --audit-log /var/log/futu/

Restart=on-failure
RestartSec=60s

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

The daemon cert and key files must be readable by the futu user. The health-check, Prometheus, and reverse-proxy users must be able to read their CA files. Do not bypass certificate verification to work around permissions or trust-chain configuration.

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
StartLimitIntervalSec=15min
StartLimitBurst=3

[Service]
Type=simple
User=futu
Group=futu
Environment=HOME=/var/lib/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=60s

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

First complete one interactive authentication run with the same service user, HOME, and password file. If SMS is required, enter it during this step; only enable boot startup after setup succeeds:

# Temporarily let the service user write the password. read -s does not echo
# and the value never enters argv or shell history.
sudo install -o futu -g futu -m 0600 /dev/null /etc/futu-opend/login-pwd
sudo -u futu env HOME=/var/lib/futu bash -c '
  read -rsp "Login password: " password
  printf "\n" >&2
  printf "%s" "$password" > /etc/futu-opend/login-pwd
'
sudo -u futu env HOME=/var/lib/futu \
  /usr/local/bin/futu-opend --setup-only \
  --login-account ACCOUNT --login-pwd-file /etc/futu-opend/login-pwd
# After setup succeeds, make it the root-only systemd credential source.
sudo chown root:root /etc/futu-opend/login-pwd
sudo chmod 0600 /etc/futu-opend/login-pwd
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

Authentication, password, device, or SMS failures are limited to three starts within 15 minutes, at least 60 seconds apart. The unit then remains failed instead of retrying forever. After an operator fixes the cause, run:

sudo systemctl reset-failed futu-opend.service futu-mcp.service
sudo systemctl start futu-opend.service futu-mcp.service

Key hardening fields:

  • EnvironmentFile contains non-password settings only; LoadCredential injects the login-password file, so the password is absent from ps output.
  • User=futu / Group=futu — non-root.
  • NoNewPrivileges / ProtectSystem=strict / ReadWritePaths=... — systemd hardening.
  • Restart=on-failure + StartLimit* — transient failures restart, while operator-required authentication failures stop within a fixed budget.

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 curl 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 --cacert /etc/futu-opend/rest-ca.pem \
        --resolve localhost:22222:127.0.0.1 \
        https://localhost:22222/readyz || 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
      - --rest-tls-cert=/etc/futu-opend/rest-cert.pem
      - --rest-tls-key=/etc/futu-opend/rest-key.pem
      - --grpc-keys-file=/etc/futu/keys.json
      - --audit-log=/var/log/futu/
    volumes:
      - /etc/futu-opend/keys.json:/etc/futu/keys.json:ro
      - /etc/futu-opend/rest-cert.pem:/etc/futu-opend/rest-cert.pem:ro
      - /etc/futu-opend/rest-key.pem:/etc/futu-opend/rest-key.pem:ro
      - /etc/futu-opend/rest-ca.pem:/etc/futu-opend/rest-ca.pem: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
      - /etc/futu-opend/rest-ca.pem:/etc/prometheus/rest-ca.pem:ro
      - /etc/futu-opend/metrics-token:/etc/prometheus/rest-metrics-token:ro
    ports:
      - "127.0.0.1:9090:9090"

prometheus.yml:

/etc/futu-opend/metrics-token stores the raw token with metrics:read (or admin) scope. Mount it read-only and ensure the Prometheus user can read it.

scrape_configs:
  - job_name: futu-opend
    scheme: https
    tls_config:
      ca_file: /etc/prometheus/rest-ca.pem
      server_name: localhost
    authorization:
      credentials_file: /etc/prometheus/rest-metrics-token
    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 https://localhost:22222 {
        transport http {
            tls_server_name localhost
            tls_trust_pool file /etc/caddy/futu-opend-rest-ca.pem
        }
    }
}

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

Caddy handles client-facing certificate issuance and renewal automatically. Its connection to the daemon still verifies upstream TLS with /etc/caddy/futu-opend-rest-ca.pem, which must be readable by the Caddy user.

7. Monitoring & alerts

  • Prometheus scrapes <gateway>:22222/metrics with HTTPS, CA trust, and a metrics-scope token. <mcp>:38765/metrics keeps its existing transport contract.
  • 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 is scraped by Prometheus with HTTPS, CA trust, and the metrics token; Grafana shows data.
  • /livez is used for liveness; /health checks Platform/backend transport; container / LB / k8s readiness probes use HTTPS + CA trust and point at /readyz.
  • 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