# nano_project 本机一步步部署指南

这份文档适合第一次在本机把整个项目跑起来的人，目标是：

- 在一台 `Linux` 或 `WSL2 Ubuntu` 机器上
- 用 `Docker` 跑完整链路
- 最后能在浏览器里注册账号，并自动创建你的专属实例

这套项目当前的推荐本机测试方式是：

- `auth-portal`
- `authz-service`
- `deploy-control`
- `router-proxy`
- `app-instance`

全部一起跑。

如果你只单独跑某个前端页面，页面能打开，但注册、登录、创建实例这些核心能力不一定会通。

---

## 0. 先说前提

### 适合的环境

推荐：

- Linux
- WSL2 Ubuntu

不推荐直接按这份文档在纯 Windows 命令行里照抄，因为这里依赖：

- Docker
- Bash 脚本
- Docker Socket 挂载
- 宿主机目录挂载

### 你需要先装好的工具

- `docker`
- `git`
- `curl`
- `openssl`
- `python3`

先检查：

```bash
docker --version
docker ps
python3 --version
openssl version
curl --version
```

如果 `docker ps` 报错，先把 Docker 启动起来。

---

## 1. 进入项目根目录

```bash
cd /home/ivan/xuan/nano_project
```

你执行完以后，建议顺手确认一下当前目录：

```bash
pwd
```

你应该看到：

```text
/home/ivan/xuan/nano_project
```

---

## 2. 准备一套本机测试变量

### 为什么这里用 `127.0.0.1.nip.io`

因为这是最省事的本机测试域名方案。

它的作用是：

- `alice.127.0.0.1.nip.io`
- 自动解析到 `127.0.0.1`

这样 `router-proxy` 就能按子域名区分不同实例。

### 直接复制执行

```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=127.0.0.1
export NANO_BASE_DOMAIN=127.0.0.1.nip.io

export NANO_PROVIDER=openai
export NANO_MODEL=openai/gpt-5
export NANO_API_KEY='把这里换成你自己的模型 API Key'
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'
```

### 这里每个变量大概是干什么的

- `PROJECT_ROOT`
  - 仓库根目录
- `NANO_NET`
  - 所有容器共用的 Docker 网络
- `NANO_DEPLOY_TOKEN`
  - `auth-portal` / `authz-service` 调 `deploy-control` 时的鉴权 token
- `NANO_AUTHZ_INTERNAL_TOKEN`
  - AuthZ 内部接口 token
- `NANO_BASE_DOMAIN`
  - 实例基础域名
- `NANO_PROVIDER`
  - 新实例默认模型提供商
- `NANO_MODEL`
  - 新实例默认模型
- `NANO_API_KEY`
  - 新实例默认模型 API Key
- `NANO_API_BASE`
  - 自定义模型网关地址，没有就留空
- `NANO_AUTHZ_URL`
  - 容器网络内访问 AuthZ 的地址
- `NANO_DEPLOY_URL`
  - 容器网络内访问 deploy-control 的地址
- `NANO_OUTLOOK_MCP_URL`
  - 可选；如果你有独立 Outlook MCP 服务，可以在这里填
- `NANO_OUTLOOK_MCP_SERVER_ID`
  - Outlook MCP 默认 server id，当前推荐固定 `outlook_mcp`

### 一个特别重要的提醒

`NANO_API_KEY` 不能空着。

如果这里不填，新用户注册时虽然页面可能能走到一半，但自动创建 `app-instance` 时大概率失败，因为实例配置里需要 `APP_INSTANCE_API_KEY`。

`NANO_AUTHZ_URL` 和 `NANO_DEPLOY_URL` 也不能留空，而且必须带协议头。

正确写法：

```text
http://nano-authz-service:19090
http://nano-deploy-control:8090
```

错误写法：

```text
nano-authz-service:19090
nano-deploy-control:8090
172.19.207.13:19090
172.19.207.13:8090
```

如果这里漏了 `http://`，注册页很容易直接报：

```text
502: Request URL is missing an 'http://' or 'https://' protocol.
```

还有一个很容易忽略的点：

