主题
09 — Deployment Pattern (Vultr Japan + cloudflared + Docker)
Status: 📐 Architecture proposal · 2026-05-07 Source: Direct port of PRD's deployment (
~/PRD/.claude/rules/deployment.md) with Docker isolation added on top Goal: Co-locate twilight-drive backend on the existing PRD Vultr Japan VPS without coupling failure modes, secrets, or resource budgets between the two projects
复用 PRD 已经验证过的 cloudflared tunnel + CF Access 模式;Docker 容器是两个项目之间的隔离边界
目标 (Goal)
把 twilight-drive 的 P1.0 backend 部署到 PRD 同一台 Vultr Japan VPS(避免新开机器的成本与运维复杂度),同时通过 Docker 容器把两个项目的故障域、文件系统、网络端口、systemd 命名空间彻底切开。
域名分配(已确认):
| 用途 | 域名 | 服务 |
|---|---|---|
| Backend API | api.fsagent.cc | cloudflared tunnel → Docker 容器 :8080 |
| Docs site | dev.fsagent.cc | Cloudflare Pages 自定义域 (existing) |
| (未来) 用户 dashboard | TBD | (P1.1+) |
PRD 已经在跑什么(不动)
Vultr Japan (Ubuntu 22.04, user openclaw, IP 139.180.196.53)
│
├── systemd user 服务(loginctl enable-linger 持久化)
│ ├── poly-trade-dashboard.service FastAPI on 127.0.0.1:8080
│ ├── poly-trade-cloudflared.service tunnel "prd-dashboard"
│ ├── poly-trade-{scan,copy,rebalance,pnl,qa}.timer
│ └── Docker: Fordefi MPC signer (--restart=always)
│
├── ~/.cloudflared/<UUID>.json (PRD tunnel creds, mode 600)
├── ~/.cloudflare-token (API token, IP-locked)
├── ~/prd/{.env, secrets, data, logs}/
│
└── Cloudflare zone fsagent.cc → tunnel "prd-dashboard" → dashboard.fsagent.cc提议加什么(隔离边界用 ▲▲▲ 标出)
Vultr Japan (same host)
│
├── (existing) PRD 的所有服务
│
├──▲▲▲ 隔离边界(Docker 容器 + 独立 home 子目录) ▲▲▲
│
├── Docker container "twilight-backend"
│ │ image: ghcr.io/lacatfly/twilight-drive:0.2.0 (固定 SHA)
│ │ binds: 127.0.0.1:8081 → 容器 :8080 (port 8080 已被 PRD 占用)
│ │ caps: drop ALL;no-new-privileges
│ │ filesystem: read-only + /tmp tmpfs
│ │ resources: cpus=1.5, memory=1500m
│ ├── volume: ~/twilight/data → /data (DuckDB cache)
│ ├── env_file: ~/twilight/.env (TUSHARE_TOKEN, BEARER_DB_PATH, ...)
│ └── healthcheck: GET /healthz
│
├── systemd user units(前缀 twilight-* 避免命名冲突)
│ ├── twilight-backend.service wraps `docker compose up`
│ └── twilight-cloudflared.service 独立 tunnel "twilight-backend"
│
├── ~/.cloudflared/twilight-backend-<UUID>.json (新 tunnel creds)
├── ~/.cloudflared/twilight-config.yml (独立配置文件)
├── ~/twilight/{.env, data, logs, compose.yml}/
│
└── Cloudflare zone fsagent.cc
├── (existing) tunnel "prd-dashboard" → dashboard.fsagent.cc
├── (new) tunnel "twilight-backend" → api.fsagent.cc
├── (new) Pages custom domain dev.fsagent.cc → twilight-drive.pages.dev
└── Access policies:
├── (existing) prd-dashboard: default-email-otp
└── (new) twilight-backend:
- early access: Email OTP for team
- P1.1 production: service tokens (one per profile, revocable)高风险服务问题与缓解(Risk Register)
1. 端口冲突 — PRD 占用 :8080
| 风险 | 影响 | 缓解 |
|---|---|---|
| 二者绑同一端口 → 后启动者崩 | backend 启动失败 | 容器对外 binds 127.0.0.1:8081;容器内部仍是 :8080,FastAPI 代码不需要知道宿主端口 |
| 端口范围耗尽(多 user profile 起来后) | P1.1 阶段问题 | Hermes 容器用 Docker 内部网络,不需要映射主机端口;iLink 是出站连接,不依赖入站端口 |
2. Secret 泄漏
| 风险 | 影响 | 缓解 |
|---|---|---|
| PRD 的 FORDEFI_API_TOKEN 被 twilight 容器读到 | 监管 + 资金风险 | Docker env_file: ~/twilight/.env(只这一个),不挂 ~/prd/;容器进程命名空间隔离 |
| twilight 的 TUSHARE_TOKEN 被 PRD 进程读到 | Tushare 配额耗尽 + token 失窃 | ~/twilight/.env mode 600 + 同一用户 (openclaw) 但容器 mount 仅 twilight 目录 |
| 镜像里烤进 secret | 镜像泄漏=secret 泄漏 | 永远不在 Dockerfile 里 COPY .env 或 secret;CI 检查(grep -r 'TUSHARE_TOKEN' Dockerfile → fail) |
| 容器逃逸读宿主文件 | 整个 home 暴露 | drop ALL caps + no-new-privileges + read_only: true + 不挂 ~ 整体,只挂 ~/twilight/data |
3. Docker 容器命名冲突
| 风险 | 缓解 |
|---|---|
| 容器名与 PRD 冲突 | 强制前缀:twilight-*(PRD 用 poly-trade-*);每用户容器名 hermes-user-<id> |
| Docker network 重叠 | 所有 twilight 容器在同一个自定义 bridge network twilight-net 中,与 PRD network 隔离 |
4. cloudflared tunnel 耦合
| 风险 | 缓解 |
|---|---|
| 复用 PRD tunnel 加 ingress rule | 不复用:独立 tunnel twilight-backend 独立 UUID 独立 creds 文件,独立 systemd service。一个 tunnel 挂掉不影响另一个 |
| 同一 IP 两个 tunnel 流量混杂 | cloudflared 协议层路由按 hostname;流量上 CF 边缘已分开 |
| API token scope 过宽(PRD 的 token 能动 twilight 的 tunnel 配置) | 给 twilight 单独申一个 IP-locked CF API token (scope: Tunnel:Edit + Access:Apps + Zone:DNS:Edit + Zone:Read);不复用,单独存 ~/.cloudflare-token-twilight |
5. 资源争用
| 风险 | 缓解 |
|---|---|
| Twilight 把 Vultr CPU/RAM 吃满 → PRD 交易暂停 | Docker --cpus 1.5 --memory 1500m(VPS 规格 4GB → twilight 上限 ~37%;PRD 平时 ~1GB) |
| DuckDB 写并发把磁盘 IO 压满 | DuckDB cache 是单写者(FastAPI 进程);P2-A 仓库写在 Mac Mini,不在 Vultr |
| 日志膨胀填满磁盘 | systemd journald rate-limit 默认;docker 用 --log-opt max-size=10m --log-opt max-file=3 |
6. 镜像供应链
| 风险 | 缓解 |
|---|---|
| 第三方 base image 投毒 | base 镜像 pin SHA:FROM python:3.11-slim@sha256:<digest>;用 GitHub Dependabot 跟进新 SHA |
| pip install 拉到中毒包 | pip-audit 进 CI;requirements.txt 用 hash pinning (pip install --require-hashes) |
| 镜像 push 到 ghcr.io 时 token 泄漏 | GitHub Actions OIDC,不存长期 PAT;workflow 只在 v* tag push 时跑 |
| 攻击者篡改 ghcr.io 镜像 | 部署时 pull by SHA digest(不是 tag),systemd unit 里写死 image: ghcr.io/lacatfly/twilight-drive@sha256:<digest> |
7. Docker daemon 共享
| 风险 | 缓解 |
|---|---|
| PRD Fordefi signer + twilight backend 共用同一个 docker daemon | 容器之间默认 bridge 网络隔离;不用 --network=host;provisioner 例外:挂载 docker.sock 仅用于创建 Hermes 容器,其他容器一律不挂 |
| 一个容器漏洞 → docker daemon → 整机 root | 这是 docker 的固有风险;rootless docker 是更彻底方案,但 PRD 现在不是 rootless,先不动;中期 (P2) 评估迁移 |
8. 网络出口
| 风险 | 缓解 |
|---|---|
| 容器被攻击后向外探测/扫 | Docker 默认无出口限制;v1 接受。P2 评估 --network 自定义 + iptables egress allowlist (api.tushare.pro, *.eastmoney.com, openrouter, siliconflow) |
| Tushare 配额被异常请求耗尽 | backend 进程内 token-bucket rate-limiter(属 P1.0 实现)+ DuckDB cache 摊销重复请求 |
9. 故障恢复
| 风险 | 缓解 |
|---|---|
| docker daemon 重启把所有容器搞掉 | systemd unit Restart=always;docker --restart=unless-stopped |
| 进程 OOM kill | systemd Restart=on-failure;docker --memory 设了 cgroup 限制;超出会被 kernel OOM killer 干掉而不是拖累整机 |
| cloudflared tunnel 边缘连接断 | cloudflared 自带自动重连;systemd Restart=always;CF Access 仍然挡未授权流量,所以"暂时无服务"远好过"裸暴露" |
| DuckDB cache 损坏 | cache 是可重建的 (24h TTL);rm 文件 + 重启即可 |
10. 监控盲区
| 风险 | 缓解 |
|---|---|
| 容器 silently 卡在某个 import 上 | healthcheck: GET /healthz + Docker unhealthy 后 systemd Restart=always |
| Tushare 配额异常增长 | backend 实现日 quota 计数(P1.0 任务) |
| 用户量爬升后忘记升 Vultr 套餐 | docker stats + docker ps --format 监控总内存;超过 3.2 GB 时告警;Mac Mini overflow 预案 |
11. 升级 / 回滚
| 风险 | 缓解 |
|---|---|
| 新版本 break prod | 部署都走 local edit → GitHub PR → merge → CI 推 ghcr.io → VPS docker pull —— 跟 PRD 同样纪律 |
| 回滚困难 | docker compose 写明 image: 含 SHA digest;docker compose pull 拉新版前先记录旧版 SHA;回滚 = 改 SHA + docker compose up -d |
| 数据库迁移半成 | P1.0 cache 无 schema 迁移;P1.1 Postgres 用 alembic(migrations 与 backend image 一起发) |
部署流程(要落到 deploy/ 目录)
One-shot bootstrap (人工,VPS 上跑一次)
bash
# On Vultr (ssh tvps)
mkdir -p ~/twilight/{data,logs,secrets}
cd ~/twilight
git clone https://github.com/LaCatFly/twilight-drive.git source
# Apply env (manual — copy .env.example, fill in values)
cp source/profile/template-stock-research-pro/.env.example .env
chmod 600 .env
$EDITOR .env
# Tunnel: create + DNS + creds
cloudflared tunnel create twilight-backend
cloudflared tunnel route dns twilight-backend api.fsagent.cc
mv ~/.cloudflared/<UUID>.json ~/.cloudflared/twilight-backend-<UUID>.json
# Stash UUID for systemd unit
echo "<UUID>" > ~/.cloudflared/TWILIGHT_TUNNEL_UUID
# Cloudflare API token (separate from PRD's, IP-locked, scoped down)
echo "<NEW_TOKEN>" > ~/.cloudflare-token-twilight
chmod 600 ~/.cloudflare-token-twilight
# Compose config (substitute UUID into the template)
cd source && bash deploy/install.sh
# CF Access policy
# Manual via Zero Trust dashboard:
# Access > Applications > Add an application > Self-hosted
# Domain: api.fsagent.cc
# Policy: default-email-otp (allowlist team emails)Recurring deploy (CI-triggered on v* tag)
GitHub Actions release.yml (extended):
├── Build wheel (existing)
├── Pack skill tarball (existing)
└── Build + push Docker image
tag: v0.2.0 → ghcr.io/lacatfly/twilight-drive:0.2.0
also: ghcr.io/lacatfly/twilight-drive:latest
digest pinned in compose.yml on next git pull on VPS
VPS:
cd ~/twilight/source && git pull
cd ~/twilight && docker compose pull && docker compose up -d
systemctl --user restart twilight-backend與其他 spec 的关系
- 00-multi-tenancy:profile 隔离的另一半;本文是 backend 与 PRD 之间的隔离
- 08-hermes-aligned-structure:profile 模板的
config.yaml.template把TWILIGHT_SERVICE_URL指向https://api.fsagent.cc - superpowers/specs/phase1:P1.0 spec 之前写"Vultr + Caddy + systemd"——本文取代 Caddy 部分(cloudflared 同时给我们 TLS + 公网入口 + Access auth,Caddy 是冗余的);其余 systemd + DuckDB cache + bearer auth 不变
- superpowers/specs/phase2 Initiative C:未来 New-API gateway 跑在另一个 Docker 容器,复用本文的隔离/cloudflared 模式
關鍵決策點 (Open Questions)
Q1. 数据库选型 — DuckDB on host volume vs Postgres in second container?
- A) DuckDB 文件 + host bind volume(P1.0 cache 简单,规划里就是这个)
- B) Postgres 容器(P1.1 用户/计费表的位置)
- C) 两者并存:DuckDB 给 cache(regenerable,无需备份),Postgres 给 user state(要备份)
→ 建議 C:DuckDB cache 容器内 + bind mount,丢了重建;Postgres 起独立容器(twilight-postgres),独立 volume + cron 备份。两者职责不一样不能混。
Q2. base 镜像选择?
- A)
python:3.11-slim(官方,~120MB) - B)
python:3.11-alpine(~50MB 但 musl libc 跟一些包不兼容) - C)
gcr.io/distroless/python3(Google 维护,~50MB,无 shell 增加防御)
→ 建議 A:slim 是最稳的选择;alpine musl 跟 pandas/akshare 经常出 wheel 兼容问题;distroless 调试不方便(没 shell)等成熟后再换。
Q3. cloudflared tunnel — 复用 PRD tunnel 还是独立?
- A) 复用:在 PRD
~/.cloudflared/config.yml加 ingress rule - B) 独立:新 tunnel UUID + 新 systemd unit
→ 已决 B(独立):见 §"高风险" §4,独立 tunnel 给独立故障域 + 独立 API token scope。
Q4. CF Access — 谁能访问 backend?
- A) Email OTP(团队 allowlist)
- B) Service token(每 profile 一个,机器对机器)
- C) 阶段切换:v1.x Email OTP(人手测);P1.1 起 service token(profile 自动调用)
→ 已决 C:service token 是正确的机器鉴权方式,但 v1 阶段还没 provisioner,先用 OTP 给团队人手验证;P1.1 起切。
Q5. 镜像 SHA pinning 在哪做?
- A)
compose.yml写image: ghcr.io/lacatfly/twilight-drive:0.2.0(tag) - B)
compose.yml写image: ghcr.io/lacatfly/twilight-drive@sha256:<digest>(digest pin) - C) 两层:CI 用 tag 推,部署用 digest pin
→ 建議 C:tag 易读但可被推覆盖;digest pin 不可篡改。CI workflow 在 release 后把 digest 写回 compose.yml 并自动开 PR(Dependabot-style)。
下一步 (Next Steps)
详见 plans/2026-05-07-deploy-pattern.md。Highlights:
- 落
deploy/目录到仓库(Dockerfile, compose.yml, systemd units, install.sh, cloudflared config template, env.example) — 这一 PR - CI release.yml 扩展支持 push Docker image(同一 PR 内最好;或分一 PR 跟 image 实测一起)
- 实施 P1.0 backend FastAPI(Week 2)—— Dockerfile 等到这一步才能真 build
- 在 PRD VPS 上手动跑一次 install.sh(你来)—— 一次性建 tunnel + DNS + Access 应用
- CF Pages 给 docs site 加自定义域
dev.fsagent.cc(你在 CF 控制台点一下)
进一步阅读
plans/2026-05-07-deploy-pattern.md:本 plan 的实施视角(dated)- [PRD
.claude/rules/deployment.md](file:///Users/syone/PRD/.claude/rules/deployment.md):本文的来源样板 - 00 — Multi-tenancy
- Phase 1 spec:P1.0 backend 设计(cloudflared 部分需要回填本文决策)