# 部署与运维

## 1. 运行模型

Edgeservice 只交付一个镜像。镜像内包含 Vue 静态资源、Spring Boot/Java 25 运行时、Eclipse Milo 和 SQLite，不依赖 Compose 或其他必需容器。容器以 UID/GID `10001` 运行，所有可变数据写入 `/data`。

推荐主机基线为 4 vCPU、8 GiB RAM、SSD，`/data` 至少 30 GiB。产品只支持回环地址或内网地址，不支持直接暴露到公网。

## 2. 构建和启动

```bash
docker build --pull -t opc-hub:0.1.0 .

docker volume create opchub-data
docker run -d --name opc-hub --restart unless-stopped \
  -p 127.0.0.1:18080:18080 \
  -p 127.0.0.1:14840-14849:14840-14849 \
  -e OPCHUB_ADMIN_PASSWORD='replace-with-a-long-random-password' \
  -e OPCHUB_HTTP_BIND_ADDRESS=0.0.0.0 \
  -e OPCHUB_OPCUA_BIND_ADDRESS=0.0.0.0 \
  -e OPCHUB_ALLOW_INSECURE_LAN=true \
  -e OPCHUB_OPCUA_ALLOW_ANONYMOUS=true \
  -e OPCHUB_OPCUA_ALLOW_INSECURE_ENDPOINT=true \
  -v opchub-data:/data \
  opc-hub:0.1.0
```

这里的容器进程监听 `0.0.0.0`，但 Docker 只把端口映射到宿主机 `127.0.0.1`；风险确认及 Anonymous/None 均为本机试用而显式开启。若端口映射到内网地址，必须使用下节的 TLS/安全 Endpoint 配置。

检查启动：

```bash
docker inspect --format '{{json .State.Health}}' opc-hub
curl --fail http://127.0.0.1:18080/actuator/health/readiness
```

首次启动会创建 `config.db`、`runtime.db`、主密钥和 OPC UA Server PKCS#12。启动密码只用于首次创建管理员记录，数据库中保存 BCrypt 哈希；后续修改该环境变量不会重置现有管理员。

## 3. 配置

| 环境变量 | 默认值 | 说明 |
| --- | --- | --- |
| `OPCHUB_DATA_DIRECTORY` | `/data` | 持久化根目录 |
| `OPCHUB_ADMIN_USERNAME` | `admin` | 首次管理员名，3–64 个安全字符 |
| `OPCHUB_ADMIN_PASSWORD` | 无 | 首次启动必填，12–256 字符 |
| `OPCHUB_MASTER_KEY` | 无 | 可选；Base64 编码的 32 字节密钥。缺省时在 `/data/secrets/master.key` 生成 |
| `OPCHUB_HTTP_BIND_ADDRESS` | `127.0.0.1` | 管理及业务 REST 监听地址 |
| `OPCHUB_HTTP_PORT` | `18080` | HTTP 端口 |
| `OPCHUB_HTTP_TLS_ENABLED` | `false` | 启用 Spring Boot HTTPS |
| `OPCHUB_HTTP_KEY_STORE` | 无 | HTTP PKCS#12/JKS 路径 |
| `OPCHUB_HTTP_KEY_STORE_PASSWORD` | 无 | HTTP KeyStore 密码，建议由容器 Secret 注入 |
| `OPCHUB_HTTP_KEY_STORE_TYPE` | `PKCS12` | HTTP KeyStore 类型 |
| `OPCHUB_ALLOW_INSECURE_LAN` | `false` | 对非回环监听的显式风险确认；生产 HTTP 应优先启用 TLS |
| `OPCHUB_OPCUA_SERVER_ENABLED` | `true` | 启用内嵌 OPC UA Server |
| `OPCHUB_OPCUA_BIND_ADDRESS` | `127.0.0.1` | OPC UA 监听地址 |
| `OPCHUB_OPCUA_PORT` | `14840` | 默认 OPC UA Server 端口；新建 Server 从该端口起选择首个未占用端口 |
| `OPCHUB_OPCUA_ADVERTISED_HOST` | `127.0.0.1` | Endpoint 对客户端公布的主机名/IP |
| `OPCHUB_OPCUA_ENDPOINT_PATH` | `/opchub/server` | Endpoint 路径 |
| `OPCHUB_OPCUA_APPLICATION_URI` | `urn:opchub:server` | Server Application URI |
| `OPCHUB_OPCUA_NAMESPACE_URI` | `urn:opchub:published` | 发布变量 Namespace URI |
| `OPCHUB_OPCUA_ALLOW_ANONYMOUS` | 回环为 `true`，其他为 `false` | 是否接受匿名用户令牌 |
| `OPCHUB_OPCUA_USERNAME` | 无 | 可选用户名认证账户 |
| `OPCHUB_OPCUA_PASSWORD` | 无 | 可选用户名认证密码 |
| `OPCHUB_OPCUA_ALLOW_INSECURE_ENDPOINT` | 回环为 `true`，其他为 `false` | 是否提供 SecurityPolicy None |
| `OPCHUB_RETENTION_CLEANUP_CRON` | `0 17 2 * * *` | UTC 清理计划 |