- 你在 shell 里重新 `export NANO_DEPLOY_URL=...`
- 不会自动修改已经在运行中的 `nano-authz-service` 和 `nano-auth-portal`

也就是说：

- 变量改对了
- 但容器没重建

注册页还是会继续报同一个 502。

改完变量以后，至少要重建这些容器：

- `nano-authz-service`
- `nano-auth-portal`

---

## 3. 创建运行目录

```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"
```

这一步的作用是给下面几个东西留持久化空间：

- AuthZ 数据
- 实例注册表
- 每个用户实例的配置目录
- router-proxy 生成出来的路由文件

---

## 4. 构建镜像

第一次构建会比较久，正常情况要等几分钟。

```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
```

如果中间有某个镜像失败，不要继续往下跑，先把失败那一步修掉。

常见失败原因：

- Docker 没启动
- 网络拉镜像失败
- 你的本机磁盘空间不够

---

## 5. 创建共享 Docker 网络

```bash
docker network inspect "$NANO_NET" >/dev/null 2>&1 || docker network create "$NANO_NET"
```

执行完后可确认：

```bash
docker network ls | grep "$NANO_NET"
```

应该能看到：

```text
nano-instance-edge
```

---

## 6. 启动统一入口代理 `router-proxy`

```bash
cd "$PROJECT_ROOT"

PROXY_NETWORK_NAME="$NANO_NET" \
PROXY_HTTP_PORT=8088 \
./router-proxy/start-proxy.sh --replace
```

启动后，统一入口走：

```text
http://<你的实例slug>.127.0.0.1.nip.io:8088
```

例如：

```text
http://alice.127.0.0.1.nip.io:8088
```

---

## 7. 启动 `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` 这里不能写成：

```text
http://127.0.0.1:19090
```

因为后面新创建的 `app-instance` 也是通过 Docker 网络去访问 AuthZ 的。

所以这里要写容器网络里可访问的地址：

```text
http://nano-authz-service:19090
```

`DEPLOY_API_BASE_URL` 也一样，不能空，不能只写 `host:port`。

这里如果传成空字符串，或者写成：

```text
nano-deploy-control:8090
```

注册页会在 `authz-service -> deploy-control` 这一步直接报：

```text
502: Request URL is missing an 'http://' or 'https://' protocol.
```

启动完可以立刻确认：

```bash
docker inspect nano-authz-service --format '{{range .Config.Env}}{{println .}}{{end}}' | egrep '^(AUTHZ_ISSUER|DEPLOY_API_BASE_URL)='
```

---

## 8. 启动 `deploy-control`

这一步最容易配错的点是挂载目录。

一定要注意：

- `app-instance` 和 `router-proxy` 的宿主机路径，要按原路径挂进容器
- 不能偷懒挂到容器里的另一个短路径比如 `/app-instance`
- 同时要把 `APP_INSTANCE_DIR` 和 `ROUTER_PROXY_DIR` 也明确传进去

直接执行：

```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
```

### 这一步在做什么

`deploy-control` 会负责：

- 收到“创建实例”的请求
- 调用 `app-instance/create-instance.sh`
- 通过 Docker 创建对应用户实例
- 刷新 `router-proxy`

---

