又是 AI 代写,我只负责实操和踩坑,当个记录使用。
说在前面
我之前一直用 Koodo Reader 看电子书,网页版挺爽的,在 Windows 台式机和 Mac 上各开一个浏览器标签页,格式支持也非常完善(主要看 Z-Library 下的 EPUB 和网文 TXT)。
但有个致命痛点:它的"同步"根本不是真同步,是两端各存一份数据。
导书要在 Windows 和 Mac 上分别上传两遍,看完一本书想"归档",得在两边手动删两次。更要命的是进度同步 —— 虽然 Koodo 理论上能连 WebDAV / 网盘做备份,但必须手动点同步按钮,还是"合并后在本地和云端各保存一份"的逻辑。当然,现实中我连这个都没做过,只是每次记住进度,然后在另一端手动进行同步···
这不是我要的"即用即走"。我希望的是:
- 书只存一份(在 NAS),Windows 和 Mac 就是两个"显示器";
- 随时在任何设备打开,进度无缝同步,自动在那儿,不需要我想"有没有同步"这回事;
- 看完的书从书架上"消失",但物理文件安全归档在 NAS 上,以后想找还能找回来。
所以这次折腾的目标就是:搭一个真正的"云端原生"阅读器,所有状态只在 NAS 上,彻底干掉"两边存两份、手动同步、上传两次"的割裂体验。
本次折腾概览
- 最终选型:Kavita(纯服务端架构,Docker 部署在黑群晖 NAS 上)
- 抛弃方案:Koodo(假同步)、Calibre-Web(太重、为图书馆管理设计)、微信读书(web端不支持上传、隐私不可控)
- 核心工作流:Z-Library 下书 → 扔进 NAS 的
books_active/reading文件夹 → 自动扫描出现在书架 → 看完拖回books归档区 → 书架自动清空 - 外网访问:FRP 穿透 + Caddy HTTPS 反代(复用之前 Open WebUI 的架构)
- 踩的坑:Kavita 初始化被墙、时区 8 小时偏差、文件夹结构强迫症、扫描不实时
整体架构
最终的访问链路长这样:
手机 / Mac / 外网
↓ HTTPS :443
云服务器 Caddy
↓ 本机 127.0.0.1:13008
云服务器 frps
↓ 加密 FRP 隧道
家里群晖 frpc
↓ 本机 ip:5050
NAS 上的 Kavita 容器(Docker)
↓ 挂载
NAS 数据盘 /docker/books_active
数据只在 NAS 的硬盘里存一份,Windows 和 Mac 的浏览器就是两个"终端",进度、书签、高亮全实时共享。
为什么是 Kavita,不是 Koodo / Calibre
Koodo 被踢出局的原因
我翻了 Koodo 的官方文档,发现它的"同步"其实是这样的:
“合并后的数据在本地和云端各保存了一份。在设备 A 上导入新图书,然后点击软件顶部的同步按钮……然后在设备 B 上,点击同步按钮……”
关键词:“各保存一份” + “点击同步按钮”。
这就是彻底的单机软件思路 + 云盘备份。浏览器本地仍然会在 IndexedDB 里存一大份数据,NAS 或 WebDAV 只是个"中转云盘"。必须手动同步,这直接违背了我"合上电脑就走、拿起手机接着看"的核心诉求。
Calibre-Web:给图书馆管理员用的
Calibre 生态非常强大,格式转换、元数据刮削、封面管理一应俱全。但我的真实需求是:
“Z-Library 下完扔进去就看,看完多数不会读第二遍,最好读完就消失。”
我不需要维护一个精心分类、标签齐全、封面完美的图书馆。Calibre 那套"先导入、填作者、改标签、刮削封面"的流程,对我来说是纯粹的额外摩擦力。
Kavita:真正的纯服务端
Kavita 的核心逻辑和 Koodo 完全不同:
- 它是一个跑在 NAS 上的真正的服务器。
- 所有的书、阅读进度、用户状态,100% 只存在 NAS 上。
- 你的 Windows 和 Mac 浏览器只是一个"显示器",每一页都是在和 NAS 实时通讯。
- 完全不需要手动点同步。你在台式机看到第 50 页,直接关掉网页走人;拿起 Mac 登录同一个网址,点开那本书,它就在第 50 页。
格式支持是 Kavita 的弱项,只支持 EPUB、PDF 和漫画等。它不支持 TXT 和 MOBI,但既然我主力用 Z-Library(EPUB 资源管够),偶尔遇到 TXT 顺手用在线工具转一下也不算大事。
一、NAS 上准备文件夹
我的思路是把 NAS 当作"物理级的 GTD 系统":
/docker/books_active/reading:正在看的书(当前工作台,永远只放个位数的书)/books:旧书归档区(地下室书柜,看完的书全扔这里,想找还能找到)
这样的好处是:
- 书架永远干净(Kavita 只扫
books_active); - 看完的书不会真的物理删除,而是安全归档;
- 以后想重看,从归档区拖回
books_active即可,进度数据还在 Kavita 的数据库里。
在群晖的 File Station 里:
- 找到已有的
/docker文件夹(之前部署 Open WebUI / FRP 时建的)。 - 在
docker下新建文件夹kavita,再在kavita下建config。 - 在
docker下新建文件夹books_active,再在books_active下建子文件夹reading。
最终结构:
/docker
├─ kavita
│ └─ config (Kavita 的配置和数据库)
├─ books_active
│ └─ reading (当前在看的书)
└─ ...
/books (旧书归档区,不挂给 Kavita)
为什么要嵌套一个子文件夹? Kavita 有个底层防呆机制:它会自动忽略直接丢在"根目录"下的散装文件。如果你把书直接扔在
books_active里,Kavita 认为这是"根目录散装",会跳过不扫。必须再套一层子文件夹(名字随意),书才会被识别。
二、Windows 打包 Docker 镜像
我的 NAS 之前折腾 FRP 时就遇到过:国内网络拉 Docker Hub 镜像经常超时、EOF、甚至根本连不上。所以我一开始就不指望 NAS 能直接 docker pull,而是在 Windows 台式机上拉好、打包、传给 NAS。
在 Windows 的 PowerShell 里(确保 Docker Desktop 已运行):
# 拉取 Kavita 官方镜像
docker pull jvmilazz0/kavita:latest
# 打包成本地文件(会在当前目录生成 kavita.tar,大概几百 MB)
docker save -o kavita.tar jvmilazz0/kavita:latest
然后通过 SMB 把 kavita.tar 拖进 NAS 的任意文件夹(比如 /docker)。
清理 Windows 上的残留
传完之后,Windows 上会留下两个"污染":
- 本地的
kavita.tar文件 —— 直接删掉即可(Shift + Delete彻底删除)。 - Docker Desktop 里缓存的镜像 —— 打开 Docker Desktop 界面,点左侧 Images,找到
jvmilazz0/kavita,点右侧垃圾桶删除。
如果想一键清理所有未使用的 Docker 缓存:
docker system prune -f
三、在群晖导入镜像并启动容器
打开群晖的 Container Manager(DSM 7.2 之后把"Docker"改名成了这个),点击左侧的 “映像”。
点击上方 “新增” → “从文件导入”,选择刚才传进 NAS 的 kavita.tar,等进度条走完。
导入完成后,在"映像"列表里选中 jvmilazz0/kavita:latest,点 “运行”。
关键配置
1. 常规设置
- 容器名称随意(比如
kavita); - 勾选 “启用自动重新启动”(这样 NAS 重启后阅读器会自动拉起)。
2. 端口设置
- 本地端口(左边):填
5050(或其他不冲突的端口,别用 5000,那是群晖管理界面的默认端口); - 容器端口(右边):保持
5000不变。
3. 存储空间设置(最关键)
点击"添加文件夹"两次,分别配置:
| 文件夹(NAS 侧) | 装载路径(容器侧) |
|---|---|
/docker/kavita/config |
/kavita/config |
/docker/books_active |
/books |
4. 环境变量(时区修正)
点击 “环境” 标签页,添加一条:
- 变量名称:
TZ - 值:
Asia/Shanghai
(不设置的话,Kavita 会用 UTC 时间,扫描时间会显示"8 hours ago",看着别扭。)
点击"下一步"、“完成”,容器启动。
坑 1:初始化被墙,容器无限重启(闪黄灯)
刚启动时,我的容器状态一直闪黄灯(无限重启)。打开日志(双击容器 → “日志"标签页)看到:
[Error] Kavita.Server.Program Unable to download https://raw.githubusercontent.com/...
Flurl.Http.FlurlHttpTimeoutException: Call timed out: GET https://raw.githubusercontent.com/...
我的实际情况: 我还没来得及挂代理,它自己在重试了一段时间后突然变绿了——应该是某次重启时偶然碰上了 GitHub CDN 的某个能连通的节点。算是运气好。所以后面的原因和解法也是 AI 写的,我也不确定是否是合适的。
原因: Kavita 第一次运行时,会去 GitHub 下载几个默认的邮件模板文件。但 raw.githubusercontent.com 在国内被墙了,超时后容器判断启动失败,触发自动重启,然后再次超时……死循环。
解法(临时挂代理):
- 在 Windows 台式机上,打开代理软件(Clash / V2ray 等),开启 “允许局域网连接 (Allow LAN)”,记下端口号(通常是
7890或10808)。 - 回到群晖 Container Manager,停止 Kavita 容器。
- 选中容器,点"编辑”,找到 “环境” 标签页,添加两条环境变量:
HTTP_PROXY:http://你的WindowsIP:代理端口(如http://192.168.31.x:7890)HTTPS_PROXY:同上
- 保存,重新启动容器。这次有梯子,GitHub 文件几秒就拉下来了,黄灯变绿灯。
(可选)过河拆桥:
等容器变绿、网页能打开后,再次停止容器,把刚才的两个 PROXY 环境变量删掉,重新启动。以后它就是纯本地服务,再也不需要连外网了。
四、初始化 Kavita
在浏览器里输入 http://NAS局域网IP:5050,第一次进入会要求注册管理员账号。
注意:
- 邮箱随便填(只是登录账号名,不会真的发邮件,因为你没配 SMTP);
- 密码必须设强密码(字母大小写+数字+特殊符号),因为后面会通过 FRP 映射到公网,弱密码会被全网僵尸脚本爆破。
注册进去后,点击右上角齿轮图标进入 Dashboard(控制台),左侧菜单找到 Libraries(书库),点击 Add Library。
配置如下:
- Name:随便起(比如"当前阅读");
- Library Type:选 Book;
- 点 Next,在 Folders 这一步,点 Browse for Media Folders,选中
/books(就是我们刚才挂载的books_active文件夹); - Advanced(高级设置) 里,关键的几个开关保持默认即可:
- Enable Metadata:必须开启(这样 Kavita 会直接读 EPUB 内部的书名、作者、封面,不强迫你改文件名);
- Folder Watching:必须开启(文件夹监听,后面会详细说);
- File Types:只勾选 Epub 和 Pdf(过滤掉漫画压缩包)。
点击 Save。
五、验证工作流
第一次导书测试
- 在 Windows 台式机上,通过文件资源管理器访问 NAS 的 SMB 共享(地址类似
\\192.168.31.x\docker); - 找到
books_active/reading文件夹,把它固定到左侧侧边栏的"快速访问"里(这样以后 Z-Library 下载时可以直接保存到这里,不用经过本地 Downloads 文件夹); - 随便找一本 EPUB 扔进去;
- 回到 Kavita 网页,刷新一下,书应该会出现在首页书架上(可能需要等几秒到一分钟,或者要进到刚刚说的 Library 选项卡,手动 Scan。后面会讲扫描频率问题)。
跨设备同步测试
- 在 Windows 浏览器里打开这本书,随便翻到某一章;
- 关掉网页(或直接关机);
- 拿起 Mac(或手机),在浏览器里输入同一个地址
http://192.168.31.x:5050,登录; - 打开刚才那本书,进度应该自动恢复到你在 Windows 上看到的那一章。
如果这两步都通过,说明"真同步"已经实现了。
“阅后归档"测试
- 在 Windows 的文件管理器里,把刚才那本书从
books_active/reading剪切(或拖动) 到你的老/books归档文件夹里; - 回到 Kavita 网页,刷新一下,这本书应该从书架上消失了;
- 但文件还在 NAS 的
/books里,以后想重看,再拖回books_active/reading即可。
六、解决扫描不实时的问题
我在测试时发现:往 books_active/reading 扔进一本书后,Kavita 并不会立刻扫描出来。有时候等几分钟,有时候等十几分钟,甚至更久。
为什么"文件夹监听"像死了一样?
Kavita 的 Folder Watching 依赖 Linux 系统的 inotify(文件变动通知)机制。但我是通过 Windows 的 SMB 协议把书拖进 NAS 的。在跨网络的 SMB 传输中,群晖把文件存进硬盘后,这个 inotify 的信号无法穿透 Docker 容器的隔离墙传给 Kavita。
也就是说:Kavita 根本听不到"新文件进来了"的声音,它连那默认的 10 分钟的倒计时都没有开始。
解法:改成每分钟自动扫描
既然"触发式"在 SMB + Docker 的场景下基本废了,那就用"轮询式” —— 让 Kavita 每分钟主动去问一次"有没有新书?"
进入 Kavita 后台 Dashboard → Server → Tasks → 找到 Scan Library,把 Cron 表达式改成:
* * * * *
(五个星号表示"每分钟")
保存。
关于性能和硬盘寿命的担忧
我一开始很担心"每分钟扫一次"会不会:
- 让 NAS 的硬盘(18TB 希捷酷狼 Pro)无法休眠,耗电增加;
- 频繁扫描导致硬盘寿命缩短;
- CPU 一直处于忙碌状态。
后来至少 AI 给我的结论是:完全不用担心。
原因在于 Linux 的底层机制(群晖的系统就是 Linux):
- 我的 NAS 有 16GB 内存,Linux 会自动把硬盘的"文件夹目录结构"全部缓存到内存里;
- Kavita 每分钟去问"有没有新书?“时,它实际上是在跟内存对话,内存发现没改动,直接返回"没有”,这整个过程不到 0.01 秒,根本不会去唤醒物理硬盘;
- 只有当你真的在 Windows 上丢了一本新书进去时,硬盘才会被唤醒写入,Kavita 下一分钟扫描时才会真正调用 CPU 去解析那本书。
而且,我的 books_active 文件夹将常年只放个位数的书(“用完就删"的工作台),扫描这个几乎为空的文件夹,代价无限趋近于零。
最终体验:
我在 Windows 上把 Z-Library 下好的书丢进 books_active/reading → 拿起 Mac 走到阳台(或掏出手机) → 打开 Kavita。这段物理走路的时间,基本就能覆盖掉那最多 60 秒(平均 30 秒)的盲区。网页一开,书已经在那里了。
七、打通外网访问(FRP + Caddy)
局域网闭环跑通后,最后一步就是让我在外出时也能访问。
我复用了之前部署好的 FRP 架构:云服务器当中继,家里 NAS 跑 frpc 客户端。
1. 修改 NAS 的 FRP 配置
我的 NAS 上之前已经有一个 frpc-nas 容器(用于唤醒台式机的 WOL 面板)。打开群晖 File Station,编辑 /docker/wol/frpc.toml,在最末尾追加:
[[proxies]]
name = "nas-kavita"
type = "tcp"
localIP = "127.0.0.1"
localPort = 5050
remotePort = 13008
注意:
localPort = 5050是 Kavita 在 NAS 上的端口;remotePort = 13008是云服务器上分配给 Kavita 的隐藏端口,必须在 frps 的allowPorts白名单里(我之前配的是13000-13010,所以13008没问题)。
保存后,去 Container Manager 里把 frpc-nas 容器 重新启动 (Restart)。
2. 在 Cloudflare 加一条 DNS 记录
去 Cloudflare,新增一条 A 记录:
- 名称:比如
reader(这样以后的网址就是reader.你的域名); - IPv4 地址:你的云服务器公网 IP;
- 代理状态:DNS Only(灰云)。
为什么用灰云? 因为 Kavita 的阅读体验依赖流式响应和 WebSocket,走 Cloudflare 橙云代理会多一层变量(超时、SSL 模式、流式兼容),没必要。Caddy 可以直接向 Let’s Encrypt 自动签 HTTPS 证书,不需要 Cloudflare 代理。
3. 修改云服务器的 Caddy 配置
SSH 连上云服务器,编辑 Caddyfile:
sudo nano /etc/caddy/Caddyfile
在文件最末尾加上(跟之前的 local-域名A 平级):
reader.你的域名.dev {
reverse_proxy 127.0.0.1:13008
}
保存退出,重载 Caddy:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
4. 验证外网访问
拿起手机,关掉 Wi-Fi,用 5G 移动网络,在浏览器里输入 https://reader.你的域名.dev:
- 能看到绿色的安全锁吗?
- 能正常登录进去吗?
- 打开一本书,翻几页,进度能保存吗?
如果都没问题,说明外网穿透彻底成功。
5. 安全加固(可选但推荐)
确保云服务器的安全组里,13008 端口没有对公网放行。
因为 Caddy 和 frps 在同一台机器,Caddy 走 127.0.0.1:13008 不经过安全组,外面就访问不到裸的 IP:13008 了。最终对外只留 80、443、7000(FRP 控制端口)。
八、Kavita 网页端的一个小遗憾
我发现 Kavita 的网页端没有"删除书籍"的按钮。
这其实不是 bug,而是设计理念:Kavita 把自己定位为"纯粹的阅读显示器”,而不是"图书文件管理器"。它默认不提供在前端直接把底层物理文件删掉的便捷操作。
但这反而完美契合了我的"阅后归档"工作流:
- 如果网页端能直接删除,你点一下,这本 EPUB 就会从 NAS 硬盘里彻底灰飞烟灭,想找回来都难;
- 现在的逻辑是:你在 Windows 的文件管理器里,把书从
books_active/reading拖回老books归档文件夹,书在一分钟内从 Kavita 书架上"消失",但物理文件安全归档在 NAS 上。
所以,把 Windows 的文件资源管理器当作你唯一且绝对安全的控制台。看完了不需要在阅读器里学第二套管理逻辑,直接拖文件就行。
最终的工作流
- Z-Library 下书,浏览器保存路径直接选
books_active/reading(已固定在快速访问里); - 最多等一分钟,书自动出现在 Kavita 书架上;
- 在任何设备(Windows / Mac / 手机)打开 Kavita,进度自动同步;
- 看完后,在 Windows 文件管理器里把书从
books_active/reading拖回/books归档区; - 书架自动清空,但文件还在 NAS 上,以后想重看还能找到。
零管理成本、零手动同步、零数据冗余。
我是用新的阅读器读的很爽,随地大小读的 Wenix,希望你开心~