https://d3qpg7p2n3hazf.cloudfront.net/api/v1/client/subscribe?token=2185761c5925a800c2d2c1ec44449b65 # nano_project 单机部署版运行结构: - `auth-portal` - 用户入口页,提供登录、注册、跳转。 - `authz-service` - AuthZ 服务。 - 现在负责注册编排:`auth-portal -> authz-service -> deploy-control -> app-instance -> authz-service` - `deploy-control` - 部署机控制面。 - 负责创建实例、解析实例、刷新反向代理。 - `router-proxy` - 统一入口代理。 - 按 Host 把 `.` 转发到对应实例容器。 - `app-instance` - 真正的单用户实例。 - 一个容器里同时包含前端、后端和 Nginx。 ## 请求链路 注册: ```text Browser -> auth-portal -> authz-service POST /portal/register -> deploy-control POST /api/instances/register -> create-instance.sh -> app-instance POST /api/auth/register -> authz-service /oauth/register or /backends/register ``` 登录: ```text Browser -> auth-portal -> deploy-control POST /api/instances/resolve -> app-instance POST /api/auth/login ``` ## 这份部署指南的前提 这份 README 只覆盖一套基准方案: - 一台 Linux 服务器 - 用 Docker 运行 `auth-portal`、`authz-service`、`deploy-control`、`router-proxy` - `deploy-control` 通过 Docker socket 在同一台机器上创建 `app-instance` - 所有容器共用一个 Docker network:`nano-instance-edge` - 测试域名先用 `nip.io` 如果你后面要接 HTTPS、外部 LB、Kubernetes、真实 DNS,这份流程仍然适合作为最小可运行基线。 可直接参考这些模板文件: - [`.env.example`](/home/ivan/xuan/nano_project/.env.example) - [`auth-portal/src/.env.example`](/home/ivan/xuan/nano_project/auth-portal/src/.env.example) - [`authz-service/.env.example`](/home/ivan/xuan/nano_project/authz-service/.env.example) - [`deploy-control/.env.example`](/home/ivan/xuan/nano_project/deploy-control/.env.example) - [`router-proxy/.env.example`](/home/ivan/xuan/nano_project/router-proxy/.env.example) 注意: - 这些文件是模板,不会被现有脚本自动加载 - 你可以手动 `export`,或者在 `docker run` 时使用 `--env-file` ## 部署前必须先定好的值 先准备这些值: - `PROJECT_ROOT` - 仓库根目录。 - `NANO_NET` - Docker network 名。 - 推荐固定成 `nano-instance-edge`。 - `NANO_DEPLOY_TOKEN` - `auth-portal` / `authz-service` 调 `deploy-control` 时用的 Bearer token。 - `NANO_AUTHZ_INTERNAL_TOKEN` - AuthZ 内部接口 token。 - `NANO_SERVER_IP` - 服务器公网 IP,供 `nip.io` 测试使用。 - `NANO_BASE_DOMAIN` - 实例基域名。 - 测试环境推荐 `.nip.io` - `NANO_PROVIDER` - 默认 provider,例如 `openai` - `NANO_MODEL` - 默认模型,例如 `openai/gpt-5` - `NANO_API_KEY` - 默认分发给新实例的 provider API key - `NANO_API_BASE` - 可空,自定义 provider base URL - `NANO_AUTHZ_URL` - 这个值必须是 `app-instance` 容器能访问到的 AuthZ 地址 - `NANO_OUTLOOK_MCP_URL` - 可空。 - 如果配置了,所有新创建的 `app-instance` 都会默认带一个 Outlook MCP HTTP 工具配置。 - `NANO_OUTLOOK_MCP_SERVER_ID` - Outlook MCP 默认 server id。 - 推荐固定成 `outlook_mcp`。 - `NANO_DEPLOY_URL` - `auth-portal` 和 `authz-service` 在容器网络里访问 deploy-control 的地址 直接导出一套最小配置: ```bash export PROJECT_ROOT=/home/ivan/xuan/nano_project export NANO_NET=nano-instance-edge export NANO_DEPLOY_TOKEN="$(openssl rand -hex 32)" export NANO_AUTHZ_INTERNAL_TOKEN="$(openssl rand -hex 32)" export NANO_SERVER_IP=203.0.113.10 export NANO_BASE_DOMAIN="${NANO_SERVER_IP}.nip.io" export NANO_PROVIDER=openai export NANO_MODEL=openai/gpt-5 export NANO_API_KEY='sk-xxxxxxxx' export NANO_API_BASE='' export NANO_AUTHZ_URL='http://nano-authz-service:19090' export NANO_OUTLOOK_MCP_URL='' export NANO_OUTLOOK_MCP_SERVER_ID='outlook_mcp' export NANO_DEPLOY_URL='http://nano-deploy-control:8090' ``` ## 变量到底是什么 最容易混淆的是下面这几组: - `DEPLOY_API_TOKEN` - 调用方带出去的 token。 - `auth-portal` 和 `authz-service` 会用它请求 `deploy-control`。 - `DEPLOY_CONTROL_API_TOKEN` - `deploy-control` 服务端校验的 token。 - 它必须和 `DEPLOY_API_TOKEN` 相等。 - `AUTHZ_ISSUER` - 当前实现里它既是 AuthZ 的 issuer,也是新实例要访问的 AuthZ base URL。 - 所以不要乱写 `127.0.0.1`,要写成实例容器能访问到的地址。 - `APP_INSTANCE_PROVIDER` - 新实例默认 provider。 - `APP_INSTANCE_MODEL` - 新实例默认模型。 - `APP_INSTANCE_API_KEY` - 新实例默认 API key。 - `APP_INSTANCE_API_BASE` - 新实例默认 API base。 - `DEFAULT_AUTHZ_BASE_URL` - `deploy-control` 在没收到显式 `authz_base_url` 时,给新实例写入的兜底 AuthZ 地址。 当前版本里,provider 配置不是从 AuthZ setting 下发的。 它是在创建实例时由 `deploy-control` 写入 `app-instance` 的 `config.json`。 ## 目录持久化 至少保留这几个目录: - `authz-service/runtime/data` - `app-instance/runtime` - `router-proxy/runtime` 建议先创建: ```bash mkdir -p \ "$PROJECT_ROOT/authz-service/runtime/data" \ "$PROJECT_ROOT/app-instance/runtime/instances" \ "$PROJECT_ROOT/app-instance/runtime/registry" \ "$PROJECT_ROOT/router-proxy/runtime/conf.d" ``` ## 1. 构建镜像 先把四个镜像都构建好。虽然 `deploy-control` 在镜像缺失时也能触发构建,但上线前先显式构建更容易排错。 ```bash cd "$PROJECT_ROOT" docker build -t nano/app-instance:latest app-instance docker build -t nano/authz-service:latest authz-service docker build -t nano/deploy-control:latest deploy-control docker build -t nano/auth-portal:latest auth-portal/src ``` ## 2. 创建共享网络 ```bash docker network inspect "$NANO_NET" >/dev/null 2>&1 || docker network create "$NANO_NET" ``` ## 3. 启动 router-proxy ```bash cd "$PROJECT_ROOT" PROXY_NETWORK_NAME="$NANO_NET" \ PROXY_HTTP_PORT=8088 \ ./router-proxy/start-proxy.sh --replace ``` 默认对外入口: - `router-proxy`: `http://.:8088` ## 4. 启动 authz-service ```bash docker rm -f nano-authz-service >/dev/null 2>&1 || true docker run -d \ --name nano-authz-service \ --restart unless-stopped \ --network "$NANO_NET" \ -p 19090:19090 \ -v "$PROJECT_ROOT/authz-service/runtime/data:/var/lib/authz-service/data" \ -e AUTHZ_ISSUER="$NANO_AUTHZ_URL" \ -e AUTHZ_INTERNAL_TOKEN="$NANO_AUTHZ_INTERNAL_TOKEN" \ -e DEPLOY_API_BASE_URL="$NANO_DEPLOY_URL" \ -e DEPLOY_API_TOKEN="$NANO_DEPLOY_TOKEN" \ nano/authz-service:latest ``` 这里最重要的是: - `AUTHZ_ISSUER` 现在不能只按“外部访问地址”理解 - 它必须是后续 `app-instance` 容器也能访问到的地址 - 这套单机 Docker 方案里直接用 `http://nano-authz-service:19090` ## 5. 启动 deploy-control `deploy-control` 需要高权限: - 读写 Docker socket - 访问 `app-instance/` - 访问 `router-proxy/` ```bash docker rm -f nano-deploy-control >/dev/null 2>&1 || true docker run -d \ --name nano-deploy-control \ --restart unless-stopped \ --network "$NANO_NET" \ -p 8090:8090 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v "$PROJECT_ROOT/app-instance:$PROJECT_ROOT/app-instance" \ -v "$PROJECT_ROOT/router-proxy:$PROJECT_ROOT/router-proxy" \ -e APP_INSTANCE_DIR="$PROJECT_ROOT/app-instance" \ -e ROUTER_PROXY_DIR="$PROJECT_ROOT/router-proxy" \ -e DEPLOY_CONTROL_API_TOKEN="$NANO_DEPLOY_TOKEN" \ -e APP_INSTANCE_IMAGE="nano/app-instance:latest" \ -e APP_INSTANCE_NETWORK_NAME="$NANO_NET" \ -e APP_INSTANCE_PROVIDER="$NANO_PROVIDER" \ -e APP_INSTANCE_MODEL="$NANO_MODEL" \ -e APP_INSTANCE_API_KEY="$NANO_API_KEY" \ -e APP_INSTANCE_API_BASE="$NANO_API_BASE" \ -e DEFAULT_AUTHZ_BASE_URL="$NANO_AUTHZ_URL" \ -e DEFAULT_AUTHZ_OUTLOOK_MCP_URL="$NANO_OUTLOOK_MCP_URL" \ -e DEFAULT_OUTLOOK_MCP_SERVER_ID="$NANO_OUTLOOK_MCP_SERVER_ID" \ -e DEPLOY_PUBLIC_SCHEME="http" \ -e DEPLOY_PUBLIC_BASE_DOMAIN="$NANO_BASE_DOMAIN" \ -e DEPLOY_PUBLIC_PORT="8088" \ -e DEPLOY_AUTO_START_PROXY="1" \ nano/deploy-control:latest ``` 这里不要把宿主机目录挂到容器内的另一个短路径,比如 `/app-instance`。 原因是 `deploy-control` 会通过挂载进来的 Docker socket 再去创建 `app-instance` 容器;这时传给 Docker 的 bind mount 源路径必须是宿主机真实路径。如果你把宿主机目录映射成容器内短路径,`create-instance.sh` 生成的挂载源就会变成错误路径,最终表现为: - 注册接口超时 - `app-instance` 容器反复重启 - 日志里出现 `Missing Boardware Genius config: /root/.nanobot/config.json` 当前版本里,新实例的默认大模型配置就是从这里分发的: - `APP_INSTANCE_PROVIDER` - `APP_INSTANCE_MODEL` - `APP_INSTANCE_API_KEY` - `APP_INSTANCE_API_BASE` 如果 `APP_INSTANCE_API_KEY` 没配,新用户注册时创建实例会直接失败。 ## 6. 启动 auth-portal ```bash docker rm -f nano-auth-portal >/dev/null 2>&1 || true docker run -d \ --name nano-auth-portal \ --restart unless-stopped \ --network "$NANO_NET" \ -p 3081:3081 \ -e AUTHZ_API_BASE_URL="$NANO_AUTHZ_URL" \ -e DEPLOY_API_BASE_URL="$NANO_DEPLOY_URL" \ -e DEPLOY_API_TOKEN="$NANO_DEPLOY_TOKEN" \ nano/auth-portal:latest ``` 当前页面入口: - `http://:3081` ## 7. 上线前健康检查 先确认四个基础组件都起来了: ```bash curl http://127.0.0.1:19090/healthz curl http://127.0.0.1:8090/healthz curl -I http://127.0.0.1:3081 docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' ``` 再确认 `router-proxy` 在跑: ```bash docker logs --tail=50 nano-router-proxy ``` ## 8. 首次注册验收 打开: ```text http://:3081/register ``` 注册一个新用户后,预期结果是: 1. `auth-portal` 调 `authz-service /portal/register` 2. `authz-service` 调 `deploy-control /api/instances/register` 3. `deploy-control` 创建一个新的 `app-instance` 4. `app-instance` 回调 AuthZ 完成 backend 身份初始化 5. 浏览器被跳转到该实例自己的 URL 同时你应该能看到: ```bash cd "$PROJECT_ROOT" ./app-instance/list-instances.sh --json ``` 新实例 URL 形如: ```text http://.:8088 ``` 如果你前面用的是: ```text NANO_BASE_DOMAIN=.nip.io ``` 那么实例地址会像: ```text http://alice.203.0.113.10.nip.io:8088 ``` ## 9. 常见问题 ### 1. 为什么不要把 `AUTHZ_ISSUER` 写成 `http://127.0.0.1:19090` 因为 `app-instance` 容器里访问 `127.0.0.1` 只会打到它自己,不会打到 AuthZ 容器。 在当前实现里,`AUTHZ_ISSUER` 会被直接传给新实例当作 `authz_base_url`。 ### 2. `DEPLOY_API_TOKEN` 和 `DEPLOY_CONTROL_API_TOKEN` 为什么要一样 因为一个是客户端发出去的 token,一个是服务端拿来校验的 token。 ### 3. 这些 provider 配置是写到哪里 写到每个实例自己的: ```text app-instance/runtime/instances//nanobot-home/config.json ``` 不是写在 AuthZ 的某个 setting 里。 ### 4. 现在支持每个用户注册时填自己的 API key 吗 后端请求模型已经支持 `provider/model/api_key/api_base` 字段,但当前 `auth-portal` 页面没有把这些字段暴露出来。 当前上线流程默认是:所有新实例先继承 `deploy-control` 上配置的默认 provider 凭证。 ### 5. 现在内置 HTTPS 吗 没有。 当前内置 `router-proxy` 是 HTTP 入口。如果你要公网 HTTPS: - 在外面再放一层 Nginx / Caddy / LB 做 TLS 终止 - 再把流量转给 `router-proxy:8088` 和 `auth-portal:3081` ## 10. 仓库结构 ```text /home/ivan/xuan/nano_project ├── README.md ├── app-instance/ ├── auth-portal/ ├── authz-service/ ├── deploy-control/ └── router-proxy/ ``` 各子目录更细的实现说明见: - [`app-instance/README.md`](/home/ivan/xuan/nano_project/app-instance/README.md) - [`auth-portal/src/README.md`](/home/ivan/xuan/nano_project/auth-portal/src/README.md) - [`authz-service/README.md`](/home/ivan/xuan/nano_project/authz-service/README.md) - [`deploy-control/README.md`](/home/ivan/xuan/nano_project/deploy-control/README.md) - [`router-proxy/README.md`](/home/ivan/xuan/nano_project/router-proxy/README.md)