# nano_project

单机部署版运行结构：

- `auth-portal`
  - 用户入口页，提供登录、注册、跳转。
- `authz-service`
  - AuthZ 服务。
  - 现在负责注册编排：`auth-portal -> authz-service -> deploy-control -> app-instance -> authz-service`
- `deploy-control`
  - 部署机控制面。
  - 负责创建实例、解析实例、刷新反向代理。
- `router-proxy`
  - 统一入口代理。
  - 按 Host 把 `<slug>.<base_domain>` 转发到对应实例容器。
- `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`
  - 实例基域名。
  - 测试环境推荐 `<SERVER_IP>.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://<slug>.<base_domain>: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://<server-ip>: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://<server-ip>: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://<instance-slug>.<base-domain>:8088
```

如果你前面用的是：

```text
NANO_BASE_DOMAIN=<server-ip>.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/<slug>/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)
