返回星港

在 RK3588 盒子上部署 Cloudreve 4.19.1:从静态二进制到内网可访问

OBSERVER又菜又爱玩的小朱 LAUNCH JUMPS4 次跃迁
在 RK3588 盒子上部署 Cloudreve 4.19.1:从静态二进制到内网可访问 · 封面

为什么要自己搭一个网盘

手上这台 ztl A588 是一台 RK3588 的 ARM 盒子,跑 Ubuntu 24.04,16GB 内存、没有 Swap。系统装在板载 eMMC 上,另外通过 USB 挂了一块 1.8T 的机械硬盘,日常文件主要靠 SMB 共享在局域网里流转。SMB 很好用,但它解决不了网页化管理的需求:在浏览器里看文件、生成分享链接、按用户分权限这些事都得靠应用层。所以决定在这台机器上再叠一层私有网盘。

选型落在 Cloudreve 社区版 4.19.1。原因很直接:官方提供 ARM64 静态二进制,不需要在 aarch64 上交叉编译;数据库可以直接复用机器上已有的 MySQL 容器,不必再起一套;本体用宿主机 systemd 服务管理,不额外吃一层容器开销。

目标也很明确——内网可用就行。最终只监听 192.168.10.62:5212,不做公网映射,也不做内网穿透。

环境与前提

项目实际情况
设备 / 系统dcztl(ztl A588),Ubuntu 24.04,RK3588 aarch64
软件版本Cloudreve 社区版 4.19.1,官方 ARM64 静态二进制
二进制校验包 SHA-256 f55d77e0e5eb8bc699a9a6bb1216b0ee3afa3bfe8d7c9d405ca9845339ae7b0e
数据库已有 1Panel 容器 1Panel-mysql-5qkq(mysql:8.4.11 ARM64),经宿主机 127.0.0.1:3306 连接
外接盘UUID 8b9cbec5-c4d7-466f-acea-d1c9531ccada,ext4,固定挂载 /media/ztl/LinuxDate
运行方式不使用 Docker,由 cloudreve.service 管理

有一个前提要先说清楚:这台机器没有 Swap,同时还跑着别的服务,所以任何常驻进程都不该无脑吃满八个核。下面的部署里专门做了 CPU 亲和性限制。

部署步骤

1. 程序本体与运行身份

程序放在 /opt/cloudreve/ 下,与许可证文件并列:

  • /opt/cloudreve/cloudreve —— 二进制本体
  • /opt/cloudreve/LICENSE —— 许可证
  • /opt/cloudreve/data/ —— 私密配置与运行数据

服务不用 root 跑,专门建了一个系统用户 cloudreve,shell 为 nologin。私密配置 /opt/cloudreve/data/conf.ini 权限 600、属主就是这个用户。这个文件里存着数据库口令,不要提交到 Git,也不要放进普通归档。

2. systemd 单元:小核绑定与句柄上限

单元文件位于 /etc/systemd/system/cloudreve.service,两个关键设置:

重启前后都核对过:全部 9 个线程都在 CPU 0–3 上。

下面把三份单元文件的完整内容贴出来,照着抄即可。

① 主单元 /etc/systemd/system/cloudreve.service

[Unit]
Description=Cloudreve file service
Documentation=https://docs.cloudreve.org/
Wants=network-online.target
After=network-online.target docker.service
Requires=docker.service
RequiresMountsFor=/media/ztl/LinuxDate
BindsTo=media-ztl-LinuxDate.mount
After=media-ztl-LinuxDate.mount
StartLimitIntervalSec=0

[Service]
Type=simple
User=cloudreve
Group=cloudreve
WorkingDirectory=/opt/cloudreve
EnvironmentFile=/etc/default/cloudreve-storage
ExecStartPre=/usr/local/libexec/cloudreve-storage-check
ExecStart=/opt/cloudreve/cloudreve server -c /opt/cloudreve/data/conf.ini
Restart=on-failure
RestartSec=15s
TimeoutStopSec=90s
KillMode=mixed
CPUAffinity=0 1 2 3
Environment=GOMAXPROCS=4
LimitNOFILE=65535
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/cloudreve/data /media/ztl/LinuxDate/cloudreve
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

② /etc/systemd/system/cloudreve.service.d/20-blob-storage.conf —— 只给 Blob 专用目录开写范围,是后来做文件实体迁移时加的补丁;只做部署这一轮的话可以先不放这个 drop-in。

[Service]
# 仅新增Blob专用目录可写,不放开整个files目录或原SMB共享。
ReadWritePaths=/media/ztl/LinuxDate/files/cloudreve
ExecStartPre=/usr/local/libexec/cloudreve-blob-check

