部署 Kavita 自托管阅读器 —— 从 Koodo 的"假同步"到真正的跨设备无感阅读

又是 AI 代写,我只负责实操和踩坑,当个记录使用。

说在前面

我之前一直用 Koodo Reader 看电子书,网页版挺爽的,在 Windows 台式机和 Mac 上各开一个浏览器标签页,格式支持也非常完善(主要看 Z-Library 下的 EPUB 和网文 TXT)。

但有个致命痛点:它的"同步"根本不是真同步,是两端各存一份数据。

导书要在 Windows 和 Mac 上分别上传两遍,看完一本书想"归档",得在两边手动删两次。更要命的是进度同步 —— 虽然 Koodo 理论上能连 WebDAV / 网盘做备份,但必须手动点同步按钮,还是"合并后在本地和云端各保存一份"的逻辑。当然,现实中我连这个都没做过,只是每次记住进度,然后在另一端手动进行同步···

这不是我要的"即用即走"。我希望的是:

  1. 书只存一份(在 NAS),Windows 和 Mac 就是两个"显示器";
  2. 随时在任何设备打开,进度无缝同步,自动在那儿,不需要我想"有没有同步"这回事;
  3. 看完的书从书架上"消失",但物理文件安全归档在 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:旧书归档区(地下室书柜,看完的书全扔这里,想找还能找到)

这样的好处是:

  1. 书架永远干净(Kavita 只扫 books_active);
  2. 看完的书不会真的物理删除,而是安全归档;
  3. 以后想重看,从归档区拖回 books_active 即可,进度数据还在 Kavita 的数据库里。

在群晖的 File Station 里:

  1. 找到已有的 /docker 文件夹(之前部署 Open WebUI / FRP 时建的)。
  2. 在 docker 下新建文件夹 kavita,再在 kavita 下建 config。
  3. 在 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 上会留下两个"污染":

  1. 本地的 kavita.tar 文件 —— 直接删掉即可(Shift + Delete 彻底删除)。
  2. 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 在国内被墙了,超时后容器判断启动失败,触发自动重启,然后再次超时……死循环。

解法(临时挂代理):

  1. 在 Windows 台式机上,打开代理软件(Clash / V2ray 等),开启 “允许局域网连接 (Allow LAN)”,记下端口号(通常是 7890 或 10808)。
  2. 回到群晖 Container Manager,停止 Kavita 容器。
  3. 选中容器,点"编辑”,找到 “环境” 标签页,添加两条环境变量:
    • HTTP_PROXY:http://你的WindowsIP:代理端口(如 http://192.168.31.x:7890)
    • HTTPS_PROXY:同上
  4. 保存,重新启动容器。这次有梯子,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。

五、验证工作流

第一次导书测试

  1. 在 Windows 台式机上,通过文件资源管理器访问 NAS 的 SMB 共享(地址类似 \\192.168.31.x\docker);
  2. 找到 books_active/reading 文件夹,把它固定到左侧侧边栏的"快速访问"里(这样以后 Z-Library 下载时可以直接保存到这里,不用经过本地 Downloads 文件夹);
  3. 随便找一本 EPUB 扔进去;
  4. 回到 Kavita 网页,刷新一下,书应该会出现在首页书架上(可能需要等几秒到一分钟,或者要进到刚刚说的 Library 选项卡,手动 Scan。后面会讲扫描频率问题)。

跨设备同步测试

  1. 在 Windows 浏览器里打开这本书,随便翻到某一章;
  2. 关掉网页(或直接关机);
  3. 拿起 Mac(或手机),在浏览器里输入同一个地址 http://192.168.31.x:5050,登录;
  4. 打开刚才那本书,进度应该自动恢复到你在 Windows 上看到的那一章。

如果这两步都通过,说明"真同步"已经实现了。

“阅后归档"测试

  1. 在 Windows 的文件管理器里,把刚才那本书从 books_active/reading 剪切(或拖动) 到你的老 /books 归档文件夹里;
  2. 回到 Kavita 网页,刷新一下,这本书应该从书架上消失了;
  3. 但文件还在 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 表达式改成:

* * * * *

(五个星号表示"每分钟")

保存。

关于性能和硬盘寿命的担忧

我一开始很担心"每分钟扫一次"会不会:

  1. 让 NAS 的硬盘(18TB 希捷酷狼 Pro)无法休眠,耗电增加;
  2. 频繁扫描导致硬盘寿命缩短;
  3. 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 的文件资源管理器当作你唯一且绝对安全的控制台。看完了不需要在阅读器里学第二套管理逻辑,直接拖文件就行。

最终的工作流

  1. Z-Library 下书,浏览器保存路径直接选 books_active/reading(已固定在快速访问里);
  2. 最多等一分钟,书自动出现在 Kavita 书架上;
  3. 在任何设备(Windows / Mac / 手机)打开 Kavita,进度自动同步;
  4. 看完后,在 Windows 文件管理器里把书从 books_active/reading 拖回 /books 归档区;
  5. 书架自动清空,但文件还在 NAS 上,以后想重看还能找到。

零管理成本、零手动同步、零数据冗余。

我是用新的阅读器读的很爽,随地大小读的 Wenix,希望你开心~