## 9. 启动 `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
```

这个页面就是用户看到的登录/注册入口。

虽然注册入口主要依赖 `AUTHZ_API_BASE_URL`，这里还是建议把 `DEPLOY_API_BASE_URL` 一起带上并确认非空，避免后面运行态调用 deploy-control 时再踩同一个坑。

启动完可以确认：

```bash
docker inspect nano-auth-portal --format '{{range .Config.Env}}{{println .}}{{end}}' | egrep '^(AUTHZ_API_BASE_URL|DEPLOY_API_BASE_URL)='
```

---

## 10. 做健康检查

### 先检查接口

```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
```

你应该大致看到：

- `authz-service` 返回健康 JSON
- `deploy-control` 返回健康 JSON
- `auth-portal` 返回 `HTTP/1.1 200 OK`

### 再看容器状态

```bash
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
```

你至少应该能看到这些容器：

- `nano-authz-service`
- `nano-deploy-control`
- `nano-auth-portal`
- `nano-router-proxy`

### 再看一下代理日志

```bash
docker logs --tail=50 nano-router-proxy
```

如果这一步没有明显报错，就可以开始浏览器测试了。

---

## 11. 浏览器首次测试

打开：

```text
http://127.0.0.1:3081/register
```

然后按顺序操作：

1. 注册一个新账号
2. 注册成功后，系统会自动创建一个你的专属实例
3. 浏览器应该跳到你的实例地址

跳转目标一般长这样：

```text
http://你的slug.127.0.0.1.nip.io:8088
```

例如：

```text
http://alice.127.0.0.1.nip.io:8088
```

---

## 12. 确认实例真的被创建出来了

```bash
cd "$PROJECT_ROOT/app-instance"
./list-instances.sh
./list-instances.sh --json
```

你应该能看到类似：

- `instance_id`
- `instance_slug`
- `container_name`
- `public_url`

以及对应的 `app-instance-<slug>` 容器。

你还可以继续查：

```bash
docker ps --format 'table {{.Names}}\t{{.Status}}' | grep app-instance
```

---

## 13. 如果你只是想单独看前端页面

如果你只是想看 `auth-portal` 页面样子，不跑全链路，也可以单独启动它的前端开发模式：

```bash
cd /home/ivan/xuan/nano_project/auth-portal/src
npm install
npm run dev
```

然后打开：

```text
http://127.0.0.1:3081
```

但是要注意：

- 这只能看页面
- 注册、登录、创建实例这些动作是否成功，仍然取决于 `authz-service` 和 `deploy-control` 有没有另外启动

---

## 14. 一键排错命令

如果你感觉“不对劲”，先跑这几条：

```bash
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'

docker logs --tail=100 nano-authz-service
docker logs --tail=100 nano-deploy-control
docker logs --tail=100 nano-auth-portal
docker logs --tail=100 nano-router-proxy

curl http://127.0.0.1:19090/healthz
curl http://127.0.0.1:8090/healthz
curl -I http://127.0.0.1:3081
```

如果是实例创建失败，再加两条：

```bash
cd "$PROJECT_ROOT/app-instance"
./list-instances.sh --json

docker ps --format 'table {{.Names}}\t{{.Status}}' | grep app-instance
```

如果注册页弹出：

```text
502: Request URL is missing an 'http://' or 'https://' protocol.
```

优先查这两条：

```bash
docker inspect nano-authz-service --format '{{range .Config.Env}}{{println .}}{{end}}' | egrep '^(AUTHZ_ISSUER|DEPLOY_API_BASE_URL)='
docker inspect nano-auth-portal --format '{{range .Config.Env}}{{println .}}{{end}}' | egrep '^(AUTHZ_API_BASE_URL|DEPLOY_API_BASE_URL)='
```

重点看：

- `nano-authz-service` 里的 `DEPLOY_API_BASE_URL`
- `nano-auth-portal` 里的 `AUTHZ_API_BASE_URL`

它们都必须是完整 URL，不能是空字符串，也不能是裸 `host:port`。

如果你已经改过 `export NANO_DEPLOY_URL=...`，但这里查出来还是空，说明你只是改了当前 shell 变量，没有把容器重建掉。

这时直接按下面重建：

```bash
export NANO_AUTHZ_URL='http://nano-authz-service:19090'
export NANO_DEPLOY_URL='http://nano-deploy-control:8090'