③ /etc/systemd/system/cloudreve.service.d/40-usb-storage-guard-intent.conf —— 把人工停服的意图同步给 usb-storage-guard,避免手动 stop 之后被守护又拉起来。

[Service]
# 同步记录手动停服;只变更自身意图,不改业务用户/CPU/端口。
ReadWritePaths=/var/lib/usb-storage-guard
ExecStartPre=+/opt/usb-storage-guard/venv/bin/python /opt/usb-storage-guard/guard.py --config /etc/usb-storage-guard.json service-start %n
ExecStopPost=+/opt/usb-storage-guard/venv/bin/python /opt/usb-storage-guard/guard.py --config /etc/usb-storage-guard.json service-stop %n

drop-in 目录不存在时先建:install -d -m 0755 /etc/systemd/system/cloudreve.service.d。文件放好后执行 systemctl daemon-reload,再 systemctl enable --now cloudreve.service。

3. 复用已有的 MySQL 容器

不需要新装数据库。这里有两个很容易掉进去的细节:

  • 用户指定的库名是 Cloudreve,但这个 MySQL 实例实际把库名规范化成了小写 cloudreve;
  • 连接账号是 Cloudreve,不要把它和 账号@主机 这种完整标识混在一起整体填进用户名字段,用户名就是 Cloudreve。

验证过的结果是:库初始为空、认证成功、字符集 utf8mb4,应用通过宿主机端口连接后成功初始化了 17 张表。

另外,这个 MySQL 容器当前的 cpuset 是空的。要满足整机低功耗的目标,应该在 1Panel 管理的 Compose 配置入口里设置 cpuset: "0-3",重建后验证 mysqld 的全部线程;临时用 taskset 不算持久化方案。这次没有去动既有容器。

4. 上传目录放到外接盘,ACL 只开最小的门

大文件不该落在 eMMC 上,所以默认上传入口做成了指往外接盘的符号链接:

/opt/cloudreve/data/uploads -> /media/ztl/LinuxDate/cloudreve/uploads/

默认存储策略的路径模板是 /opt/cloudreve/data/uploads/{uid}/{path},经过符号链接后实际上就已经落到外接盘了,可以在后台确认策略配置。

麻烦在于外接盘的父目录权限默认不允许普通服务用户穿越。这里没有递归改权限,只给两级父目录加了一条命名 ACL,给 cloudreve 用户 --x(只可穿越、不可列举),修改前的 ACL 记录在 history/storage-acl-before.txt 里,方便回退。

服务通过 RequiresMountsFor 和 BindsTo 关联真实的 mount 单元;启动检查脚本 /usr/local/libexec/cloudreve-storage-check(配置在 /etc/default/cloudreve-storage)会核对 UUID、文件系统类型、写权限和符号链接。缺盘或 UUID 变化时服务直接拒绝启动,mount 单元停止时 Cloudreve 也会跟着停。

热插拔仍然有风险:盘被拔掉时正在进行的 I/O 可能失败,不能宣称“零丢失”。盘重新接好后由 usb-storage-guard 自动核对挂载并恢复服务。真实拔插和缺盘重启目前尚未实测。

5. 注册首个管理员

初始状态监听在回环地址,所以第一次注册要么走 SSH 隧道,要么先把监听切到内网。隧道方式在 Windows PowerShell 里执行:

ssh -i "$env:USERPROFILE\.ssh\id_ed25519_me-rk3588" -N -o ExitOnForwardFailure=yes -L 127.0.0.1:5212:127.0.0.1:5212 root@192.168.10.62

保持窗口不关闭,浏览器访问 http://127.0.0.1:5212 注册自己的管理员账号,然后进 /admin 确认用户组、注册策略和默认存储策略。自用的话建议顺手关掉开放注册。如果本地 5212 端口已被占用,把命令左侧端口改成 15212,改用 http://127.0.0.1:15212 访问,并同步配置站点 URL。

6. 从回环切到内网监听

注册完成后就可以让局域网直接访问了。改配置前先备份:

cp -a /opt/cloudreve/data/conf.ini /opt/cloudreve/data/conf.ini.bak.$(date +%Y%m%d-%H%M%S)
# 用编辑器把 Listen = 127.0.0.1:5212 改为 Listen = 192.168.10.62:5212
systemctl restart cloudreve.service
ss -lnt 'sport = :5212'

有几个细节要注意:不要写成 :5212 或 0.0.0.0:5212;改完还要在管理面板里把站点 URL 设成 http://192.168.10.62:5212,否则生成的下载链接可能还指向隧道地址,必要时清理 Blob URL 缓存。这份备份依然是私密配置,保持 600 权限,不能进普通归档。

内网 HTTP 没有浏览器到服务器的端到端 TLS。需要 HTTPS 得另行配置,但无论哪种情况都不要做公网映射或穿透。

配置文件示例:conf.ini.example

