自托管 Waline 评论:SQLite 与反向代理
静态博客最大的缺口是评论。第三方服务要么加载慢,要么把读者数据放在别人那里,要么要求登录。Waline 是这几年的一个折中答案:开源、可自托管、支持匿名评论。这篇文章记录一次最小化部署的过程,包括两个我实际踩到的坑。
一、为什么不直接用第三方
先说结论,再说过程。我评估过的方案:
| 方案 | 优点 | 问题 |
|---|---|---|
| Disqus | 接入快,生态成熟 | 国内加载不稳定,数据在境外 |
| utterances / giscus | 无需数据库,基于 GitHub | 读者必须有 GitHub 账号 |
| 云服务版 Waline | 免运维 | 数据在第三方,免费额度有限 |
| 自托管 Waline | 数据自己掌握,支持匿名 | 需要自己维护一个容器 |
对我这种流量不大、但希望评论数据长期可用的博客,自托管是合适的。代价是多了 100 MB 左右的常驻内存,比原来的 Java 服务轻得多。
二、部署:一个 compose 文件
Waline 的服务端只有一个容器,SQLite 模式下不需要额外的数据库。目录结构很简单:
/opt/waline/
├── docker-compose.yml
├── .env
└── data/
└── waline.sqlite
docker-compose.yml:
services:
waline:
image: lizheming/waline@sha256:261302985f164ec224231ea004631f30f8a3b57c10c913a9e1b3ad96630815dd
container_name: waline
restart: unless-stopped
ports:
- "127.0.0.1:8360:8360"
environment:
SITE_NAME: Stone Blog
SITE_URL: https://stone.us.kg
SECURE_DOMAINS: stone.us.kg
SQLITE_PATH: /app/data
JWT_TOKEN: ${WALINE_JWT_TOKEN}
TZ: Asia/Shanghai
volumes:
- ./data:/app/data
几个决定值得说明:
- 镜像用 digest 固定,而不是
latest。评论服务不常升级,固定版本可以避免某天自动拉到不兼容的新版本; - 端口只绑
127.0.0.1,不对外暴露。外部访问一律经过反向代理; JWT_TOKEN放.env,权限设为600,不写进 compose 文件,也就不容易误提交。
关于 SQLite 的适用边界
SQLite 适合单机、低并发写入的场景。博客评论的写入频率很低,读多写少,完全够用。如果以后并发上来了,把
SQLITE_PATH换成 MySQL/PostgreSQL 的连接串即可,Waline 本身支持多种存储。
三、第一个坑:no such table
容器启动后,我直接请求评论接口,得到的是:
{"errno":500,"errmsg":"no such table: wl_Comment"}
容器日志里同时出现:
SQLITE_ERROR: no such table: wl_Comment
原因很简单:SQLite 模式下,Waline 不会自动建表。容器启动时只创建了一个空的 waline.sqlite 文件,表结构需要手动初始化。
官方的建表脚本在仓库的 assets/waline.sqlite.sql。下载后导入:
sqlite3 /opt/waline/data/waline.sqlite < waline.sqlite.sql
sqlite3 /opt/waline/data/waline.sqlite '.tables'
预期输出:
wl_Comment wl_Counter wl_Users
导入完成后重启容器,再请求一次接口,返回就正常了:
{"errno":0,"errmsg":"","data":{"page":1,"totalPages":0,"pageSize":10,"count":0,"data":[]}}
这里有个顺序问题:如果容器先启动、后建表,最好重启一次。我遇到的情况是建表后不重启,连接池仍然拿着旧的文件句柄,重启后才恢复正常。
四、第二个坑:Forbidden
表建好之后,用 curl 直接请求接口,返回:
{"errno":403,"errmsg":"Forbidden"}
换成带 Origin 和 Referer 的请求就正常了:
curl -H 'Origin: https://stone.us.kg' \
-H 'Referer: https://stone.us.kg/about/' \
'http://127.0.0.1:8360/api/comment?path=/about/'
这是 Waline 的防滥用机制:SECURE_DOMAINS 里的域名才有资格调用接口。通过反向代理访问时,浏览器会自动带上正确的 Origin,所以这个问题只在本地直连调试时出现,不是配置错误,不需要为此关掉校验。
反过来说,如果线上也出现 403,就要检查反向代理有没有正确传递 Host:
proxy_set_header Host $host;
五、反向代理:把 /comment/ 接进来
因为站点已经有域名和证书,我不想再为评论单独申请一个子域名,于是用路径前缀来区分:
location ^~ /comment/ {
proxy_pass http://127.0.0.1:8360/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_read_timeout 60s;
client_max_body_size 10m;
}
两个细节:
proxy_pass末尾的/会把/comment/xxx改写成/xxx再转发,所以 Waline 收到的路径和独立部署时一致;^~前缀匹配优先于正则匹配,避免/comment/api/comment被静态资源的规则截走。
前端配置对应地指向这个地址:
waline:
serverURL: https://stone.us.kg/comment
path: window.location.pathname
dark: 'html[data-user-color-scheme="dark"]'
requiredMeta: ['nick']
path 用页面路径做区分,这样一个服务就能给所有文章提供评论;dark 绑定主题的深色模式属性,切换主题时评论框会跟着变。
六、备份与升级
SQLite 的好处在这里体现得很直接——备份就是复制一个文件:
docker exec waline sh -c 'sqlite3 /app/data/waline.sqlite ".backup /app/data/backup.sqlite"'
cp /opt/waline/data/backup.sqlite /mnt/backups/waline-$(date +%F).sqlite
用 .backup 而不是直接 cp,是因为服务运行中直接复制可能拿到写入到一半的文件。
升级也很简单:改 compose 里的 digest,docker compose up -d。升级前备份一次,出问题就回退 digest,数据文件不受影响。
七、安全上的几个默认项
自托管不等于裸奔,部署时我确认了这几件事:
- 不暴露管理接口:端口只绑定
127.0.0.1,外部只能通过反向代理访问/comment/; - 限定来源域名:
SECURE_DOMAINS只写自己的域名,防止别的站点盗用评论接口; - 限制请求体大小:
client_max_body_size 10m,避免大文件把服务打满; - 不开启匿名管理员:管理员账号通过环境变量单独设置,不和普通评论混在一起。
如果评论量变大,还可以加频率限制、Akismet 反垃圾,这些 Waline 都支持,按需开启即可。
八、小结
整套方案的成本很低:一个容器、一个 SQLite 文件、一段反向代理配置。它不追求高可用,也不适合高并发,但对一个个人博客来说,已经足够稳定。
真正花时间的不是部署,而是两个坑:SQLite 要手动建表、接口校验依赖 Origin。把这两点写下来,下次重装能少走不少弯路。