Skip to content

部署

本文面向把系统跑到真实环境的操作员。生产控制面是 Cloudflare Workers 上的 TS 面(2026-07 切流);Python 面以 docker 全栈形态保留为温备与自托管参考;节点 agent 一律宿主 systemd。接入节点见 node-onboarding.md,配置项见 ../reference/configuration.md,TS 面内部结构见 ../internals/control-server-ts.md

本文所有 /api/v1/admin/* 调用都需携带 -H "Authorization: Bearer <admin token>",示例中为简洁省略。

部署形态总览

形态适用产物
Cloudflare Workers 控制面(TS 面)现役生产(入口 api.natlan.ioapps/control-server-ts + wrangler
docker 全栈控制面(Python 面)温备 / 自托管参考(control-server + PostgreSQL + Redis)docker/
systemd 节点 agent真实 DN42 节点deploy/systemd/
本地源码运行开发调试uvicorn + python -m agent.main(见 ../tutorials/01-quickstart.md

去 compose 澄清:节点 runtime 的路由容器用 docker-compose(Agent 直连 Docker Engine API 创建)。docker/ 的 compose 只编排 Python 控制面后端不含 node-agent——agent 在真实节点上跑宿主原生 venv + systemd。

生产控制面:Cloudflare Workers(TS 面)

部署单元是单个 Worker dn42-control-server-tsapps/control-server-ts)。非 Git 关联——从工作机用 wrangler 发布;wrangler.jsonc 是绑定与非机密配置的唯一事实源。

前置资源(一次性)

资源说明
PostgreSQL 源库两面共享的单一后端库(现役:GCP 主机上的 dn42-ts-pg 容器)。DB 入 Cloudflare 的计划见 ../ROADMAP.md
Hyperdrive 配置边缘连接池指向源库,id 写进 wrangler.jsonchyperdrive 绑定
KV namespaceCACHE_KV(desired-state / registry 名称 / liveness / pflap 热态 / routing 预计算)
R2 桶wrangler r2 bucket create dn42-agent-releases(agent wheel 分发源)
Durable ObjectsDoorbell / ProbeHubwrangler.jsoncmigrations 自动创建
Custom Domainapi.natlan.io 经 Workers → 域名与路由挂载(workers.dev 入口已关闭,唯一 HTTP 入口是自定义域名)

机密与配置

机密用 wrangler secret put 注入(逐项含义见 ../reference/configuration.md):

bash
cd apps/control-server-ts
npx wrangler secret put ADMIN_TOKEN            # admin API 静态凭据(fail-closed:不设整面 403)
npx wrangler secret put ENROLLMENT_TOKEN       # 全局 enrollment token(可不设,仅用单节点 token)
npx wrangler secret put RECOVERY_PUBLIC_KEY    # WG 私钥 escrow 恢复公钥(PEM)
npx wrangler secret put REGISTRY_TOKEN         # DN42 registry(Forgejo)同步凭据
npx wrangler secret put TURNSTILE_SECRET_KEY   # 账号登录人机验证(不设则账号登录 400 fail-closed)
npx wrangler secret put BOOTSTRAP_ADMIN        # 首个管理员账号 bootstrap(见 authentication.md)

非机密项(健康阈值、CORS、registry 源、会话 TTL、flap 参数)在 wrangler.jsoncvars,随部署生效。

数据库迁移(drizzle)

TS 面的迁移由 drizzle-kit 在 Worker 外部直连源 PG 应用(不经 Hyperdrive),与 Worker 部署解耦:

bash
cd apps/control-server-ts
npm run db:generate                                # schema.ts → drizzle/*.sql
DATABASE_URL=postgres://user:pass@host/db npm run db:migrate

工作机直连源库不便时(如自签 SSL),可把 SQL 经 ssh 管道灌进库端容器:

bash
cat drizzle/0003_route_prefix_hashes.sql | ssh <pg-host> "docker exec -i <pg-container> psql -U <user> -d <db> -v ON_ERROR_STOP=1"

Python 面的 Alembic 链与 drizzle 链描述同一套 schema,双面迁移的对应关系与例外见 ../reference/database.md

发布与验证

bash
cd apps/control-server-ts
npm install
npm run typecheck        # tsc --noEmit
npm run deploy           # wrangler deploy
curl -s https://api.natlan.io/healthz   # {"status":"ok","runtime":"workers-ts"}

部署后现象:Worker 重部署会让全部 agent 门铃 WebSocket 同时断连重连——管理端短暂看到「全 agent 掉线」是预期,不是事故。

回滚(切回 Python 面)

切流剧本保留了回滚路径:逐节点把 /etc/dn42-control/<node_id>.env 恢复为切流前备份 <node>.env.bak-py-plane(controller URL 指回 Python 面)并重启 agent 单元。Python 温备面持续在跑、读写同一个库,无需数据迁移。

温备 / 自托管参考:docker 全栈控制面(Python 面)

docker/ 提供 Python 控制面标准后端栈,一条 compose 拉起:

服务说明
control-serverFastAPI;连 postgres + redis,默认仅发布到 127.0.0.1:${CONTROL_PORT:-8000}
postgres 16后端库,持久卷 pg-data,仅容器网内可达
redis 7缓存(maxmemory + LRU,不持久化);旁路降级——不可用时 control-server 自动回落 DB
bash
# 仓库根执行;先把样例 env 复制为 .env 填好密码/token
cp docker/.env.example docker/.env       # 至少改 POSTGRES_PASSWORD / DN42_CONTROL_ADMIN_TOKEN
docker compose -f docker/docker-compose.yml --env-file docker/.env up -d --build

启动顺序由 depends_on: service_healthy 保证:postgres + redis 先 healthy,control-server 再起;启动期 create_all 在空库建当前 schema(与 alembic upgrade head 基本等价,例外见 ../reference/database.md)。详见 ../../docker/README.md。

对外暴露:设 CONTROL_BIND=0.0.0.0(或经前置 TLS 反代),并务必用 DN42_CONTROL_ADMIN_TOKEN 保护 + 防火墙收窄入站源。关停:down(留卷保 DB)/ down -v(连数据卷删)。

生产的温备实例即此形态(跑在 GCP 主机,前置 nginx,入口 legacy.api.natlan.io)。两面读写同一个 PostgreSQL,行为变更必须锁步(见 ../ROADMAP.md)。

nginx 前置要点(docker/nginx/conf.d/control-server.conf):Cloudflare Origin CA 证书按 SNI 分流*.ngworks.orgnatlan.io/*.natlan.io 各一套证书),配合 CF SSL 模式 Full (Strict);80 与 443 都回源、不强跳 HTTPS(CDN 无论 Full 还是 Flexible 都不会 301 死循环);agent WebSocket(/api/v1/agent/ws/…)透传。

systemd 节点 agent

node-agent 在真实节点上跑宿主原生 venv + systemd(不走容器)——以便 apply 模式下渲染产物直接落宿主、宿主 dockerd 给路由容器做 bind 时路径一致,且 agent 直接访问宿主 docker.sockdeploy/systemd/ 提供模板单元(注释内含完整安装步骤):

单元说明
dn42-node-agent@.serviceagent 模板单元:%inode_id,同机可跑多实例;Restart=always 常驻
dn42-control-server.servicePython 控制面也可走 systemd(uvicorn + 外部 postgres/redis),但推荐用上面的 docker 全栈
deploy/agent-self-update/dn42-node-agent-update.serviceagent 自更新器单元(install-updater.sh 一次性安装,见下)

生产 fleet 的实际布局与模板单元略有差异:每节点单实例、非模板单元 dn42-node-agent.service,venv 在 /opt/dn42-agent(与 deploy/agent_pip_rollout.shdeploy/agent-self-update/ 一致)。模板单元适合实验环境同机多实例。

安装骨架(agent):

bash
sudo cp deploy/systemd/*.service /etc/systemd/system/
sudo mkdir -p /opt/dn42-control && sudo rsync -a ./ /opt/dn42-control/   # 或按单元注释路径
sudo python3 -m venv /opt/dn42-control/.venv
# 安装依赖 + 一方包(见单元文件 PYTHONPATH / ExecStart)
sudo systemctl daemon-reload
sudo systemctl enable --now dn42-node-agent@edge1
journalctl -u dn42-node-agent@edge1.service -f

agent 实例的节点专属变量放 /etc/dn42-control/<node_id>.env

text
DN42_AGENT_CONTROLLER_URL=https://api.natlan.io
DN42_AGENT_ENROLLMENT_TOKEN=change-me

首次装机后再装自更新更新器(每节点一次):

bash
sudo bash deploy/agent-self-update/install-updater.sh

此后 agent 升级由控制面驱动(上传 wheel → 设目标版本 → 心跳推送 → 更新器安装 + 健康门回滚),不再逐机 pip,见 upgrades-and-migrations.md。完整接入流程见 node-onboarding.md

数据库(PostgreSQL)+ 缓存

生产数据层是单一 PostgreSQL,两面共享:

  • TS 面(生产):经 Hyperdrive 连接池访问;缓存用 KVCACHE_KV,最小 TTL 60s)+ Cache API(秒级响应缓存),无 Redis。
  • Python 面(温备)DN42_CONTROL_DATABASE_URL=postgresql+asyncpg://... 直连;缓存经 DN42_CONTROL_REDIS_URL(未配置或不可用即全程 no-op 回落 DB)。
  • ⚠️ ASN 等列用 BigInteger(DN42 ASN 4242420000+ 超 int32 上限)。
  • 建表:Python 启动期 Base.metadata.create_all 建缺失表;规范化迁移双轨——Python 用 Alembic(alembic upgrade head),TS 用 drizzle(见上)。等价性与例外见 ../reference/database.md
  • SQLite → PostgreSQL 迁移(旧库升级):docker/migrate_sqlite_to_postgres.py——按 FK 拓扑序整库拷贝(类型安全)+ 重置自增序列,源库只读。

迁移操作见 upgrades-and-migrations.md

生产必做项

  • 必设 admin 凭据:TS 面 ADMIN_TOKEN secret / Python 面 DN42_CONTROL_ADMIN_TOKEN——不设则 admin API 整体 403(fail-closed)。
  • 配置管理员账号体系(浏览器登录用):BOOTSTRAP_ADMIN + TURNSTILE_SECRET_KEY,见 authentication.md
  • 改掉 enrollment token / POSTGRES_PASSWORD 默认值
  • 配 recovery 公钥RECOVERY_PUBLIC_KEY(TS)/ DN42_CONTROL_RECOVERY_PUBLIC_KEY(Python),用于 WG 私钥 escrow,见 secret-recovery.md
  • 收紧 CORSCORS_ORIGINS(TS vars)/ DN42_CONTROL_CORS_ORIGINS(Python)只列 Web UI 的来源。
  • 网络层保护(Python 温备面):admin API 前置 TLS 反代 + 防火墙收窄入站源,见 ../internals/security.md。TS 面的入口即 Cloudflare 边缘,无需自管 TLS。

全部配置项见 ../reference/configuration.md

Web UI 托管

Web UI 是 SvelteKit + adapter-static纯静态 SPA,源码在独立仓库 dn42-control-webui,与控制面分开托管。生产部署是 Cloudflare Git 关联 Worker(静态资产,console.natlan.io):push 到仓库即自动构建部署;仓库内的 wrangler.jsonc 声明 assets.not_found_handling: "single-page-application" 做 SPA fallback,必须保留(否则构建向导会改写构建命令)。

也可自行构建、用任意静态服务器伺服:

bash
git clone https://github.com/chidakiko/dn42-control-webui
cd dn42-control-webui
npm ci && npm run build      # 产物在 build/(adapter-static)

前端经 CORS + Bearer(凭据存 localStorage)直连控制面 /api/v1/*,因此控制面 CORS 必须包含 Web UI 的来源,且 SPA 路由需服务器 fallback 到 index.html。操作指南见 web-ui.md

同一生态还有两个静态站点,均不在本仓库:natlan.io 裸域首页(webui 仓库 workers/natlan-io-home/,非 Git 关联 Worker,消费 公开地图接口)与 docs.natlan.io 文档中心(natlan-docs 仓库,把本 docs/ 同步为 VitePress 站点)。入口总览见 ../overview.md