Files
Havline/docs/fnos-app-packaging-plan.md
T
2026-10-01 18:58:28 +08:00

1079 lines
25 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` 数据迁移。
---
## 十九、官方文档复核与实施修正(2026-10-01)
本轮已逐页复核飞牛应用开放平台文档中的以下内容:
- 快速开始:准备工作、创建应用、测试应用、上架应用
- 核心概念:`manifest`、权限、资源、运行时、环境变量、应用入口、统一网关、用户向导
- 开发工具:`fnpack`、`appcenter-cli`
- Docker 应用案例:`fnpack create --template docker`、Compose、资源声明、状态检查、桌面入口
### 19.1 已确认正确的方案
现有方案的大方向与官方 Docker 应用案例一致:
1. 使用 Docker 应用模板,而不是首发做 CGI 或原生应用。
2. 包结构使用 `manifest`、`config/privilege`、`config/resource`、`app/docker/docker-compose.yaml`、`app/ui/config`、`cmd/main` 和 `wizard/`。
3. `config/resource` 声明 Docker 项目资源的格式正确:
```json
{
"docker-project": {
"projects": [
{
"name": "havline",
"path": "docker"
}
]
}
}
```
4. `cmd/main` 的 `start` 和 `stop` 可以空操作,Docker 项目启停由飞牛应用中心处理;`status` 检查容器状态,运行中返回 `0`,未运行返回 `3`。
5. 首发关闭 Havline 自带 updater,避免 Docker Socket、宿主机路径映射和飞牛应用中心升级冲突。
6. 默认使用 `run-as=package`,不使用 Root,不挂载 Docker Socket。
7. 使用 `TRIM_SERVICE_PORT`、`TRIM_APPDEST`、`TRIM_PKGVAR` 等变量,而不是硬编码安装路径。
### 19.2 需要修正或补充的实现细节
1. **以 `fnpack` 模板为起点**
官方 Docker 案例推荐先执行:
```bash
fnpack create havline --template docker
cd havline
```
因此 `fnos/` 不应完全手写。正确做法是:
- 先用模板生成标准骨架;
- 再将 Havline 的 Compose、入口和生命周期脚本替换进去;
- 让构建脚本只维护差异部分,避免未来官方模板升级时难以同步。
2. **`cmd/main` 的状态检查必须与 Compose 一致**
Compose 中有 `container_name: havline` 时,`cmd/main` 应检查该名称:
```bash
CONTAINER_NAME="havline"
```
如果以后移除 `container_name`,`cmd/main` 需要按 Compose 实际生成的容器名检查,否则应用中心会一直显示未运行。
3. **向导密钥字段必须使用 `password` 类型**
官方向导支持 `password` 类型,不能把管理员初始密码或 Token 用普通 `text` 收集:
```json
{
"type": "password",
"field": "wizard_initial_admin_password",
"label": "初始管理员密码",
"rules": [
{ "required": true, "message": "请输入初始管理员密码" }
]
}
```
向导值会变成同名环境变量,生命周期脚本使用前必须再次校验,并禁止写入日志。
4. **自定义向导字段统一使用 `wizard_` 前缀**
官方建议:
- 自定义字段使用 `wizard_` 前缀;
- 字段名一旦发布应保持稳定;
- 不要使用 `TRIM_` 前缀,因为该前缀保留给系统变量。
Havline 建议字段:
```text
wizard_service_port
wizard_nginx_http_port
wizard_nginx_https_port
wizard_timezone
wizard_initial_admin_password
```
5. **生命周期脚本错误必须写入 `TRIM_TEMP_LOGFILE`**
官方要求生命周期脚本以非零状态退出前,将用户可见错误写入:
```text
TRIM_TEMP_LOGFILE
```
安装、升级、配置和卸载脚本都应遵循该约定,不能只输出到 stdout。
6. **`app/ui/config` 的 `port` 必须三处一致**
以下三个值必须保持一致:
```text
manifest.service_port
app/ui/config 的 port
docker-compose.yaml 中 ${TRIM_SERVICE_PORT}:6893
```
否则应用卡片可能打开失败,或应用中心显示运行但入口不可访问。
7. **Docker Compose 支持官方路径变量**
官方明确指出 Docker Compose 可以使用:
```text
TRIM_SERVICE_PORT
TRIM_APPDEST
TRIM_PKGVAR
TRIM_DATA_SHARE_PATHS
```
Havline 推荐继续使用:
```yaml
volumes:
- "${TRIM_PKGVAR}:/data"
```
不要额外拼出无意义的 `${TRIM_PKGVAR}/data`,除非确实要在数据根下再做子目录。
8. **共享目录只暴露明确需要的路径**
`config/resource` 的 `data-share` 会让用户在文件管理器中看到路径。首发只能暴露备份目录,绝不能暴露:
```text
session_secret
havline.db
certs/
nginx/
frp/
cloudflared/
```
9. **镜像必须真正支持 amd64 与 arm64**
`platform=all` 只代表应用包不含架构相关二进制,容器镜像本身仍必须支持目标架构。CI 必须构建并推送:
```text
linux/amd64
linux/arm64
```
否则 x86 测试通过但 ARM 设备仍会启动失败。
10. **版本和升级说明由 fnOS 管理**
首发使用飞牛应用中心升级。`manifest.changelog` 应面向用户简洁表达本次变更;
Havline 内部升级页面、`havline-updater` sidecar 和 Docker Socket 默认不进入 fnOS 包。
11. **端口入口和容器端口必须区分**
`app/ui/config` 的 `port` 支持使用向导变量,因此管理入口可以写成:
```json
"port": "${wizard_service_port}"
```
Compose 中应把宿主机端口映射到 Havline 容器内的固定端口:
```yaml
ports:
- "${wizard_service_port}:6893"
- "${wizard_nginx_http_port}:80"
- "${wizard_nginx_https_port}:443"
```
不要把 `18080` 或 `9443` 当成容器内部监听端口。它们是宿主机端口,容器内仍然是 `80/443`。
### 19.3 建议的首发实施顺序
阶段 1 只做“能安装、能打开、能保留数据”:
1. 用 `fnpack create havline --template docker` 生成标准骨架。
2. 改写 `manifest`,确定 `appname=havline`、`service_port` 和桌面入口。
3. 编写 Havline 专用 `app/docker/docker-compose.yaml`。
4. 编写 `cmd/main` 容器状态检查。
5. 编写 `wizard/install`,只收集端口、时区和初始管理员密码。
6. 编写 `cmd/install_callback`,创建数据目录、修正权限并完成首次初始化。
7. 构建 amd64 / arm64 镜像。
8. 用 `fnpack build` 打包并在飞牛测试设备安装验证。
当前打包工程位于主仓库同级的飞牛工作目录中,不进入主仓库:
```text
F:\Fonu-main\fnos\
├── fnpack.exe
├── build.ps1
├── README.md
└── havline/
```
主程序仍位于 `F:\Fonu-main\havline`,打包工程通过主仓库的 `VERSION` 和
`fnos/build.ps1` 保持版本同步。
阶段 1 暂不实现:
- Havline 自带 updater;
- Docker Socket;
- 统一网关 Unix Socket;
- 高级 host 网络模式;
- 自动迁移旧 Docker Compose 数据。
### 19.4 首发验收清单
```text
[ ] fnpack 模板生成的项目结构与官方一致
[ ] manifest 必填字段完整
[ ] service_port、入口 port、Compose 映射三处一致
[ ] cmd/main status 与 Compose 容器名一致
[ ] wizard 使用 password 类型收集初始密码
[ ] 安装回调创建并修正 TRIM_PKGVAR 权限
[ ] 数据目录持久化,升级后 session_secret 不变
[ ] 默认不挂载 Docker Socket
[ ] 默认关闭 Havline updater
[ ] amd64 与 arm64 镜像均可拉取并启动
[ ] 卸载默认保留数据,明确选择后才删除
[ ] TRIM_TEMP_LOGFILE 能输出可读错误
```
### 19.5 结论
现有方案不需要推倒重来,主要修正点是把手工设计的包结构收敛到官方
`fnpack create --template docker` 模板,并补齐向导密码、状态检查、
路径变量、生命周期错误输出和双架构镜像验证。