公开归档里放的是无口令的模板 /root/AI/cloudreve/conf.ini.example,实际部署时复制成 /opt/cloudreve/data/conf.ini,权限 600、属主 cloudreve。模板里的 Password 刻意留空,真实口令从密码管理器里填进去即可(可用 openssl rand -base64 24 生成一个独立口令,不要复用),口令本身不要写进文章、Git 或普通归档。

[System]
Mode = master
Listen = 127.0.0.1:5212
Debug = false
LogLevel = info
GracePeriod = 60

[Database]
Type = mysql
Host = 127.0.0.1
Port = 3306
User = Cloudreve
; 使用密码管理器中的实际口令,切勿提交私密配置。
Password =
Name = Cloudreve
Charset = utf8mb4

两点提醒:注册第一个管理员之前 Listen 保持 127.0.0.1:5212,按第 6 步切内网时再改成 192.168.10.62:5212;Name 与 User 的大小写要按实际库和账号填,这台机器上建库时库名最终被规范化成了小写 cloudreve,以现场实测为准。

验证结果

检查项结果
首页访问HTTP 200(当前电脑与服务器直连均通过)
监听范围仅 192.168.10.62:5212,无公网 / IPv6 监听
配置权限600,属主 cloudreve
线程亲和性9 个线程全部 CPU 0–3,重启前后一致
文件句柄65535
启动检查外盘真实 UUID 通过;错误 UUID 拒绝;不存在目录拒绝
写权限服务用户可在外盘目录创建/删除空测试文件
数据库utf8mb4,初始化 17 张表

日常运维命令:

systemctl status cloudreve.service --no-pager
systemctl restart cloudreve.service
systemctl stop cloudreve.service
journalctl -u cloudreve.service -n 60 --no-pager
/root/AI/cloudreve/tests/verify.sh

还有一条排错经验:Cloudreve 启动失败会自动 15 秒重试,而 Docker 的启动顺序并不代表 MySQL 已经就绪。数据库没就绪时先看容器日志,再看应用日志。另外别开 Debug 模式,详细的数据库日志可能带出敏感信息。

注意事项与踩坑

  • 数据库端口别用通配发布。 如果 MySQL 以 0.0.0.0 或 [::] 这类通配方式发布端口,那么“服务在内网”并不等于外部一定访问不到。建议在 1Panel 受支持的配置入口把端口绑定收敛为只监听 127.0.0.1(例如 127.0.0.1:3306:3306),改完用 docker inspect 和 ss 复核。
  • 数据库账号权限要收敛。 应用账号只应访问自己的库,并给到建表、改表所必需的最小权限即可。如果顺手授了额外的授权能力,建议通过 1Panel 权限管理按最小权限收敛,不要长期保留超出应用需要的授权。
  • 别把 Blob 存储目录指向已有的 SMB 共享。 已有文件必须按应用自己的机制导入,不能当成普通目录被自动索引。
  • 临时空间要留意。 数据库、运行日志和其他默认临时文件都保留在系统盘上,大文件上传或离线下载可能仍然占用临时空间。正式用大文件之前先观察 eMMC 占用并评估临时目录配置,不能只看上传目录。
  • 备份与归档是两回事。 普通归档只包含程序、源码、systemd 单元、无口令的配置模板、启动检查脚本和测试说明,不含用户库、uploads、缓存和实际的 conf.ini。真正的业务备份必须另做:暂停应用写入后用有相应权限的账号做一致性逻辑导出(视权限可能需要 --single-transaction --no-tablespaces,别把密码写在命令行上),加密备份 /opt/cloudreve/data(排除 uploads 符号链接的重复跟随),再备份外盘 Cloudreve 目录并复制到另一台电脑或 NAS。不要用复制运行中的 MySQL 数据目录来代替一致性备份。
  • 重装恢复顺序不能乱。 Docker/1Panel/MySQL → 固定 UUID 挂载 → 从加密业务备份恢复数据库/运行数据/uploads → 运行归档默认预演 → ./restore.sh --apply 恢复部署文件 → 手工补齐实际 conf.ini 口令并保证 600 与 cloudreve 属主 → 启用服务并做局部验证 → 注册/登录和上传下载回归。别让应用连上空库就以为业务已经恢复。
  • 升级前先备份。 新版本可能迁移 schema,只换二进制不一定能回退,必要时得同时恢复升级前的一致性快照。要停用本体就用 systemctl disable --now cloudreve.service,它不会停止 MySQL 或其他容器,也不删除数据库与上传文件。

这次部署的边界也说清楚:没有测试业务上传下载、分享和 WebDAV,没做整机重启,没拔过盘,没做数据库中断测试,也没测功耗。后续如果还要在这台机器上叠加功能,建议逐个功能单独验证,别一次全开。