diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fd1f9c5 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +# Shared values used by the root deployment flow in README.md + +PROJECT_ROOT=/home/ivan/xuan/nano_project +NANO_NET=nano-instance-edge + +NANO_DEPLOY_TOKEN=change-me +NANO_AUTHZ_INTERNAL_TOKEN=change-me + +NANO_SERVER_IP=203.0.113.10 +NANO_BASE_DOMAIN=203.0.113.10.nip.io + +NANO_PROVIDER=openai +NANO_MODEL=openai/gpt-5 +NANO_API_KEY=sk-xxxxxxxx +NANO_API_BASE= + +# Must be reachable from app-instance containers. +NANO_AUTHZ_URL=http://nano-authz-service:19090 + +# Must be reachable from auth-portal and authz-service containers. +NANO_DEPLOY_URL=http://nano-deploy-control:8090 diff --git a/README.md b/README.md index b6f0556..39c69fe 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,395 @@ # nano_project -按当前部署模型整理后的顶层目录: +单机部署版运行结构: -- `app-instance/` - - 单用户实例容器。 - - 一个容器里同时包含前端和后端。 - - `backend/` 来自 `nanobot-backend` - - `frontend/` 来自 `nanobot-fronted` - - 已包含统一 Dockerfile、启动脚本和实例管理脚本。 -- `authz-service/` - - 鉴权服务容器。 - - `src/` 来自 `Authz-service` - - 已包含 Dockerfile、启动脚本和空白种子数据。 -- `auth-portal/` - - 登录 / 注册 / 跳转入口容器。 - - `src/` 来自 `nanobot-auth-portal` - - `runtime/` 预留给后续启动脚本和环境配置。 -- `deploy-control/` - - 部署机接口容器。 - - 负责创建实例、解析实例路由、刷新反向代理。 -- `router-proxy/` - - 专属 URL 到实例容器的反向代理容器。 - - 按 Host 路由到对应 `app-instance`。 +- `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 +``` -- `/home/ivan/xuan/nano_project/app-instance/backend` 来自 `/home/ivan/xuan/steven_project/nanobot-backend` -- `/home/ivan/xuan/nano_project/app-instance/frontend` 来自 `/home/ivan/xuan/steven_project/nanobot-fronted` -- `/home/ivan/xuan/nano_project/authz-service/src` 来自 `/home/ivan/xuan/steven_project/Authz-service` -- `/home/ivan/xuan/nano_project/auth-portal/src` 来自 `/home/ivan/xuan/steven_project/nanobot-auth-portal` +登录: -之后你在 `nano_project` 里继续改代码,不会再联动改到原仓库。 +```text +Browser + -> auth-portal + -> deploy-control POST /api/instances/resolve + -> app-instance POST /api/auth/login +``` -## 本次复制排除项 +## 这份部署指南的前提 -为避免把本地依赖和构建垃圾一起带过来,这次复制排除了这些内容: +这份 README 只覆盖一套基准方案: -- `.git/` -- `.venv/` -- `node_modules/` -- `.next/` -- `.next-dev/` -- `.pytest_cache/` -- `.ruff_cache/` -- `__pycache__/` -- `dist/` -- `build/` -- `*.pyc` -- `tsconfig.tsbuildinfo` -- `.env` -- `.env.local` +- 一台 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_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_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:/app-instance" \ + -v "$PROJECT_ROOT/router-proxy:/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 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_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 -│ ├── backend/ -│ ├── frontend/ -│ └── runtime/ -├── deploy-control -│ ├── Dockerfile -│ └── server.py -├── router-proxy -│ ├── runtime/ -│ ├── nginx.conf -│ └── render-routes.py -├── authz-service -│ ├── src/ -│ ├── runtime/ -│ └── Dockerfile -└── auth-portal - ├── src/ - └── runtime/ +├── app-instance/ +├── auth-portal/ +├── authz-service/ +├── deploy-control/ +└── router-proxy/ ``` -## app-instance 当前可用能力 +各子目录更细的实现说明见: -`/home/ivan/xuan/nano_project/app-instance` 现在已经具备: - -- 统一镜像构建:`Dockerfile` -- 容器内启动前端、后端、Nginx:`entrypoint.sh` -- 创建实例并写配置、起容器、登记注册表:`create-instance.sh` -- 查看实例:`list-instances.sh` -- 删除实例并可选清理数据:`remove-instance.sh` -- 实例注册表与端口分配:`instance-registry.py` - -更具体的使用说明见: - -- `/home/ivan/xuan/nano_project/app-instance/README.md` -- `/home/ivan/xuan/nano_project/deploy-control/README.md` -- `/home/ivan/xuan/nano_project/router-proxy/README.md` -- `/home/ivan/xuan/nano_project/authz-service/README.md` - -## 后续建议 - -下一步可以在这三个目录下分别补: - -- `.env` 模板 -- portal 到部署机的创建实例调用 -- portal 登录后的统一账号查找和跳转联调 -- authz-service 的控制面接入和部署编排 +- [`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) diff --git a/auth-portal/src/.env.example b/auth-portal/src/.env.example new file mode 100644 index 0000000..eaab1eb --- /dev/null +++ b/auth-portal/src/.env.example @@ -0,0 +1,5 @@ +# auth-portal server-side runtime config + +AUTHZ_API_BASE_URL=http://nano-authz-service:19090 +DEPLOY_API_BASE_URL=http://nano-deploy-control:8090 +DEPLOY_API_TOKEN=change-me diff --git a/auth-portal/src/README.md b/auth-portal/src/README.md index ca52b74..ea77727 100644 --- a/auth-portal/src/README.md +++ b/auth-portal/src/README.md @@ -10,6 +10,10 @@ Dedicated login/register frontend for nanobot containers. Registration now goes through AuthZ, while login/runtime lookup still uses deploy-control: +See also: + +- [`.env.example`](/home/ivan/xuan/nano_project/auth-portal/src/.env.example) + ```bash AUTHZ_API_BASE_URL=http://127.0.0.1:19090 DEPLOY_API_BASE_URL=http://127.0.0.1:8090 diff --git a/authz-service/.env.example b/authz-service/.env.example new file mode 100644 index 0000000..4730fd0 --- /dev/null +++ b/authz-service/.env.example @@ -0,0 +1,9 @@ +# authz-service runtime config + +AUTHZ_ISSUER=http://nano-authz-service:19090 +AUTHZ_INTERNAL_TOKEN=change-me +AUTHZ_ACCESS_TOKEN_TTL_SECONDS=3600 +AUTHZ_UPSTREAM_TIMEOUT_SECONDS=15 + +DEPLOY_API_BASE_URL=http://nano-deploy-control:8090 +DEPLOY_API_TOKEN=change-me diff --git a/authz-service/README.md b/authz-service/README.md index 09391d9..e0904da 100644 --- a/authz-service/README.md +++ b/authz-service/README.md @@ -10,8 +10,8 @@ - 初始化数据目录并启动 `uvicorn` - `start-authz.sh` - 本地或服务器快速启动脚本 -- `env_template` - - 常用环境变量模板 +- `.env.example` + - 推荐的环境变量模板 - `runtime/seed-data/` - 空白初始化数据 - `src/` @@ -29,7 +29,11 @@ ```bash cd /home/ivan/xuan/nano_project/authz-service -AUTHZ_INTERNAL_TOKEN='change-me' ./start-authz.sh --build +cp .env.example .env +set -a +. ./.env +set +a +./start-authz.sh --build ``` 启动后验证: diff --git a/deploy-control/.env.example b/deploy-control/.env.example new file mode 100644 index 0000000..812c68e --- /dev/null +++ b/deploy-control/.env.example @@ -0,0 +1,28 @@ +# deploy-control runtime config + +DEPLOY_CONTROL_HOST=0.0.0.0 +DEPLOY_CONTROL_PORT=8090 +DEPLOY_CONTROL_API_TOKEN=change-me + +APP_INSTANCE_IMAGE=nano/app-instance:latest +APP_INSTANCE_NETWORK_NAME=nano-instance-edge + +APP_INSTANCE_PROVIDER=openai +APP_INSTANCE_MODEL=openai/gpt-5 +APP_INSTANCE_API_KEY=sk-xxxxxxxx +APP_INSTANCE_API_BASE= + +# Used as a fallback when authz-service does not explicitly pass authz_base_url. +DEFAULT_AUTHZ_BASE_URL=http://nano-authz-service:19090 + +DEPLOY_PUBLIC_SCHEME=http +DEPLOY_PUBLIC_BASE_DOMAIN=203.0.113.10.nip.io +DEPLOY_PUBLIC_HOST_TEMPLATE={slug}.{base_domain} +DEPLOY_PUBLIC_PORT=8088 +DEPLOY_AUTO_START_PROXY=1 +DEPLOY_HEALTH_TIMEOUT_SECONDS=60 +DEPLOY_HEALTH_INTERVAL_SECONDS=1 + +# Passed through to create-instance.sh when the app-instance image is rebuilt. +AUTH_PORTAL_URL=http://203.0.113.10:3081 +AUTH_PORTAL_PORT=3081 diff --git a/deploy-control/README.md b/deploy-control/README.md index 8424a4a..16c1bdb 100644 --- a/deploy-control/README.md +++ b/deploy-control/README.md @@ -23,6 +23,10 @@ - `DEPLOY_PUBLIC_SCHEME` - `APP_INSTANCE_NETWORK_NAME` +建议直接参考: + +- [`.env.example`](/home/ivan/xuan/nano_project/deploy-control/.env.example) + 默认实例 URL 形如: ```text diff --git a/router-proxy/.env.example b/router-proxy/.env.example new file mode 100644 index 0000000..1b0a7fb --- /dev/null +++ b/router-proxy/.env.example @@ -0,0 +1,9 @@ +# router-proxy startup config + +PROXY_IMAGE=nginx:1.27-alpine +PROXY_CONTAINER_NAME=nano-router-proxy +PROXY_NETWORK_NAME=nano-instance-edge +PROXY_HTTP_PORT=8088 + +REGISTRY_PATH=/home/ivan/xuan/nano_project/app-instance/runtime/registry/instances.json +OUTPUT_PATH=/home/ivan/xuan/nano_project/router-proxy/runtime/conf.d/instances.conf diff --git a/router-proxy/README.md b/router-proxy/README.md index e67852d..391cf02 100644 --- a/router-proxy/README.md +++ b/router-proxy/README.md @@ -23,6 +23,10 @@ - Docker network:`nano-instance-edge` - 对外端口:`8088` +建议直接参考: + +- [`.env.example`](/home/ivan/xuan/nano_project/router-proxy/.env.example) + ## 启动 ```bash