# Beaver Project 域名配置指引

最后更新：2026-06-16。

这份文档说明如何从本机测试域名 `localhost` 子域名切换到正式域名。

核心结论：

- DNS 只负责把域名解析到 IP。
- DNS 不负责端口。
- `auth-portal` 和用户实例建议使用不同域名。
- 正式环境建议用外层 Nginx、Caddy、Traefik 或云负载均衡监听 `80/443`。
- `router-proxy` 必须收到原始 `Host` 头，才能按实例域名转发。
- 正式实例入口推荐使用真实域名；不要用裸 IP 当实例基域名，除非你明确要走每实例直连端口模式。

## 1. 默认端口职责

| 端口 | 组件 | 是否建议公网直接暴露 |
| --- | --- | --- |
| `3081` | `auth-portal`，用户登录和注册入口 | 可以，或由外层代理转发 |
| `8088` | `router-proxy`，所有实例统一入口 | 可以，或由外层代理转发 |
| `8090` | `deploy-control`，内部部署控制面 | 不建议 |
| `19090` | `authz-service`，内部鉴权服务 | 不建议 |
| `8787` | `external-connector` sidecar 管理/调试口 | 不建议 |
| `9000/9001` | 本地 MinIO S3 API / Console | 不建议 |
| `20000-29999` | app-instance 直连端口池，通常绑定 `127.0.0.1`，裸 IP 模式可能对外绑定 | 不建议 |

正式部署时，通常由外层入口暴露 `80/443`，再转发到本机端口：

```text
portal.example.com  -> 127.0.0.1:3081
*.apps.example.com  -> 127.0.0.1:8088
```

## 2. 推荐域名规划

推荐拆成两个域名空间：

```text
https://portal.example.com
https://alice.apps.example.com
https://bob.apps.example.com
```

含义：

- `portal.example.com` 给 `auth-portal`
- `*.apps.example.com` 给 `router-proxy`
- 每个实例使用一个子域名，例如 `alice.apps.example.com`

不要把门户和实例混在同一个 Host 上。`router-proxy` 是实例入口，`auth-portal` 是认证入口，两者职责不同。

## 3. DNS 要怎么配

假设服务器公网 IP 是 `203.0.113.10`。

DNS 记录：

```text
portal.example.com      A      203.0.113.10
apps.example.com        A      203.0.113.10
*.apps.example.com      A      203.0.113.10
```

如果你的 DNS 服务商支持 CNAME，也可以让通配子域名 CNAME 到一个已有 A 记录，但最终结果仍然必须能解析到服务器入口 IP。

注意：

- `*.apps.example.com` 用于实例子域名。
- `apps.example.com` 本身不是必须给用户访问，但建议也解析到同一入口，方便证书和排查。
- DNS 不会决定 `3081`、`8088`、`443` 这些端口。

## 4. 外层反向代理要做什么

外层代理负责：

- 监听公网 `80/443`
- 处理 TLS 证书
- 按 Host 转发请求
- 透传原始 Host
- 支持 WebSocket upgrade

最小映射：

```text
portal.example.com  -> http://127.0.0.1:3081
*.apps.example.com  -> http://127.0.0.1:8088
```

`*.apps.example.com` 转发到 `router-proxy` 时必须保留原始 Host，例如：

```nginx
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
```

否则 `router-proxy` 无法知道请求属于哪个实例。

如果需要支持用户文件系统的大文件上传，外层代理还要允许足够大的 body。项目内 app-instance Nginx 当前是 `client_max_body_size 5g`，外层 Nginx/Caddy/负载均衡的限制不能比实际业务需求更小。

## 5. 项目内部要改哪些变量

实例公网地址由 `deploy-control` 里的这些变量决定：

| 变量 | 含义 |
| --- | --- |
| `DEPLOY_PUBLIC_SCHEME` | 对外协议，`http` 或 `https` |
| `DEPLOY_PUBLIC_BASE_DOMAIN` | 实例基域名，例如 `apps.example.com` |
| `DEPLOY_PUBLIC_HOST_TEMPLATE` | Host 生成模板，默认 `{slug}.{base_domain}` |
| `DEPLOY_PUBLIC_PORT` | 对外端口，`80` / `443` 会在生成 URL 时省略 |
| `DEPLOY_DIRECT_PUBLIC_HOST_BIND_IP` | 仅裸 IP 基域名直连模式使用，控制实例宿主机端口绑定地址 |

本机测试：

```bash
export BEAVER_BASE_DOMAIN=localhost
```

`deploy-control`：

```bash
-e DEPLOY_PUBLIC_SCHEME="http" \
-e DEPLOY_PUBLIC_BASE_DOMAIN="$BEAVER_BASE_DOMAIN" \
-e DEPLOY_PUBLIC_PORT="8088" \
```

生成实例地址：

```text
http://alice.localhost:8088
```

正式 HTTPS：

```bash
export BEAVER_BASE_DOMAIN=apps.example.com
```

`deploy-control`：

```bash
-e DEPLOY_PUBLIC_SCHEME="https" \
-e DEPLOY_PUBLIC_BASE_DOMAIN="apps.example.com" \
-e DEPLOY_PUBLIC_PORT="443" \
```

生成实例地址：

```text
https://alice.apps.example.com
```

前提是外层代理已经把 `*.apps.example.com:443` 转发到 `router-proxy:8088`。

