背景
我曾经维护过几年博客,但随着时代在变、年纪渐长,注意力越来越难以支持长文的阅读和书写。很多杂乱的想法越来越倾向于在各种社交媒体上发表,唯一仅剩的长文书写欲仅存于技术和各种个人服务折腾经验的分享。对于这种类型的文本来说,相比按照发布时间线形排布的博客,类似于笔记的知识库系统显然是更好的选择。
目前将 Obsidian 笔记公开发布到 Web 的方法除了每月 8 USD 的官方 Obsidian Publish,比较主流的似乎也就只有 Quartz 和 Obsidian Digital Garden。两者我都试用过,后者的部署方式始终过于捆绑 Vercel 这种 serverless 部署平台,秉持着手头已经有一堆 VPS 何必再给这种部署平台花钱的心态,我是非常抵触的,于是 Quartz 成了一个必然的选择。
Quartz 的本质类似于 Jekyll 和 Hexo 这类静态站点生成器,只是天生针对 Obsidian 开发。和搭建博客一样,部署公开的知识库看似有很多方案可以选择,但实际称得上好用的并不多。有的编辑语法复杂得像 Wikipedia,有的定位面向团队内部而不支持自定义域名。我并非是 Obsidian 或者静态网站生成器的教徒,只是要同时满足我所有需求的选择实在不多:
- 支持 Markdown 语法
- 方便的数据导入、导出格式
- 支持桌面端和移动端无缝切换编辑、发布
- 支持自托管/提供廉价、可自定义域名的托管方案
- 美观的前端和后台界面
- 好用的后台编辑环境
在这种情况下往往只能选择一个最接近的方案缝缝补补,而这套 Obsidian + Quartz 是我目前能找到的最接近我需求的方案。
为什么不用 Quartz 官方部署方案
按照 Quartz 官方文档的指引,我们本该在本地克隆官方 GitHub 仓库,通过 Git 仓库创建、管理、发布站点和更新 Quartz 程序。然而这套方案里有两个我很不喜欢的点:
- Quartz 程序代码未与站点数据分离:与 Hexo 等软件通过
npm全局安装后在任意目录创建站点的使用方法不同,Quartz 的程序代码与站点数据、配置等全都挤在同一个 Git 仓库目录。这导致本该可以随地创建的 Obsidian 笔记仓库难以脱离 Git 仓库目录。 - 移动端的适配问题:这是静态网站生成器的一个通病,而 Quartz 更甚。Quartz 程序的更新和站点的数据同步、部署发布都严重依赖 Git 和
npx命令。尽管这方便了站点数据与配置的版本管理和同步,但同时也引入了更加严峻的移动端适配问题。尤其是在 iOS 环境中,用户很难提供一个自由的 Git 环境用于管理、同步仓库,各种 Quartz 命令也更是难以执行。
如前文所说,我手头已经有好几台 VPS,所以使用 GitHub Pages 部署静态页面并不是我的强需求。说实话,相比让这种笔记仓库出现在我的 GitHub 里,我反而更希望它能像我的其他个人服务一样使用 Docker 部署在自己的 VPS 上统一管理。
Docker + WebDAV 的同步部署思路
值得庆幸的是,Quartz 现在提供了官方的 Docker 镜像,因此我们可以直接使用 Docker 部署实时服务器监听数据目录变化,再将必要的站点配置文件和数据目录挂载进容器,实现站点数据与程序代码分离。
解决完 Docker 发布,剩下的问题是如何将本地的数据推送至远端 VPS 目录。得益于 Obsidian 的插件系统,我们有很多选择。我的方案是在 VPS 端同样使用 Nginx 的 Docker 镜像架设一个 WebDAV 服务器,再使用 Remotely Save 插件将笔记仓库作为站点数据推送到远端。
当然,这只是其中一种方案,你可以根据你惯用的多端同步方式进行选择,毕竟服务器端本质上也只是多端同步的一个单向节点。我使用 WebDAV 的前提是我的 Obsidian 仓库本身已经通过 iCloud 在 Mac 和 iOS 间无感同步,此时仅需一个单向通道来将本地设备端的数据推送至远端,触发实时服务器更新构建站点。
参考配置
compose.yaml
services:
quartz:
container_name: quartz
image: ghcr.io/jackyzha0/quartz:latest
restart: unless-stopped
mem_limit: 800M
memswap_limit: 1G
volumes:
- ./temp:/usr/src/app/.quartz
- ./data/content:/usr/src/app/content
- ./data/quartz.config.yaml:/usr/src/app/quartz.config.yaml
entrypoint:
- /bin/sh
- -c
command:
- |
npx quartz plugin install --from-config &&
exec npx quartz build --serve
networks:
- web
labels:
- traefik.enable=true
- traefik.http.routers.quartz.rule=Host(`example.com`)
- traefik.http.services.quartz.loadbalancer.server.port=8080
webdav:
container_name: webdav
build:
context: .
dockerfile: Dockerfile.webdav
restart: unless-stopped
volumes:
- ./data:/var/www/webdav
- ./config/webdav.conf:/etc/nginx/nginx.conf:ro
- ./config/htpasswd:/etc/nginx/htpasswd:ro
networks:
- web
labels:
- traefik.enable=true
- traefik.http.routers.webdav.rule=Host(`push.example.com`)
- traefik.http.services.webdav.loadbalancer.server.port=80
networks:
web:
external: trueDockerfile.webdav
FROM alpine:3.20
RUN apk add --no-cache nginx nginx-mod-http-dav-ext apache2-utils && \
mkdir -p /run/nginx
CMD ["nginx", "-g", "daemon off;"]webdav.conf
user root;
worker_processes auto;
load_module /usr/lib/nginx/modules/ngx_http_dav_ext_module.so;
events {}
http {
server {
listen 80;
server_name _;
client_max_body_size 500M;
location / {
root /var/www/webdav;
autoindex off;
dav_methods PUT DELETE MKCOL COPY MOVE;
dav_ext_methods PROPFIND OPTIONS LOCK UNLOCK;
dav_access user:rw group:rw all:r;
create_full_put_path on;
auth_basic "WebDAV";
auth_basic_user_file /etc/nginx/htpasswd;
}
}
}