外观
控制面整栈部署
控制面是一个 docker compose 整栈,编排文件是 deploy/docker/docker-compose.yml单个文件。生产即此形态。
本文所有
/control/v1/admin/*调用都需携带-H "Authorization: Bearer <admin token>",示例中为简洁省略。
栈内服务
| 服务 | 说明 | 发布 |
|---|---|---|
nginx | 收口 80/443,TLS 终止(Cloudflare Origin CA),回源内网各服务 | 0.0.0.0 与 [::] 的 80/443 |
control-server | FastAPI,/control/v1/* | 默认仅 127.0.0.1:8000 |
auth-server | ASN 归属验证与 OAuth2/OIDC Provider,/api/v1/* 与根上的协议端点 | 默认仅 127.0.0.1:8100 |
registry-server | 官方 registry 副本,/registry/v1/* | 不发布,仅容器网内 |
postgres 16 | 三服务各自独立 database(同实例),持久卷 pg-data | 见下 |
redis 7 | 控制面缓存(maxmemory + LRU,不持久化)。旁路降级——不可用时自动回落 DB | 不发布 |
cap | 自托管人机验证服务 | 默认仅 127.0.0.1:3000 |
valkey | Cap 的 KV 后端,兼作本栈通用 KV,持久卷 valkey-data | 不发布 |
全部容器同处一个显式命名的网络,彼此用服务名直连。
PostgreSQL 在生产上对公网发布了 51888 端口供远程管理:pg_hba 已收紧到公网仅管理账号可认证(scram),超级用户只在容器网与回环可达,云侧防火墙需另行放行。不需要远程管理的部署应删掉这段 ports。
起栈
bash
cd deploy/docker
docker compose -p docker-compose up -d-p docker-compose 必须显式给。 项目名决定容器名(docker-compose-<service>-1),不给会按目录名派生成 docker-*,把既有容器名全改一遍。
build context 是仓库根,.env 自动从 deploy/docker/ 读取。
启动顺序由 depends_on: service_healthy 保证:postgres 与 redis 先 healthy,应用容器再起。registry-server 是软依赖(service_started)——名称富化会回落占位符,不能让它的故障连坐拖住控制面重启。
关停:down 保留数据卷,down -v 连数据卷一起删。
配置载体
生产以 TOML 为主载体,compose 的 environment 里只留容器网内的连接串。
| 文件 | 挂载到 | 内容 |
|---|---|---|
deploy/docker/control-server.toml | /etc/dn42-control/ | 控制面全部配置 |
deploy/docker/auth-server.toml | /etc/dn42-auth/ | 认证服务全部配置 |
deploy/docker/registry-server.toml | /etc/dn42-registry/ | 副本服务全部配置 |
deploy/docker/.env | — | compose 变量(数据库口令、绑定地址、Cap 管理密钥) |
⚠️ 往
environment里加一项,就等于把 TOML 同名项永久压掉(env > TOML)。这是「改了配置文件没生效」的第一嫌疑。
TOML 文件含 token 与口令,宿主侧务必 chmod 600。全部键位见 配置参考。
首次初始化(空卷)时 pg-init/10-create-auth-db.sh 会顺带建出 auth-server 与 registry-server 的独立 database。既有卷不会重跑,手动补:
bash
docker exec <pg> psql -U dn42 -d dn42_control -c "CREATE DATABASE dn42_auth"
docker exec <pg> psql -U dn42 -d dn42_control -c "CREATE DATABASE dn42_registry"nginx 前置
配置在 deploy/docker/nginx/conf.d/,三份 conf 加两份 include 片段跟踪在仓库里——这是生产 nginx 配置的单一真相源。
| 文件 | 承载 |
|---|---|
control-server.conf | 控制面(含 api.natlan.io)与 SNI 分流 |
auth-natlan.conf | auth.natlan.io:授权页静态资产加 auth-server 反代 |
challenges-natlan.conf | Cap 实例,widget 端点公开、管理面 IP 收窄 |
00-admin-allowlist.conf | 全站唯一的运维 IP 白名单 |
cloudflare-realip.inc / security-headers.inc | 共用片段 |
要点:
- 证书按 SNI 分流:
*.ngworks.org与natlan.io/*.natlan.io各一套 Cloudflare Origin CA 证书。 - 80 与 443 都回源,不强跳 HTTPS:CDN 无论 Full 还是 Flexible 模式都不会 301 死循环。
- agent WebSocket 透传(
/control/v1/agent/ws/…),proxy_read_timeout放到 3600s。 CF-Connecting-IP与X-Forwarded-For是覆写而非透传。realip 只修正 nginx 自己的$remote_addr,客户端自带的这两个头仍会原样转给上游,而应用侧的client_ip()读的就是它们。不覆写的话,直连源站的请求可以任意伪造来源 IP,把按 IP 的限速与审计一并架空。因此这里不能用$proxy_add_x_forwarded_for——那是追加语义,会把客户端伪造的那一跳留在首项。
⚠️
upstream control_backend是静态解析:容器名在 nginx 配置加载时解析一次并缓存,之后不再重解析。单独重建 control-server 会让它指着已失效的旧 IP,对外一律 502。 重建后必须nginx -s reload;整栈up -d会连 nginx 一起重建,故不受影响。
auth-natlan.conf里的块用resolver加变量形式,属于每请求重解析,不吃这个亏。
授权页静态资产
前端站点都在独立的前端仓库里,唯独 OAuth 授权页与 auth-server 同源挂在 auth.natlan.io 下——免 CORS、免第三方 Cookie、地址栏始终显示用户正在授权的那个域名。它的构建产物因此手工投放并跟踪在本仓库:deploy/docker/nginx/www/auth/,由本栈的 nginx 直接发。
静态白名单是显式列举的:/authorize、/assets/、/favicon.svg 走静态,其余一律回后端。反过来做(默认静态、列举后端路由)会在后端新增端点时被 404 悄悄遮住。
换产物前必须验两件事:
- 产物风味。授权页依赖
index.html里的__CSP_NONCE__占位符加 nginx 的sub_filter注入。若拿到的是为其它托管形态构建的产物(删了占位符、改由别处注入),内联脚本会拿不到 nonce 被 CSP 拦掉——表现为验证码不出现而页面无任何报错。 - CSP 与人机验证的兼容。
/authorize块的 CSP 里'unsafe-eval'不是可选项:Cap 的 instrumentation 挑战脚本(服务端逐次下发的反自动化代码)用eval与Function。只给'wasm-unsafe-eval'的话它会静默死在沙箱 iframe 里、二十秒后超时——违规事件发生在那个 opaque origin 的子文档里,主文档控制台一条都看不到。
改 CSP 前先读 deploy/docker/nginx/www/README.md。
数据库与缓存
DN42_CONTROL_DATABASE_URL=postgresql+asyncpg://…直连;缓存经DN42_CONTROL_REDIS_URL(未配置或不可用即全程 no-op 回落 DB)。- ASN 等列用
BigInteger——DN42 的 4242420000+ 超 int32 上限。 - 建表:由
[database] auto_migrate分流——true走alembic upgrade head(fail-fast),false走create_all(只建缺失的表)。⚠️ 现役生产置false(compose 的DN42_CONTROL_DB_AUTO_MIGRATE=false):库由create_all建起、未被 alembic 纳管,直接upgrade会从 base 重跑撞已存在的表。接管步骤与已知的 ORM 偏差见 数据库迁移。 - SQLite 升级到 PostgreSQL:
deploy/docker/migrate_sqlite_to_postgres.py按外键拓扑序整库拷贝(类型安全)并重置自增序列,源库只读。
迁移操作见 数据库迁移。
生产必做项
| 项 | 说明 |
|---|---|
| 设 admin 凭据 | [server] admin_token。不设则管理面整体 403(fail-closed) |
| 配管理员账号 | [auth] bootstrap_admin 加三项 cap_*,浏览器登录用。见 认证与账号 |
| 改掉默认口令 | POSTGRES_PASSWORD 与 enrollment token |
| 保护密钥表 | node_wireguard_keys.private_key 是明文的全 fleet 隧道私钥。收紧库的访问与备份;可选配 [server] recovery_public_key 启用离线托管(现役生产未配)。见 密钥托管与恢复 |
| 收紧 CORS | [server] cors_origins 只列前端站点的真实来源 |
| 网络层保护 | 管理面前置 TLS 反代加防火墙收窄入站源。生产由 Cloudflare 代理到源站 nginx,TLS 两侧各终止一次(回源用 Origin CA,CDN 侧 SSL 模式取 Full (Strict)) |
全部配置项见 Control Server 配置。
升级重建
控制面走整栈重建。步骤与验收见 控制面升级。