自托管 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,数据文件不受影响。

七、安全上的几个默认项

自托管不等于裸奔,部署时我确认了这几件事:

  1. 不暴露管理接口:端口只绑定 127.0.0.1,外部只能通过反向代理访问 /comment/;
  2. 限定来源域名:SECURE_DOMAINS 只写自己的域名,防止别的站点盗用评论接口;
  3. 限制请求体大小:client_max_body_size 10m,避免大文件把服务打满;
  4. 不开启匿名管理员:管理员账号通过环境变量单独设置,不和普通评论混在一起。

如果评论量变大,还可以加频率限制、Akismet 反垃圾,这些 Waline 都支持,按需开启即可。

八、小结

整套方案的成本很低:一个容器、一个 SQLite 文件、一段反向代理配置。它不追求高可用,也不适合高并发,但对一个个人博客来说,已经足够稳定。

真正花时间的不是部署,而是两个坑:SQLite 要手动建表、接口校验依赖 Origin。把这两点写下来,下次重装能少走不少弯路。


自托管 Waline 评论:SQLite 与反向代理
https://stone.us.kg/posts/self-hosted-waline
作者
Mark Luo
发布于
2026年11月04日
许可协议