裸 IP 特例：

```bash
export BEAVER_BASE_DOMAIN=203.0.113.10
```

当 `DEPLOY_PUBLIC_BASE_DOMAIN` 是 IP 地址时，`deploy-control` 会进入直连端口模式：每个实例从 `20000-29999` 端口池分配一个宿主机端口，生成类似：

```text
http://203.0.113.10:20037
```

这不是 `router-proxy` 的 Host 路由入口，也无法得到 `https://alice.apps.example.com` 这类实例子域名。正式环境推荐使用 `apps.example.com` 这类真实域名和通配 DNS。

## 6. 什么时候 URL 里可以不带端口

浏览器默认端口：

- `http` 默认 `80`
- `https` 默认 `443`

所以：

```bash
DEPLOY_PUBLIC_SCHEME=https
DEPLOY_PUBLIC_PORT=443
```

生成：

```text
https://alice.apps.example.com
```

而不是：

```text
https://alice.apps.example.com:443
```

如果你设置：

```bash
DEPLOY_PUBLIC_SCHEME=http
DEPLOY_PUBLIC_PORT=8088
```

生成：

```text
http://alice.apps.example.com:8088
```

因为 `8088` 不是 HTTP 默认端口。

## 7. 一套推荐生产配置

假设：

- 门户域名：`portal.example.com`
- 实例基域名：`apps.example.com`
- 外层代理负责 HTTPS
- 项目基础容器仍在同一台机器上通过 Docker 运行

部署变量：

```bash
export BEAVER_BASE_DOMAIN=apps.example.com
export BEAVER_AUTHZ_URL='http://beaver-authz-service:19090'
export BEAVER_DEPLOY_URL='http://beaver-deploy-control:8090'
```

`deploy-control`：

```bash
-e DEPLOY_PUBLIC_SCHEME="https" \
-e DEPLOY_PUBLIC_BASE_DOMAIN="apps.example.com" \
-e DEPLOY_PUBLIC_PORT="443" \
```

外层代理：

```text
portal.example.com  -> 127.0.0.1:3081
*.apps.example.com  -> 127.0.0.1:8088
```

DNS：

```text
portal.example.com      -> 服务器 IP
apps.example.com        -> 服务器 IP
*.apps.example.com      -> 服务器 IP
```

正常域名部署不依赖 `DEPLOY_DIRECT_PUBLIC_HOST_BIND_IP`；它只影响裸 IP 直连端口模式。生产入口应优先让外层代理监听 `80/443`，再转发到本机 `3081` 和 `8088`。

## 8. Nginx 外层代理示例

示例只展示关键转发逻辑，证书路径和自动签发方式按你的环境调整。

```nginx
server {
  listen 80;
  server_name portal.example.com *.apps.example.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl http2;
  server_name portal.example.com;

  ssl_certificate /etc/letsencrypt/live/portal.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/portal.example.com/privkey.pem;

  location / {
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass http://127.0.0.1:3081;
  }
}

server {
  listen 443 ssl http2;
  server_name *.apps.example.com;

  ssl_certificate /etc/letsencrypt/live/apps.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/apps.example.com/privkey.pem;

  location / {
    client_max_body_size 5g;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
    proxy_pass http://127.0.0.1:8088;
  }
}
```

证书注意：

- `portal.example.com` 可以用普通单域名证书。
- `*.apps.example.com` 需要通配符证书，通常要用 DNS-01 验证。
- 也可以用支持自动证书的 Caddy / Traefik 简化这层。

## 9. 常见误区

### DNS 不能配置端口

DNS 只能做：

```text
域名 -> IP
```

端口来自：

- URL 里显式端口
- 协议默认端口
- 外层代理监听和转发规则

### 不要把 portal 转到 8088

`8088` 是实例入口，不是认证门户入口。

推荐：

```text
portal.example.com  -> 3081
*.apps.example.com  -> 8088
```

### 不要公开内部端口

`8090` 是部署控制面，`19090` 是内部 AuthZ 服务。它们应该只允许容器网络或可信内网访问。

同理，本地 MinIO 的 `9000/9001`、`external-connector:8787` 和实例直连端口池 `20000-29999` 也不应该作为正式公网入口。正式入口通常只有：

```text
https://portal.example.com
https://<slug>.apps.example.com
```

外层代理再把它们分别转发到本机 `3081` 和 `8088`。

### 修改 DEPLOY_PUBLIC_* 后旧实例不会自动改 URL

这些变量影响新创建实例的 `public_url` 和 `instance_host`。旧实例已经写入注册表，需要重新创建或手动更新注册表和代理配置。

### 裸 IP 不是通配子域名

如果 `DEPLOY_PUBLIC_BASE_DOMAIN=203.0.113.10`，系统会生成 `http://203.0.113.10:<host_port>` 形式的直连地址，不会生成可用的 `alice.203.0.113.10` 子域名入口。要使用每用户子域名，必须准备真实域名并配置 `*.apps.example.com` 这类通配 DNS。

## 10. 本机测试不需要正式域名

如果只是本机验证完整链路，继续使用：

```bash
export BEAVER_BASE_DOMAIN=localhost
```

它已经足够测试：

- 注册
- 登录
- 自动创建实例
- 跳转个人实例

等准备对外访问时，再切换正式 DNS、HTTPS 和外层代理。
