外观
部署
本文面向把系统跑到真实环境的操作员。生产控制面是 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.io) | apps/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-ts(apps/control-server-ts)。非 Git 关联——从工作机用 wrangler 发布;wrangler.jsonc 是绑定与非机密配置的唯一事实源。
前置资源(一次性)
| 资源 | 说明 |
|---|---|
| PostgreSQL 源库 | 两面共享的单一后端库(现役:GCP 主机上的 dn42-ts-pg 容器)。DB 入 Cloudflare 的计划见 ../ROADMAP.md |
| Hyperdrive 配置 | 边缘连接池指向源库,id 写进 wrangler.jsonc 的 hyperdrive 绑定 |
| KV namespace | CACHE_KV(desired-state / registry 名称 / liveness / pflap 热态 / routing 预计算) |
| R2 桶 | wrangler r2 bucket create dn42-agent-releases(agent wheel 分发源) |
| Durable Objects | Doorbell / ProbeHub 随 wrangler.jsonc 的 migrations 自动创建 |
| Custom Domain | api.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.jsonc 的 vars,随部署生效。
数据库迁移(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-server | FastAPI;连 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.org与natlan.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.sock。deploy/systemd/ 提供模板单元(注释内含完整安装步骤):
| 单元 | 说明 |
|---|---|
dn42-node-agent@.service | agent 模板单元:%i 即 node_id,同机可跑多实例;Restart=always 常驻 |
dn42-control-server.service | Python 控制面也可走 systemd(uvicorn + 外部 postgres/redis),但推荐用上面的 docker 全栈 |
deploy/agent-self-update/dn42-node-agent-update.service | agent 自更新器单元(install-updater.sh 一次性安装,见下) |
生产 fleet 的实际布局与模板单元略有差异:每节点单实例、非模板单元
dn42-node-agent.service,venv 在/opt/dn42-agent(与deploy/agent_pip_rollout.sh、deploy/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 -fagent 实例的节点专属变量放 /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 连接池访问;缓存用 KV(
CACHE_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_TOKENsecret / 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。 - 收紧 CORS:
CORS_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。