Files
Havline/docs/fnos-app-packaging-plan.md
T
2026-10-01 16:54:35 +08:00

833 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Havline 飞牛 fnOS 应用打包方案
> 日期:2026-10-01
> 依据:飞牛应用开放平台文档:<https://developer.fnnas.com/docs/guide/>
> 目标:将 Havline 从 Docker Compose / 裸机部署,改造为可安装、可升级、可卸载的飞牛 fnOS `.fpk` 应用。
---
## 一、官方文档结论
飞牛 fnOS 应用包的核心组成:
```text
manifest
config/privilege
config/resource
app/
cmd/
wizard/
ICON.PNG
ICON_256.PNG
```
官方提供三种常见形态:
1. **CGI 应用**
- 适合静态页面和轻量入口。
- 不支持 WebSocket,不适合 Havline 这种常驻服务。
2. **统一网关应用**
- 复用 fnOS 访问域名和登录态。
- 服务监听 Unix Socket,由 fnOS 网关转发。
- 支持 WebSocket,但 Havline 需要新增 Unix Socket 监听能力。
3. **Docker 应用**
- 使用 `app/docker/docker-compose.yaml`。
- 在 `config/resource` 中声明 `docker-project`。
- 最适合 Havline 当前形态。
**首选方案:飞牛 Docker 应用模板。**
原因:
- Havline 已经是完整 Docker 应用。
- 镜像内已包含 Nginx、Go 主程序和数据库运行环境。
- Cloudflare Tunnel、FRP、Agent、证书管理都依赖常驻进程。
- fnOS Docker 项目资源可以托管启动、停止和运行状态。
- 避免把 Havline 的 Go、Nginx、SQLite 和 shell 依赖重新拆进原生应用生命周期。
---
## 二、当前 Havline 的部署形态
### 2.1 当前主要组成
```text
havline
├── Go 主程序
├── 内嵌 Web 前端
├── 内置 Nginx
├── SQLite
├── FRP 客户端 / Agent 管理
├── Cloudflare Tunnel / cloudflared
├── ACME / DDNS / 通知
└── 可选 havline-updater sidecar
```
### 2.2 当前关键目录
```text
/data
├── havline.db
├── session_secret
├── nginx/
├── certs/
├── logs/
├── frp/
└── cloudflared/
```
### 2.3 当前端口
| 端口 | 用途 |
| --- | --- |
| `6893` | Havline 管理界面/API |
| `80` | 本机 Nginx HTTP |
| `443` | 本机 Nginx HTTPS |
飞牛 fnOS 通常已占用 `80/443`,因此 fnOS 包应默认使用:
```text
管理端口:6893
Nginx HTTP:18080
Nginx HTTPS:9443
```
端口应允许安装向导修改。
### 2.4 当前 updater 的限制
Havline 当前的一键升级依赖:
- Docker Socket
- updater sidecar
- 宿主机 Compose 项目目录
- 对宿主 Docker 执行 `compose build/up` 的权限
在 fnOS 应用包内继续保留这条链路会带来:
- 容器内 path mapping 复杂;
- fnOS 应用目录路径和宿主路径不一致;
- 应用中心升级与 Havline 内部升级冲突;
- 需要给应用额外 Docker Socket 权限。
**方案:fnOS 首发版本默认关闭 Havline 内置 updater,统一由飞牛应用中心升级。**
后续如果需要,可做“高级模式”单独启用。
---
## 三、目标包结构
建议在仓库新增:
```text
fnos/
├── manifest
├── ICON.PNG
├── ICON_256.PNG
├── config/
│ ├── privilege
│ └── resource
├── app/
│ ├── docker/
│ │ └── docker-compose.yaml
│ └── ui/
│ ├── config
│ └── images/
│ ├── icon_64.png
│ └── icon_256.png
├── cmd/
│ ├── main
│ ├── install_init
│ ├── install_callback
│ ├── upgrade_init
│ ├── upgrade_callback
│ ├── uninstall_init
│ ├── uninstall_callback
│ ├── config_init
│ └── config_callback
└── wizard/
├── install
├── upgrade
├── config
└── uninstall
```
构建脚本负责:
1. 复制 `fnos/` 模板到临时目录。
2. 用 `VERSION` 替换 `manifest.version`。
3. 生成对应的 `docker-compose.yaml`。
4. 调用 `fnpack build`。
5. 输出 `havline-<version>.fpk`。
---
## 四、Manifest 草案
```ini
appname=havline
version=1.9.0
display_name=Havline
desc=NAS 公网访问管理:反向代理、FRP、Cloudflare Tunnel、DDNS 与证书
source=thirdparty
platform=all
maintainer=Havline
maintainer_url=https://example.com
os_min_version=1.0.0
desktop_uidir=ui
desktop_applaunchname=havline.main
service_port=6893
checkport=true
ctl_stop=true
changelog=本次更新
```
字段说明:
- `appname=havline`:应用唯一标识。
- `platform=all`:包内不含架构二进制,实际架构由 multi-arch 镜像决定。
- `service_port=6893`:桌面入口默认访问端口。
- `ctl_stop=true`:应用中心显示启动、停止和运行状态。
- `desktop_applaunchname=havline.main`:应用卡片默认入口。
### 多架构要求
发布时必须提供:
```text
linux/amd64
linux/arm64
```
否则 `platform=all` 只在镜像本身支持目标架构时成立。
---
## 五、权限与资源
### 5.1 `config/privilege`
```json
{
"defaults": {
"run-as": "package"
},
"username": "havline",
"groupname": "havline"
}
```
原则:
- 长期运行的主程序使用包用户。
- 不默认给 Root。
- 不默认挂载 Docker Socket。
- 不默认加入 `docker` 用户组。
如果未来需要启用内部 updater,应单独评估 Docker Socket 的权限边界。
### 5.2 `config/resource`
```json
{
"docker-project": {
"projects": [
{
"name": "havline",
"path": "docker"
}
]
}
}
```
可选共享目录:
```json
{
"data-share": {
"shares": [
{
"name": "havline/backups"
}
]
}
}
```
建议首发版本只暴露备份目录。
不要暴露:
- `havline.db`
- `session_secret`
- `certs/`
- `nginx/`
- `cloudflared/`
- `frp/`
这些内容包含访问控制、证书和加密凭据。
---
## 六、Docker Compose 草案
建议新建:
```text
fnos/app/docker/docker-compose.yaml
```
内容:
```yaml
name: havline
services:
havline:
image: ghcr.io/example/havline:${TRIM_APPVER}
container_name: havline
restart: unless-stopped
ports:
- "${TRIM_SERVICE_PORT}:6893"
- "${wizard_nginx_http_port}:18080"
- "${wizard_nginx_https_port}:9443"
volumes:
- "${TRIM_PKGVAR}/data:/data"
environment:
HAVLINE_DATA_DIR: /data
HAVLINE_LISTEN: ":6893"
HAVLINE_NGINX_HTTP_PORT: "${wizard_nginx_http_port}"
HAVLINE_NGINX_HTTPS_PORT: "${wizard_nginx_https_port}"
HAVLINE_SESSION_SECRET: ""
TZ: "${wizard_timezone}"
HAVLINE_UPDATER_SOCKET: ""
```
说明:
- `TRIM_SERVICE_PORT` 默认映射管理界面。
- 使用 bridge 网络,避免与 fnOS 现有 host 网络服务直接冲突。
- Nginx 使用 18080/9443,避免占用 fnOS 的 80/443。
- 数据全部落在 `TRIM_PKGVAR/data`。
- 不挂载 Docker Socket。
- 不启动 updater sidecar。
### 为什么首发不直接用 host 网络
host 网络确实更贴近 Havline 裸机模式,但:
- fnOS 本身和其他应用可能占用大量端口;
- 应用中心无法明确管理端口冲突;
- bridge + 端口映射更容易解释和回滚。
首发建议 bridge。
后续可提供“高级安装模式:host 网络”。
---
## 七、桌面入口
`app/ui/config`:
```json
{
".url": {
"havline.main": {
"title": "Havline",
"icon": "images/icon_{0}.png",
"type": "iframe",
"protocol": "http",
"port": "${TRIM_SERVICE_PORT}",
"url": "/",
"allUsers": false,
"control": {
"accessPerm": "readonly"
}
}
}
}
```
考虑:
- Havline 当前是单管理员产品,默认只让管理员可见。
- 使用端口服务,不依赖 fnOS 网关。
- 后续若新增统一网关,可把入口改成 `gatewayPrefix/gatewaySocket`,无需端口。
---
## 八、生命周期脚本
### `cmd/main`
Docker 项目的启停由 fnOS Docker 资源处理,`main` 主要提供状态检查。
```bash
#!/bin/bash
CONTAINER_NAME="havline"
is_running() {
docker inspect "$CONTAINER_NAME" 2>/dev/null | grep -q '"Status": "running"'
}
case "$1" in
start|stop)
exit 0
;;
status)
if is_running; then
exit 0
fi
exit 3
;;
*)
echo "Unknown command: $1" > "$TRIM_TEMP_LOGFILE"
exit 1
;;
esac
```
### `cmd/install_callback`
职责:
- 创建 `TRIM_PKGVAR/data`
- 修正目录权限
- 如果用户选择了迁移,执行数据复制
- 输出安装摘要
### `cmd/upgrade_callback`
职责:
- 检查数据库可读写
- 保留已有的 `session_secret`
- 记录升级前后版本
- 可选:导入旧版本迁移文件
### `cmd/config_callback`
职责:
- 配置端口或时区变化时,验证新端口
- 如果只是环境变量变化,尽量交给 Docker Compose 重建
- 输出清晰的失败原因到 `TRIM_TEMP_LOGFILE`
### `cmd/uninstall_callback`
默认行为:
- 删除容器和程序
- 保留应用数据
只有用户在卸载向导中明确选择“删除全部数据”时,才删除:
```text
${TRIM_PKGVAR}/data
```
---
## 九、安装向导
### `wizard/install`
建议字段:
```json
[
{
"stepTitle": "Havline 访问端口",
"items": [
{
"type": "text",
"field": "wizard_nginx_http_port",
"label": "Nginx HTTP 端口",
"initValue": "18080",
"rules": [
{
"required": true,
"message": "请输入端口"
},
{
"pattern": "^[0-9]+$",
"message": "端口必须是数字"
}
]
},
{
"type": "text",
"field": "wizard_nginx_https_port",
"label": "Nginx HTTPS 端口",
"initValue": "9443",
"rules": [
{
"required": true,
"message": "请输入端口"
},
{
"pattern": "^[0-9]+$",
"message": "端口必须是数字"
}
]
}
]
},
{
"stepTitle": "基础设置",
"items": [
{
"type": "select",
"field": "wizard_timezone",
"label": "时区",
"initValue": "Asia/Shanghai",
"options": [
{
"label": "Asia/Shanghai",
"value": "Asia/Shanghai"
},
{
"label": "UTC",
"value": "UTC"
}
]
}
]
}
]
```
### 管理员初始化
当前 Havline 首次启动会自动生成随机管理员密码并写入日志。
对于 fnOS 应用,建议改造为:
1. 安装向导收集初始管理员密码;或
2. 首次打开 Havline 时进入创建管理员页面;或
3. 读取 `HAVLINE_INITIAL_ADMIN_PASSWORD` 进行一次性初始化。
**不建议继续把管理员密码写入日志。**
---
## 十、环境变量映射
| fnOS 变量 | 映射到 Havline | 说明 |
| --- | --- | --- |
| `TRIM_APPVER` | 镜像 tag | 应用版本 |
| `TRIM_SERVICE_PORT` | 6893 映射 | 桌面入口 |
| `TRIM_PKGVAR` | `/data` | 持久化数据 |
| `TRIM_APPDEST` | 应用 target | 只读程序目录 |
| `wizard_nginx_http_port` | `HAVLINE_NGINX_HTTP_PORT` | Nginx HTTP |
| `wizard_nginx_https_port` | `HAVLINE_NGINX_HTTPS_PORT` | Nginx HTTPS |
| `wizard_timezone` | `TZ` | 时区 |
Havline 数据目录内应继续保留:
```text
${TRIM_PKGVAR}/data/session_secret
```
不要每次安装或升级重新生成。
---
## 十一、更新策略
### 首发方案
统一使用飞牛应用中心升级:
- 更新 `.fpk`
- 拉取新 multi-arch 镜像
- 保留 `${TRIM_PKGVAR}/data`
- 重启 Docker Compose 项目
### 禁用 Havline 自带 updater
fnOS 包内:
```text
HAVLINE_UPDATER_SOCKET=""
```
并且 compose 不启动:
```text
havline-updater
```
原因:
- App Center 已经是唯一更新入口。
- 避免 Docker Socket 权限。
- 避免应用目录映射错误。
- 避免升级 Recursive / 自更新冲突。
### 后续可选
如果确实需要应用内更新,再增加“高级更新模式”:
- 显式让用户开启;
- 显式挂载 Docker Socket;
- 显式配置宿主项目路径;
- 失败时回滚到上一版镜像。
---
## 十二、数据迁移方案
### 从 Docker Compose 迁移
旧环境可能有:
```text
/opt/havline/data
```
或 Docker 命名卷:
```text
havline-data
```
迁移步骤:
1. 停止旧 Havline。
2. 备份旧数据目录。
3. 将旧目录复制到:
```text
${TRIM_PKGVAR}/data
```
4. 确认 `session_secret` 和 `havline.db` 都存在。
5. 启动 fnOS 应用。
6. 登录并验证:
- 用户与设置
- 反向代理规则
- FRP 服务端
- Cloudflare 隧道
- 证书
- 备份列表
### 从裸机迁移
裸机默认数据目录通常是:
```text
./.data
```
迁移内容一致,重点是保持 `session_secret` 不变。
否则:
- 已加密的 API Token
- DDNS 凭据
- 通知凭据
- Cloudflare 凭据
可能无法解密。
---
## 十三、安全设计
### 默认安全基线
- `run-as=package`
- 不启用 Root
- 不挂载 Docker Socket
- 不暴露证书、数据库和加密密钥目录
- 数据目录仅应用用户可读写
- 仅暴露管理端口、Nginx 高位的 HTTP/HTTPS 端口
- 管理员入口默认只对 fnOS 管理员可见
### 首次管理员密码
必须改造当前“随机密码写日志”行为。
推荐优先级:
1. 首次进入创建管理员页面
2. 安装向导收集密码
3. 环境变量一次性初始化
### Cloudflare Token
仍由 Havline 独立加密保存:
```text
${TRIM_PKGVAR}/data/session_secret
```
不放入 fnOS 共享目录。
---
## 十四、构建与发布流程
### 1. 构建 multi-arch 镜像
```bash
docker buildx build \
--platform linux/amd64,linux/arm64 \
--build-arg VERSION_FILE=VERSION \
-t registry.example.com/havline:${VERSION} \
--push .
```
### 2. 生成 fnOS 项目
从以下模板生成:
```text
fnos/
```
替换:
- `manifest.version`
- Compose 镜像 tag
- 应用说明
- `changelog`
### 3. 打包
```bash
fnpack build --directory dist/fnos/havline
```
### 4. 测试设备安装
```bash
appcenter-cli install-fpk havline-${VERSION}.fpk --env config.env
```
### 5. 发布前验证
在至少一台 x86 和一台 ARM 测试设备验证:
- 安装
- 启动
- 桌面入口
- 端口映射
- 数据持久化
- 应用中心升级
- 卸载保留数据
- 卸载删除数据
---
## 十五、分阶段实施
### 阶段 1:可用包
- 新增 `fnos/` 目录
- 编写 manifest
- Docker Compose 模板
- 安装/卸载/状态生命周期
- 管理入口
- 端口向导
- 使用固定镜像 tag
目标:可以在飞牛测试设备安装并打开 Havline。
### 阶段 2:数据与升级
- `TRIM_PKGVAR` 持久化
- 旧 Docker Compose 数据迁移脚本
- 应用中心升级验证
- 保留/删除数据选项
- `session_secret` 兼容检查
目标:数据不丢,可稳定升级。
### 阶段 3:fnOS 体验
- 首次管理员初始化页面
- 端口冲突检测
- 应用入口图标完善
- 备份目录共享
- 时区和系统信息适配
目标:符合飞牛应用中心体验。
### 阶段 4:高级集成
- 可选统一网关
- 可选 Docker Socket updater
- fnOS 开放 API 集成
- 应用中心审核材料
- ARM 实机完整验证
---
## 十六、风险与处理
| 风险 | 影响 | 处理 |
| --- | --- | --- |
| `80/443` 被 fnOS 占用 | Havline Nginx 无法启动 | 默认改用 18080/9443,向导可改 |
| 镜像只有 amd64 | ARM 设备无法运行 | CI 构建 amd64 + arm64 |
| fnOS 应用升级与内置 updater 冲突 | 版本回退或容器失控 | 首发禁用内置 updater |
| `session_secret` 丢失 | 加密凭据无法解密 | 持久化到 `TRIM_PKGVAR`,迁移时必须保留 |
| Docker Socket 权限过大 | 安全风险 | 默认不挂载 |
| 数据目录权限错误 | 数据库无法写入 | `install_callback` 修正权限 |
| 管理员密码写日志 | 凭据泄露 | 安装向导或首次设置页 |
| 端口冲突 | 应用无法启动 | 向导配置,安装时检测 |
| ARM 镜像无 cloudflared | Cloudflare 隧道不可用 | 镜像内校验架构,或运行时下载对应 binary |
---
## 十七、官方文档参考
- 欢迎加入:<https://developer.fnnas.com/docs/guide/>
- 快速开始:
- <https://developer.fnnas.com/docs/quick-started/prerequisites/>
- <https://developer.fnnas.com/docs/quick-started/create-application/>
- <https://developer.fnnas.com/docs/quick-started/test-application/>
- <https://developer.fnnas.com/docs/quick-started/publish-application/>
- 核心概念:
- <https://developer.fnnas.com/docs/core-concepts/manifest/>
- <https://developer.fnnas.com/docs/core-concepts/privilege/>
- <https://developer.fnnas.com/docs/core-concepts/resource/>
- <https://developer.fnnas.com/docs/core-concepts/runtime/>
- <https://developer.fnnas.com/docs/core-concepts/environment-variables/>
- <https://developer.fnnas.com/docs/core-concepts/app-entry/>
- <https://developer.fnnas.com/docs/core-concepts/gateway-registration/>
- <https://developer.fnnas.com/docs/core-concepts/wizard/>
- 工具:
- <https://developer.fnnas.com/docs/cli/fnpack/>
- <https://developer.fnnas.com/docs/cli/appcentercli/>
- Docker 案例:
- <https://developer.fnnas.com/docs/examples/docker/>
---
## 十八、结论
Havline 适合做飞牛 Docker 应用,不适合首发做成 CGI 或原生应用。
首发目标:
```text
飞牛应用中心安装
→ Docker Compose 启动 Havline
→ 桌面入口打开管理界面
→ 数据持久化到 TRIM_PKGVAR
→ 应用中心升级
```
最需要先解决的代码问题:
1. 管理员初始化不再写日志。
2. 默认 updater 改为可关闭,fnOS 包中关闭。
3. 增加 multi-arch 镜像发布。
4. 增加 `fnos/` 打包模板和构建脚本。
5. 验证 `TRIM_PKGVAR/data` 数据迁移。