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 --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):
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:
EnvironmentFilecontains non-password settings only;LoadCredentialinjects the login-password file, so the password is absent frompsoutput.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:
# 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:
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:
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
}
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/metricswith HTTPS, CA trust, and a metrics-scope token.<mcp>:38765/metricskeeps its existing transport contract. - 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. -
/metricsis scraped by Prometheus with HTTPS, CA trust, and the metrics token; Grafana shows data. -
/livezis used for liveness;/healthchecks Platform/backend transport; container / LB / k8s readiness probes use HTTPS + CA trust and point at/readyz. -
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.