内网监听示例：HTTP 使用 PKCS#12 TLS；OPC UA 只提供 Basic256Sha256/SignAndEncrypt，并启用明确身份认证。

```bash
docker run -d --name opc-hub \
  -p 10.10.1.20:8443:8443 -p 10.10.1.20:14840:14840 \
  -e OPCHUB_HTTP_BIND_ADDRESS=0.0.0.0 \
  -e OPCHUB_HTTP_PORT=8443 \
  -e OPCHUB_HTTP_TLS_ENABLED=true \
  -e OPCHUB_HTTP_KEY_STORE=/data/certificates/http/server.p12 \
  -e OPCHUB_HTTP_KEY_STORE_PASSWORD='read-from-your-secret-manager' \
  -e OPCHUB_OPCUA_BIND_ADDRESS=0.0.0.0 \
  -e OPCHUB_OPCUA_ADVERTISED_HOST=10.10.1.20 \
  -e OPCHUB_OPCUA_ALLOW_ANONYMOUS=false \
  -e OPCHUB_OPCUA_ALLOW_INSECURE_ENDPOINT=false \
  -e OPCHUB_ALLOW_INSECURE_LAN=true \
  -e OPCHUB_ADMIN_PASSWORD='replace-with-a-long-random-password' \
  -v opchub-data:/data opc-hub:0.1.0
```

实际部署应由 Secret Manager 注入密码，不要把密码写进镜像、脚本或版本库。本例中的 `OPCHUB_ALLOW_INSECURE_LAN=true` 是对内网绑定的显式确认，不会启用 OPC UA None Endpoint。

## 4. 数据与权限

```text
/data/database/config.db      Connection、版本、凭据元数据、管理员、证书元数据和审计
/data/database/runtime.db     触发、Run、节点轨迹、快照和聚合桶
/data/secrets/master.key      本地生成的 AES 主密钥（若未通过环境变量提供）
/data/secrets/credentials     AES-256-GCM 密文
/data/certificates            OPC UA KeyStore 和显式信任库
/data/backups                 一致性 ZIP 备份
/data/payloads                受大小/保留期管理的大 Payload
```

挂载宿主目录时，把目录所有者设为 `10001:10001`，权限建议 `0700`。不要单独复制正在运行的 SQLite 文件；使用管理 API 创建一致性备份。

## 5. 备份与恢复

创建并下载备份：

```bash
curl -u admin:password -X POST http://127.0.0.1:18080/_admin/api/backups
curl -u admin:password -OJ http://127.0.0.1:18080/_admin/api/backups/<returned-file-name>
```

备份使用 SQLite `VACUUM INTO` 获得两个数据库的一致副本。若主密钥由 Edgeservice 本地生成，归档会包含它，因此整个 ZIP 必须按密钥材料保护；若使用 `OPCHUB_MASTER_KEY`，归档不会包含该密钥。

恢复步骤：

1. 停止容器并保存当前 `/data` 的只读副本。
2. 在空目录解压备份，得到 `database/config.db`、`database/runtime.db`，以及可能存在的 `secrets/master.key`。
3. 同步恢复原 `/data/certificates`；证书私钥文件不包含在数据库备份中。若证书资产需要一起灾备，应对整个静止的 `/data` 另做加密卷快照。
4. 若备份不含主密钥，注入与源实例完全相同的 `OPCHUB_MASTER_KEY`。
5. 以相同镜像启动，确认 readiness、登录、凭据解析和一次草稿试运行。
6. 对照 [验收手册](ACCEPTANCE.md) 执行恢复检查后再切换流量。

## 6. 升级与回滚

升级前创建 API 备份并记录当前镜像摘要。停止旧容器，使用同一 `/data` 启动新镜像；Flyway 只执行向前迁移。回滚到无法识别新 Schema 的旧镜像时，必须先停止新容器并恢复升级前备份，不能让两个版本同时写同一 SQLite 目录。

## 7. 运行诊断

- readiness/liveness：`/actuator/health/readiness`、`/actuator/health/liveness`。
- 管理 API：`/_admin/api/**`，必须 Basic 认证。
- 业务 API：`/api/**`，由已发布 REST Trigger 决定认证策略。
- OPC UA 状态：`GET /_admin/api/opcua-server`。
- Run 历史：`GET /_admin/api/connections/{id}/runs`，详情与只读快照分别为 `/runs/{runId}` 和 `/runs/{runId}/snapshot`。
- 业务 OpenAPI：`GET /api/openapi.json`。

故障排查顺序：先检查 readiness 和容器日志，再确认 `/data` 权限/剩余空间、绑定地址/端口、证书有效期与信任库，最后检查 Connection 的发布版本、启用状态和节点轨迹。