export NANO_DEPLOY_TOKEN="$(docker inspect nano-deploy-control --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n 's/^DEPLOY_CONTROL_API_TOKEN=//p')"
export NANO_AUTHZ_INTERNAL_TOKEN="$(docker inspect nano-authz-service --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n 's/^AUTHZ_INTERNAL_TOKEN=//p')"

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

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
```

重建后再确认：

```bash
docker inspect nano-authz-service --format '{{range .Config.Env}}{{println .}}{{end}}' | egrep '^(AUTHZ_ISSUER|DEPLOY_API_BASE_URL)='
docker inspect nano-auth-portal --format '{{range .Config.Env}}{{println .}}{{end}}' | egrep '^(AUTHZ_API_BASE_URL|DEPLOY_API_BASE_URL)='
```

你必须看到：

```text
DEPLOY_API_BASE_URL=http://nano-deploy-control:8090
```

---

## 15. 最常见的坑

### 1. API Key 没填

现象：

- 注册页面提交后创建实例失败

原因：

- `APP_INSTANCE_API_KEY` 没有有效值

### 2. Docker 没启动

现象：

- `deploy-control` 无法创建实例
- 或 `docker ps` 本身就报错

### 3. `AUTHZ_ISSUER` 写成了 `127.0.0.1`

错误写法：

```text
http://127.0.0.1:19090
```

正确写法：

```text
http://nano-authz-service:19090
```

原因：

- 新实例容器里访问不到宿主机自己的 `127.0.0.1:19090`

### 4. `deploy-control` 的路径挂载写错

错误思路：

- 把宿主机的 `app-instance` 挂到容器里的 `/app-instance`

正确思路：

- 宿主机原路径挂进去
- 并设置：
  - `APP_INSTANCE_DIR="$PROJECT_ROOT/app-instance"`
  - `ROUTER_PROXY_DIR="$PROJECT_ROOT/router-proxy"`

### 5. `NANO_DEPLOY_URL` 或 `NANO_AUTHZ_URL` 没带 `http://`

典型现象：

- 注册页直接弹：
  - `502: Request URL is missing an 'http://' or 'https://' protocol.`

原因：

- `authz-service` 用 `DEPLOY_API_BASE_URL` 去调 `deploy-control`
- `auth-portal` 用 `AUTHZ_API_BASE_URL` 去调 `authz-service`
- 这些值如果是空，或者写成 `nano-deploy-control:8090` 这种不带协议的字符串，请求会直接失败
- 就算你后来在 shell 里改对了，如果没重建相关容器，老的错误值仍然会继续生效

正确写法：

```text
http://nano-authz-service:19090
http://nano-deploy-control:8090
```

### 6. `nip.io` 解析失败

如果实例跳转地址打不开，先试：

```bash
ping 127.0.0.1.nip.io
```

如果你本地网络把 `nip.io` 拦了，这套子域名测试方式就会失效。

### 7. 端口被占用

默认会用到这些端口：

- `3081`
- `8090`
- `19090`
- `8088`

你可以先查：

```bash
ss -ltnp | grep -E '3081|8090|19090|8088'
```

---

## 16. 如果你要重新来一遍

如果你只是想“重新部署这四个基础容器”，可以先停掉它们：

```bash
docker rm -f \
  nano-auth-portal \
  nano-authz-service \
  nano-deploy-control \
  nano-router-proxy 2>/dev/null || true
```

如果你还想把旧实例容器也一起清掉，再额外处理 `app-instance-*`。

注意：

- 不要在你还需要旧数据的时候乱删 `runtime/`
- `authz-service/runtime/data` 和 `app-instance/runtime/instances` 里都有持久化数据

---

## 17. 本机部署成功后的结果应该是什么

如果整个流程正常，最后你会得到：

- 一个可以打开的注册页：
  - `http://127.0.0.1:3081/register`
- 一个统一实例入口代理：
  - `http://<slug>.127.0.0.1.nip.io:8088`
- 一个能自动创建用户专属容器的部署控制面
- 一份实例注册表：
  - `app-instance/runtime/registry/instances.json`

---

## 18. 你下一步最建议做什么

第一次建议这样测：

1. 用全新用户名注册一个测试账号
2. 确认浏览器跳到了你的实例 URL
3. 再执行 `./app-instance/list-instances.sh --json`
4. 确认注册表里真的有这条实例记录

如果你后面想要，我还可以继续补两份文档：

- `服务器部署指南.md`
  - 面向公网服务器、固定 IP、长期运行
- `常见报错排查.md`
  - 专门收集 502、超时、实例起不来、MCP 鉴权失败这类问题
