<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="html">Zakee's Planet</title><link href="https://zak.ee/atom.xml" rel="self"/><link href="https://zak.ee/"/><id>https://zak.ee/atom.xml</id><updated>2026-05-31T19:56:15+08:00</updated><generator uri="https://gohugo.io/" version="0.163.3">Hugo</generator><rights>This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.</rights><author><name>Zakee</name></author><entry><title type="html">自建Logseq同步服务器</title><link href="https://zak.ee/writing/2026/logseq-selfhost-sync/" rel="alternate" type="text/html"/><id>https://zak.ee/writing/2026/logseq-selfhost-sync/</id><published>2026-05-31T19:20:02+08:00</published><updated>2026-05-31T19:56:15+08:00</updated><summary>使用Logseq做我的主力笔记软件已经好几年了，笔记的同步问题却一直困扰着我。Logseq虽有官方付费同步服务，然而因为我比较少用手机版本，所以1个月5美元对我来说还是偏贵了，毕竟我现在用的服务器也才24元一个月，而我又很喜欢自部署各种服务，所以一直都希望可以自建同步服务。
由于Logseq笔记都是直接保存成本地Markdown文件，所以之前我一直是用Syncthing这样的文件同步工具实现同步。然而由于我的使用强度并不高，没必要一直开着Syncthing，所以要在手机上使用Logseq就要先打开Syncthing，等同步完成了再打开Logseq。可是用手机版Logseq大部分情况都是想要快速记录一些想法，这么一折腾就忘了要记什么了，因此可以说整体使用体验不尽如人意。</summary><content type="html"><![CDATA[<p>使用<a href="https://logseq.com/" target="_blank" rel="noopener">Logseq</a>做我的主力笔记软件已经好几年了，笔记的同步问题却一直困扰着我。Logseq虽有官方付费同步服务，然而因为我比较少用手机版本，所以1个月5美元对我来说还是偏贵了，毕竟我现在用的服务器也才24元一个月，而我又很喜欢自部署各种服务，所以一直都希望可以自建同步服务。</p>
<p>由于Logseq笔记都是直接保存成本地Markdown文件，所以之前我一直是用Syncthing这样的文件同步工具实现同步。然而由于我的使用强度并不高，没必要一直开着Syncthing，所以要在手机上使用Logseq就要先打开Syncthing，等同步完成了再打开Logseq。可是用手机版Logseq大部分情况都是想要快速记录一些想法，这么一折腾就忘了要记什么了，因此可以说整体使用体验不尽如人意。</p>
<p>后来Logseq开始开发DB版，数据改为保存到本地SQLite数据库中，这样就不能直接用Syncthing同步了。官方对自建同步的态度一直都不太明确，直到近几个月才有进展，现如今自建同步已经正式支持了，我一直在Discord上关注这个，当然也是第一时间部署啦，下面就简单分享一下部署流程。</p>
<figure class="big">
		<img alt="logseq-db.webp" height="1280" loading="lazy" src="/writing/2026/logseq-selfhost-sync/logseq-db.webp" width="1920">
		
	</figure>
	<p>需要注意的是自建同步服务只适用于Logseq DB版，原来的Markdown版（现在叫Logseq OG）不适用。目前<a href="https://github.com/logseq/logseq/releases" target="_blank" rel="noopener">GitHub</a>上的Nightly版本才是DB版本，以及访问<a href="https://test.logseq.com/" target="_blank" rel="noopener">test.logseq.com</a>也是DB版本。以下我会使用Docker来部署（感谢yshalsager提供的<a href="https://github.com/yshalsager/logseq-selfhost" target="_blank" rel="noopener">Docker镜像</a>），以及使用<a href="https://github.com/lucaslorentz/caddy-docker-proxy" target="_blank" rel="noopener">caddy-docker-proxy</a>来做反向代理，因此可以很方便地实现自动https。</p>
<h2 id="部署caddy-docker-proxy">部署caddy-docker-proxy <a href="#%e9%83%a8%e7%bd%b2caddy-docker-proxy" class="anchor tdln dimmer">#</a></h2><p>可参考<a href="https://github.com/lucaslorentz/caddy-docker-proxy#basic-usage-example-docker-compose" target="_blank" rel="noopener">文档</a>。新建一个文件夹<code>caddy</code>，用于存放caddy相关配置，然后创建<code>compose.yaml</code>文件。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">mkdir caddy
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> caddy
</span></span><span class="line"><span class="cl">touch compose.yaml
</span></span></code></pre></div><p><code>compose.yaml</code>文件内容如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">caddy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">lucaslorentz/caddy-docker-proxy:ci-alpine</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="m">80</span><span class="p">:</span><span class="m">80</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="m">443</span><span class="p">:</span><span class="m">443</span><span class="l">/tcp</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="m">443</span><span class="p">:</span><span class="m">443</span><span class="l">/udp</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">CADDY_INGRESS_NETWORKS=caddy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">caddy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/run/docker.sock:/var/run/docker.sock</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">caddy_data:/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">caddy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">caddy_data</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span></code></pre></div><p>然后创建上述名叫<code>caddy</code>的网络：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">docker network create caddy --ipv6
</span></span></code></pre></div><p>最后运行<code>docker compose up -d</code>即可。</p>
<h2 id="部署logseq-db-sync">部署Logseq db-sync <a href="#%e9%83%a8%e7%bd%b2logseq-db-sync" class="anchor tdln dimmer">#</a></h2><p>首先还是创建一个文件夹，用于存放同步服务的相关配置，以及创建<code>.env</code>以及<code>compose.yaml</code>两个文件。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">mkdir logseq
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> logseq
</span></span><span class="line"><span class="cl">touch .env
</span></span><span class="line"><span class="cl">touch compose.yaml
</span></span></code></pre></div><p><code>.env</code>内容如下，基本只用改<code>DB_SYNC_BASE_URL</code>以及<code>DB_SYNC_ADMIN_TOKEN</code>这两个值：</p>
<pre tabindex="0"><code># Node adapter runtime settings
DB_SYNC_PORT=8787
DB_SYNC_BASE_URL=https://logseq-sync.example.com # 用来访问同步服务的地址
DB_SYNC_DATA_DIR=/app/data
DB_SYNC_STORAGE_DRIVER=sqlite
DB_SYNC_ASSETS_DRIVER=filesystem
DB_SYNC_LOG_LEVEL=info

# Logseq Cognito values used for JWT validation
COGNITO_ISSUER=https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLnju8
COGNITO_CLIENT_ID=69cs1lgme7p8kbgld8n5kseii6
COGNITO_JWKS_URL=https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLnju8/.well-known/jwks.json

# Optional hardening for maintenance/admin endpoints
DB_SYNC_ADMIN_TOKEN=&lt;一串随机字符&gt;
</code></pre><p><code>compose.yaml</code>文件内容如下：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sync</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/yshalsager/logseq-selfhost-sync:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">logseq-sync</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">env_file</span><span class="p">:</span><span class="w"> </span><span class="l">.env</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">caddy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">sync_data:/app/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">read_only</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">tmpfs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/tmp</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cap_drop</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">ALL</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">security_opt</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="kc">no</span>-<span class="l">new-privileges:true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">caddy</span><span class="p">:</span><span class="w"> </span><span class="l">logseq-sync.example.com</span><span class="w"> </span><span class="c"># 改为与前面设置的相同的地址</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">caddy.reverse_proxy</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{{upstreams 8787}}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sync_data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">caddy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><p>然后运行<code>docker compose up -d</code>，caddy-docker-proxy会自动配置caddy及申请证书，如访问<code>https://logseq-sync.example.com/health</code>返回<code>{&quot;ok&quot;:true}</code>则说明部署成功。</p>
<p>值得说明的是当前Logseq需要使用AWS Cognito来实现用户认证，并不支持本地用户管理或者OIDC。前面<code>.env</code>中配置的就是当前官方使用的用户池，你也可以使用自己的用户池，不过不是必须，<a href="https://4shutosh.com/selfhost-logseq#part-2-aws-cognito-setup" target="_blank" rel="noopener">这篇文章</a>有介绍，另外如果不想用Docker部署也可以参考这篇文章。</p>
<h2 id="在logseq客户端中使用">在Logseq客户端中使用 <a href="#%e5%9c%a8logseq%e5%ae%a2%e6%88%b7%e7%ab%af%e4%b8%ad%e4%bd%bf%e7%94%a8" class="anchor tdln dimmer">#</a></h2><p>目前Logseq DB版还有诸多不完善的地方，所以强烈建议进行以下操作之前先备份自己的库。打开Logseq DB客户端或者网页版<a href="https://test.logseq.com/" target="_blank" rel="noopener">test.logseq.com</a>，打开“设置”，“高级”，修改同步服务器URL为前面的自定义地址<code>https://logseq-sync.example.com</code>。Android端是点击右上角三个点图标，打开的菜单下面就是自定义同步服务器URL的选项。然后还需要登录，以及设置加密密码，用于服务器端的加密。最后打开“所有图谱”页面，如果以上配置全部没问题的话，本地的图谱会有“使用Logseq Sync（Beta测试）”来开启同步的选项，创建新图谱的时候也会提示是否使用Logseq Sync。</p>
<p>Logseq DB的同步服务端是一个Node.js程序，资源占用并不高，在我的1G内存服务器上也能轻松部署，同时有多个客户端连接的时候内存占用也不会超过100MB，另外同步速度也很快，几乎秒同步，体验非常好。</p>
]]></content></entry><entry><title type="html">我为什么不在中英文之间加空格</title><link href="https://zak.ee/writing/2025/add-space/" rel="alternate" type="text/html"/><id>https://zak.ee/writing/2025/add-space/</id><published>2025-11-10T23:40:03+08:00</published><updated>2025-11-10T23:40:03+08:00</updated><summary>不知道大家有没有听过这句话：
打字的時候不喜歡在中文和英文之間加空格的人，感情路都走得很辛苦，有七成的比例會在 34 歲的時候跟自己不愛的人結婚，而其餘三成的人最後只能把遺產留給自己的貓。畢竟愛情跟書寫都需要適時地留白。</summary><content type="html"><![CDATA[<p>不知道大家有没有听过这句话：</p>
<blockquote>
<p>打字的時候不喜歡在中文和英文之間加空格的人，感情路都走得很辛苦，有七成的比例會在 34 歲的時候跟自己不愛的人結婚，而其餘三成的人最後只能把遺產留給自己的貓。畢竟愛情跟書寫都需要適時地留白。</p>
</blockquote>
<p>我非常讨厌这句话。且不说中英文之间手动插入一个空格的做法本身就值得商榷，而且也没有任何官方规范规定中英文之间必须加空格，我就不明白加个空格怎么就有优越感了。诚然我认为中英文之间需要有一定的空隙，但空隙不一定是空格，加空格只是实现空隙的一种方法，而且可以说只是现阶段的变通方法。这里就说说我为什么不在中英文之间手动加空格，以及讨论一下现阶段我们还有没有其他手段可以实现这个中英文之间的空隙。</p>
<h2 id="加空格的坏处">加空格的坏处 <a href="#%e5%8a%a0%e7%a9%ba%e6%a0%bc%e7%9a%84%e5%9d%8f%e5%a4%84" class="anchor tdln dimmer">#</a></h2><p>首先，加空格引入了无意义的字符，可能会影响文本的检索。虽然现在很多地方都支持模糊搜索，但并不是所有，总有这种情况比如“我的iPhone”和“我的 iPhone”就互相搜不到。英文单词与单词之间有空格，这是肯定的，因为从语义上来说就应该有空格，不然这就是一个单词，不是句子了。但是中英文之间，从语义上来说并不存在这个空格。空格本身是一个字符，只是显示成了“空格”的样子，现在你按键盘上的空格键，打出来的就是Unicode<code>U+0020</code>空格，个人认为这是无意义的字符，“污染”了原始数据，很不优雅。</p>
<p>其次这个空格的宽度不便于控制，跟使用的字体、排版引擎都有关，而且常见的<code>U+0020</code>空格太宽了，这会破坏文本的整体性，影响阅读节奏。尤其是单字母和中文之间加上了空格，就会显得过于强调这个英文字母。请看以下例子：</p>
<p>前天下单了一件T恤，至今还没发货。<br>
前天下单了一件 T 恤，至今还没发货。</p>
<p>上面应该是用比例字体（Proportional Font）显示的，如果用的是等宽字体（Monospaced Font），间距会更加夸张：</p>
<pre tabindex="0"><code>前天下单了一件T恤，今天还没发货。
前天下单了一件 T 恤，今天还没发货。
</code></pre><p>本来“T恤”是个很常见的词汇，应该是一个整体的，加上空格之后，显得过于强调“T”这个字母，整个句子读起来好像“T”要重读一样，我个人觉得是不太舒服。这种中英文混合的词汇还真不少，单字母的就有“U盘”“X光”“B超”等，还有“腾讯QQ”“UC浏览器”这种。咱就是说如果我其他地方都加了空格，这里应该也要加吧，而直觉又告诉我这里不应该有空格，真是纠结，不如都不加。</p>
<p>基于以上原因，我至今都是倾向不在中英文加空格，而且我认为实现中英文之间空隙的这个任务应该是排版引擎做的。不要以为这是什么登月级别的技术难题，MS Word就算是很古老的版本都能处理中英文之间的空格问题了，更不用说Adobe InDesign这种专业排版软件。Web这边，IE浏览器非常超前地提供了<code>text-autospace</code>这个CSS属性，可以实现增大中英文字符之间间距的效果，然而这个“黑科技”随着IE的逝去已经被人遗忘。至今我们日常接触到的软件界面、Web页面，还是处理不好中英文之间空隙的问题，不过现在终于看到了曙光，下面细说。</p>
<h2 id="现状">现状 <a href="#%e7%8e%b0%e7%8a%b6" class="anchor tdln dimmer">#</a></h2><p>很多大公司为了规范，会强制要求中英文之间加空格，比如微软就是坚定的空格支持者，Windows系统界面里的中英文都加空格了。GitHub上有一个<a href="https://github.com/RightCapitalHQ/chinese-style-guide" target="_blank" rel="noopener">中文写作排版风格指南</a>，上面也是建议中英文之间加入空格，不过说到底这只是一种风格或者叫习惯罢了。那么有没有什么标准文件有针对这个问题的规定呢？</p>
<p>首先是2017年发布的我国新闻出版行业标准<code>CY/T 154—2017</code>《<a href="https://hbba.sacinfo.org.cn/attachment/onlineRead/e3ef82e93dd272792f5aacb0f582e62f" target="_blank" rel="noopener">中文出版物夹用英文的编辑规范</a>》，其中第八节对空格的规则描述如下，可见这里并没有规定中英文之间一定要加空格。</p>
<blockquote>
<ol>
<li>中文文本中夹用英文词语时，应根据所选用的中英文字体、字符间距以及排版的视觉效果决定英文词句与中文文字之间是否留有空格间距。如留空格，应保证体例的统一。</li>
<li>中文文本中夹用英文词句时，如英文部分之前或之后有中文标点符号，则英文部分与中文标点之间不设空格。</li>
<li>中文文本中夹用英文句子或段落时，英文句子或段落内部应按英文排版规则留空。</li>
</ol>
</blockquote>
<p>再来看看Web这边，《<a href="https://www.w3.org/TR/clreq/" target="_blank" rel="noopener">中文排版需求</a>》（Requirements for Chinese Text Layout，简称CLREQ）的初稿已于2015年发布，目前最新版本中关于<a href="https://www.w3.org/TR/clreq/#mixed_text_composition_in_horizontal_writing_mode" target="_blank" rel="noopener">横排的中、西文混排配置</a>说明如下：</p>
<blockquote>
<p>原则上，汉字与西文字母、数字间使用不多于四分之一个汉字宽的字距或空白。但西文、数字出现在行首或行尾时，则无须加入空白。</p>
</blockquote>
<p>注意这里用词是“空白”以及“字距”，而不是“空格”。CLREQ是非常重要的文件，有了这个，W3C可以一边对着文件中列出的排版需求，一边对着现阶段的Web标准，出一个<a href="https://www.w3.org/TR/clreq-gap/" target="_blank" rel="noopener">差距分析</a>（Gap Analysis），其中<a href="https://www.w3.org/TR/clreq-gap/#issue401_spacing" target="_blank" rel="noopener">issue401</a>就是关于中英文之间空格的问题。可见由于有手动加空格这个临时手段，这个问题的优先级仅为<code>Needs work for advanced use</code>。</p>
<p>解决这个问题的CSS属性叫做<code>text-autospace</code>，它的定义在<a href="https://www.w3.org/TR/css-text-4/#text-autospace-property" target="_blank" rel="noopener">CSS Text Module Level 4</a>中。这个应该很早之前就已经提出了，不过终于，Chrome在2023年底<a href="https://developer.chrome.google.cn/blog/css-i18n-features?hl=zh-cn" target="_blank" rel="noopener">率先实现了这个功能</a>，从Chrome 120开始只要开启<code>chrome://flags/#enable-experimental-web-platform-features</code>这个flag就可以体验了，Edge等同理。终于终于，9月份发布的Chrome 140已经正式支持了<code>text-autospace</code>属性，最新的Safari也已支持，本来还以为Firefox动作会比较慢，我有关注一个20年前创建的issue：<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=289130" target="_blank" rel="noopener">289130</a>，本来一直没啥动静，几天前点进去发现有人说Firefox 145已经支持了，真是有点被吓到了😂。目前只要用Firefox Beta版本或者开发者版就能体验了，正式版也应该很快了。截至本文发布时的<a href="https://caniuse.com/?search=text-autospace" target="_blank" rel="noopener">浏览器支持情况</a>如下，作为一个渐进增强功能这已经非常可用了。</p>
<figure class="big">
		<img alt="text-autospace浏览器支持" height="419" loading="lazy" src="/writing/2025/add-space/caniuse-text-autospace.webp" title="Can I use text-autospace?" width="1719">
		<figcaption>
			<p>Can I use text-autospace?</p>
		</figcaption>
	</figure>
	<p>按照CSS规范<code>text-autospace: normal</code>才应该是默认，不过Chrome现阶段默认值是<code>no-autospace</code>，即浏览器不会默认加空格，跟之前的行为一样，因此网站需要自己申明<code>text-autospace: normal</code>才能看到效果。本站就已经添加这个属性了，如果你用的是最新Chrome内核的浏览器或者Safari，或者Firefox 145及更高版本，应该就能看到本站中英文之间已经自己加上1/8全角汉字宽的空隙了。我觉得这个间距非常舒服，既不会像没间距那样显得过于逼仄，又不会像手动加空格之后那样过宽，给人一种过于强调英文部分的感觉。</p>
<h2 id="写在最后">写在最后 <a href="#%e5%86%99%e5%9c%a8%e6%9c%80%e5%90%8e" class="anchor tdln dimmer">#</a></h2><p>每次看到这样的千古难题取得进展就非常激动，但是必须要承认路还很长。本文当然不是建议你也不要加空格，毕竟在现阶段，加空格确实是有用的，但是我们必须要明确这只是一个变通方法，更重要的还是推动排版引擎的进步，而不是把变通方法当成理所应当，还谜之优越感。</p>
]]></content></entry><entry><title type="html">也谈“数字基建”——我的VPS使用报告</title><link href="https://zak.ee/writing/2024/my-server-setup/" rel="alternate" type="text/html"/><id>https://zak.ee/writing/2024/my-server-setup/</id><published>2024-01-30T23:18:53+08:00</published><updated>2024-03-30T12:28:11+08:00</updated><summary>第一次看到“数字基建”这个词是在这篇博客，其中作者盘点了他搭建的各种自托管服务，而我也是一直热衷于自己搭建各种服务，尤其是近一年多以来，我部署了不少自托管服务，不像之前很多时候都只是搭建玩玩，现在很多服务都是每天必用，已经离不开了的。之前也水过一篇《Docker时代的自建服务体验》，介绍了当时我的Traefik等服务器基础服务的配置，现2023年刚过，不如就对我的VPS使用做一个盘点，也分享一下我的“数字基建”。
用过的VPS #第一次折腾服务器/VPS应该还是2016年，当时为了搭建自己的博客，就开始折腾起腾讯云教育优惠1元/月的服务器，从此走上了一条不归路。这么多年来也换过不少VPS了，简单盘点如下：</summary><content type="html"><![CDATA[<p>第一次看到“数字基建”这个词是在<a href="https://blog.thetbw.xyz/archives/talk-about-my-self-hosted-service" target="_blank" rel="noopener">这篇博客</a>，其中作者盘点了他搭建的各种自托管服务，而我也是一直热衷于自己搭建各种服务，尤其是近一年多以来，我部署了不少自托管服务，不像之前很多时候都只是搭建玩玩，现在很多服务都是每天必用，已经离不开了的。之前也水过一篇《<a href="/writing/2022/selfhosted-w-docker/">Docker时代的自建服务体验</a>》，介绍了当时我的Traefik等服务器基础服务的配置，现2023年刚过，不如就对我的VPS使用做一个盘点，也分享一下我的“数字基建”。</p>
<h2 id="用过的vps">用过的VPS <a href="#%e7%94%a8%e8%bf%87%e7%9a%84vps" class="anchor tdln dimmer">#</a></h2><p>第一次折腾服务器/VPS应该还是2016年，当时为了搭建自己的博客，就开始折腾起腾讯云教育优惠1元/月的服务器，从此走上了一条不归路。这么多年来也换过不少VPS了，简单盘点如下：</p>
<ul>
<li><strong>2016年4月~2021年11月</strong>：腾讯云以及阿里云学生机，中间也用过Vultr的机器</li>
<li><strong>2021年12月~2022年9月</strong>：Vultr（日本）</li>
<li><strong>2022年8月~2022年9月</strong>：DigitalOcean（旧金山）</li>
<li><strong>2022年9月~2022年12月</strong>：LightNode（洛杉矶）</li>
<li><strong>2022年12月~2023年8月</strong>：腾讯云轻量（新加坡）</li>
<li><strong>2023年8月至今</strong>：CloudCone（洛杉矶）</li>
<li><strong>2023年10月至今</strong>：阿里云轻量（香港）</li>
</ul>
<p>目前正在使用的VPS：</p>
<table>
	<thead>
			<tr>
					<th style="text-align: left">服务商/区域类型</th>
					<th style="text-align: left">配置</th>
					<th style="text-align: left">价格</th>
					<th style="text-align: left">备注</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td style="text-align: left">阿里云/香港轻量</td>
					<td style="text-align: left">2C2G / 60GB / 2TB@30Mbps</td>
					<td style="text-align: left">CNY34/月</td>
					<td style="text-align: left">主力机</td>
			</tr>
			<tr>
					<td style="text-align: left">CloudCone/洛杉矶DC1</td>
					<td style="text-align: left">1C0.5G / 30GB / 3TB@1Gbps</td>
					<td style="text-align: left">USD9.5/年</td>
					<td style="text-align: left">邮箱、ActivityPub服务</td>
			</tr>
			<tr>
					<td style="text-align: left">CloudCone/洛杉矶DC2</td>
					<td style="text-align: left">2C2G / 80GB / 3TB@1Gbps</td>
					<td style="text-align: left">USD29.26/年</td>
					<td style="text-align: left">8月到期不再续费</td>
			</tr>
	</tbody>
</table>
<p>CloudCone DC2这台本来是我的主力机，但是10月份我又入了阿里云香港轻量。对比CloudCone，阿里云香港的网络质量实在是好太多，我这里的延迟才十几ms，也不丢包，30Mbps带宽可以跑满，虽然价格贵了不少，但考虑到有搭建Minecraft服务器的需求，没办法，因为CloudCone上搭建的MC服务器是完全没办法正常游戏的。目前我的所有服务都已经迁移出，这台CloudCone DC2几乎是闲置的状态，只是偶尔用它build一下Docker镜像，以及连VS Code远程开发，因此我想要出掉它。这台机器是bug机，网络流量使用是不统计的，所以也许是无限流量，如有兴趣收购的话欢迎联系我，按剩余价值便宜出。</p>
<p>CloudCone DC1这台是今年圣诞促销跟风买的，主要是阿里云服务器这边想要开25端口比较麻烦，留一台CloudCone可以继续运行邮件服务，当然也可以搭建其他不方便在阿里云上部署的服务。主要是这个价格实在太优惠了，9.5美金一年，相当于一个月才6块钱，约等于白送😂。</p>
<h2 id="基础环境">基础环境 <a href="#%e5%9f%ba%e7%a1%80%e7%8e%af%e5%a2%83" class="anchor tdln dimmer">#</a></h2><h3 id="os">OS <a href="#os" class="anchor tdln dimmer">#</a></h3><p>关于服务器的操作系统，记得最开始我只敢用Windows系统，迫于配置太低卡得不行，就换成了Linux，直到现在还能回味起当时查遍各种教程，终于搭建好了自己WordPress博客时的成就感，从此Debian就是我最常用的系统了。中间有段时间用的是Ubuntu Server，然而随着系统的更新，乱七八糟的东西越来越多，motd里面打打广告也就算了，后来不知道什么时候开始<code>apt upgrade</code>的时候都有广告了，这是真的忍不了。切换回Debian就舒服多了，资源占用也少了，更重要的是Debian完全由社区维护，不会受商业公司决策的影响，而且在全球范围来看，Debian也是最广泛使用的服务器操作系统之一，不用担心找不到资源以及问题的解决方案。就是Debian stable的软件包版本更新比较慢，然而现在很多服务都直接Docker容器部署，软件包更新慢也就影响不大。因此直到今天，Debian依然是我最推荐的服务器Linux发行版。</p>
<p>然而最近我把服务器重装成了<a href="https://www.alpinelinux.org/" target="_blank" rel="noopener">Alpine Linux</a>。经常看见基于这个系统的Docker镜像，我部署的Docker容器已有不少就是基于Alpine，可我竟从没有想过在宿主机上用这个系统。之前研究一键dd脚本，就找到了<a href="https://github.com/leitbogioro/Tools" target="_blank" rel="noopener">leitbogioro/Tools</a>这个，发现它可以一键把服务器系统dd成Alpine，尝试了下，装完后就真香了。首先我惊叹于Alpine Liunx的安装以及开机速度，几乎颠覆了我对快的认知。我用<code>apk</code>命令安装了<code>tree</code>、<code>htop</code>、<code>screen</code>、<code>git</code>等软件包，安装速度真的飞快，比<code>apt</code>快不少。然后我就运行了下<code>htop</code>，内存占用才40MB，正在运行的进程寥寥无几，说实话我从来都没有见过这样的<code>htop</code>画面。此外，Alpine的磁盘空间占用更是可以忽略不计，真可谓麻雀虽小，五脏俱全，这应该是它会被广泛选为Docker基础镜像的重要原因。</p>
<p>当然，Alpine与Debian区别还是很大的。首先就是Alpine不用systemd，而是用的OpenRC，这让我突然有一种“一夜回到解放前”的感觉，毕竟之前好不容易才习惯了systemd。好在原来的Debian系统上只有两个程序，<a href="https://github.com/adnanh/webhook/" target="_blank" rel="noopener">webhook</a>和<a href="https://github.com/tsl0922/ttyd" target="_blank" rel="noopener">ttyd</a>，是用systemd启动并且需要手动修改service配置文件，转换工作量不大，不过我还是想念用<code>journalctl</code>查日志的方便。另外Alpine Linux使用的<code>musl</code>与其他Linux发行版使用的<code>glibc</code>会有些不同，一些软件包可能用不了，比如VS Code远程就用不了。不过一般我需要的软件包<code>apk</code>源里面都可以找到，能用<code>apk</code>安装的基本肯定是能用的。另外，我的Alpine版本是Edge，相当于滚动发行版，软件包都很新，这对于我这种更新强迫症来说真的很爽。安装Docker直接<code>apk add docker</code>，也不用像Debian stable那样还要添加官方的<code>apt</code>源，前两天发布的Docker Engine 25.0我今天就用上了。还有我之前安装Hugo还要从GitHub上下载<code>.deb</code>格式的安装包手动安装，现在直接<code>apk add hugo</code>，版本更新就隔个几天。</p>
<p>还有，用Alpine之前我竟然都不知道常见的软件都有这么多更轻量的替代品，我一直都喜欢基于KISS<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>准则设计的软件，原来Linux也可以不用openssh-server，还有dropbear；原来也可以不用<code>sudo</code>，还有<code>doas</code>。Alpine的简洁小巧真的深得我心，就像对于各种自托管服务一样，我也是更加倾向于简单、轻量的程序。Alpine可能不适合于多数人，但对我来说，Alpine已经完全够用了，这段时间的使用过程中也没碰到什么大问题。只有一个<code>docker stats</code>命令不显示内存占用的问题，但这个很快就得到了修复<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>。</p>
<h3 id="服务器管理">服务器管理 <a href="#%e6%9c%8d%e5%8a%a1%e5%99%a8%e7%ae%a1%e7%90%86" class="anchor tdln dimmer">#</a></h3><p>服务器我习惯新建一个非root用户，对比直接用root账户来管理服务器，很多操作要加<code>sudo</code>（或者<code>doas</code>），虽然更麻烦了，但是也降低了用root账户手滑造成毁灭性后果的风险。而且现在有些软件会限制用root用户操作，用root反而麻烦了。另外，不管是什么服务器，我自己新建用户的<code>UID</code>和<code>GID</code>都是<code>1000</code>，这样当我从一个服务器转移一些文件到另一台服务器的时候权限也是对应得上的。</p>
<p>关于服务器远程连接 ，我禁止了root直接登录ssh，并取消了密码登录而只能用ssh密钥。密钥登录不用输密码也挺方便的，在合理保管ssh私钥的前提下也比密码更安全。Windows系统自带了openssh客户端，直接就可以用<code>ssh</code>命令连接，再配合Windows Terminal，可以一键连接，所以也不需要各种远程连接软件。还有前面提到<code>ttyd</code>这个程序，有了它，我可以在浏览器里面操作命令行，这个主要是方便在不常用电脑上临时操作的情况。当然这个页面我用了<a href="https://www.authelia.com/" target="_blank" rel="noopener">Authelia</a>来加固，后面要讲。</p>
<figure class="big">
		<img alt="Alpine Linux in Windows Terminal" height="780" loading="lazy" src="/writing/2024/my-server-setup/alpine.webp" title="Alpine Linux" width="1296">
		<figcaption>
			<p>Alpine Linux</p>
		</figcaption>
	</figure>
	<p>再说说服务器的文件管理。之前用过一段时间的VS Code远程插件，挺好用的，还可以配合Docker插件来管理服务器上的Docker。只可惜现在Alpine用不了VS Code远程，可能有办法能用上，可我已经不想折腾了，就用命令行操作好了。另外，命令行下编辑文件可以用<a href="https://micro-editor.github.io/" target="_blank" rel="noopener">micro</a>来替代nano，快捷键没有nano那么阴间，还支持鼠标操作，即便是在终端中编辑各种文本文件也很方便。除此之外我有还有部署<a href="https://filebrowser.org/" target="_blank" rel="noopener">File Browser</a>，也可以在Web界面上管理服务器的文件。</p>
<p>最后再说一下服务器的备份，备份可太重要了。之前我也用过<a href="https://kopia.io/" target="_blank" rel="noopener">Kopia</a>以及<a href="https://www.duplicati.com/" target="_blank" rel="noopener">Duplicati</a>这样的增量备份工具，但是感觉它们对于我的使用场景来说有点重且复杂，特别是Kopia，如果要使用它的GUI，常驻内存占用还挺高的，这让我无法接受，而只用CLI的话又要去写自动备份的脚本，有点麻烦。现在我就用<a href="https://github.com/gobackup/gobackup" target="_blank" rel="noopener">gobackup</a>了，每天凌晨3点全量备份一次到Storj白嫖的空间，保留最近三个历史版本。我要备份的数据都不到100MB，全量备份也没有什么压力，等以后数据量大了再去折腾自动备份脚本，到时候就用Kopia，或者用<a href="https://rdiff-backup.net/" target="_blank" rel="noopener">rdiff-backup</a>两台服务器之间互相备份。</p>
<h3 id="docker管理">Docker管理 <a href="#docker%e7%ae%a1%e7%90%86" class="anchor tdln dimmer">#</a></h3><p>这个话题<a href="/writing/2022/selfhosted-w-docker/">前面有写过</a>，不过时间有些久了，实际已经有了很多变化。</p>
<p>首先就是Traefik我已经升级到了3.0-beta版，有些配置需要修改。主要是眼馋3.0支持了Brotli压缩，以及可以指定默认的入口（EntryPoint），这样我后面Docker部署各种应用就更方便了，<code>labels</code>里面少写一条。</p>
<p>其次我已经不再使用Portainer。Portainer各种操作反应迟钝，而且后面我更喜欢直接用命令行操作。目前我的大部分Docker容器依然是用docker compose的方式来部署，数据和配置都放到一个目录下，这样备份起来很方便。这个目录结构如下，<code>apps</code>目录里面包含了所有的docker compose文件以及应用的持久数据、配置文件，一个子目录对应一个<code>compose.yaml</code>文件，一个<code>compose.yaml</code>里面可能包含一个或者多个服务。</p>
<pre tabindex="0"><code>apps
├── base
│   ├── authelia_data/
│   ├── traefik_data/
│   ├── nginx/
│   └── compose.yaml
├── app1
│   ├── data/
│   └── compose.yaml
├── app2
│   ├── data/
│   ├── .env
│   ├── some_config.toml
│   └── compose.yaml
└── …
</code></pre><p><code>base</code>目录下包含最基础的服务：</p>
<ul>
<li><a href="https://traefik.io/" target="_blank" rel="noopener">Traefik</a>：全局唯一的反代服务器，接管整个服务器的80和443端口。</li>
<li><a href="https://nginx.org/" target="_blank" rel="noopener">Nginx</a>：处理静态资源，以及作为Traefik错误页面中间件。流量在Traefik这边没有路由匹配的时候就会转发到Nginx，404等各种自定义错误页面也需要交给Nginx来发。</li>
<li><a href="https://www.authelia.com/" target="_blank" rel="noopener">Authelia</a>：为应用提供单点登录服务，以及两步验证。</li>
<li><a href="https://dozzle.dev/" target="_blank" rel="noopener">Dozzle</a>：可以在网页图形界面上查看Docker容器的日志以及资源占用情况，也可以实现停止或者重启容器操作，而且资源占用极低。</li>
</ul>
<p>可以发现最关键的就是前三个服务了，没了这三个，我的大部分docker容器都没办法正常访问。你也许会问Nginx不也可以反代，为什么还要用Traefik？首先，Traefik这边直接搞定了https，我永远都不用担心SSL证书续期的问题，而且申请的是Let's Encrypt通配符证书，也配置好了所有的http请求跳转到https以及启用HSTS。其二，Traefik可以自动服务发现。我部署一个Docker容器，只需要用label指定我想用什么域名来访问这个容器以及容器内部的端口号（端口甚至是可选的，因为很多容器默认就暴露一个端口，Traefik会自动配置），容器一启动，Traefik就会通过Docker的API检测到并自动更新它的路由配置，向这个域名的请求就会转发到对应容器，容器甚至不用映射端口到主机网络。而且，由于我已经把域名泛解析到我的服务器，配合通配符证书，我甚至都不用去编辑DNS配置。这些功能仅用Nginx是实现不了的，而且这个体验实在是太好。</p>
<p>比如我想部署个Dozzle，用什么域名访问呢，就<code>dozzle.zak.ee</code>好了，于是我用以下docker compose配置启动容器，浏览器打开<code>dozzle.zak.ee</code>，直接就能自动跳转https访问了🚀。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">dozzle</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">amir20/dozzle:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">dozzle</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">network_mode</span><span class="p">:</span><span class="w"> </span><span class="l">bridge</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/run/docker.sock:/var/run/docker.sock:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">DOZZLE_AUTH_PROVIDER</span><span class="p">:</span><span class="w"> </span><span class="l">forward-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">DOZZLE_ENABLE_ACTIONS</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">DOZZLE_NO_ANALYTICS</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.dozzle.rule=Host(`dozzle.zak.ee`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.dozzle.middlewares=test-compress@file,authelia&#34;</span><span class="w"> </span><span class="c">#启用Brotli压缩以及由Authelia提供单点登录</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># - &#34;traefik.http.services.dozzle.loadbalancer.server.port=8080&#34; #这句甚至都不需要</span><span class="w">
</span></span></span></code></pre></div><figure class="big">
		<img alt="Dozzle" height="1824" loading="lazy" src="/writing/2024/my-server-setup/dozzle.webp" title="Dozzle 主界面" width="2736">
		<figcaption>
			<p>Dozzle 主界面</p>
		</figcaption>
	</figure>
	<p>接下来说说Authelia。有些时候我们部署的一些程序带有web界面，但是应用本身没有提供账户登录系统，而我们又想为这些页面设置密码保护，通常的做法是在反代上设置HTTP basicAuth。这是一个方案，但很多时候登录的体验并不友好，比如有些密码管理器不能自动填充，而且这个方案不能“记住密码”，我希望可以登录后一个月都不用再登录。这个时候就需要一个专门管理用户登录的“门户”了，我选择的是Authelia，因为它真的非常轻量，平时占用内存也就20~30MB。类似能实现这类功能的有authentik和Keycloak等，但是它们都太重了。</p>
<p>用户登录基本流程如下，就拿前面的Dozzle来举例吧，Dozzle本身有自己的登录系统，但是也支持用第三方登录，如前面的环境变量已经设置了<code>DOZZLE_AUTH_PROVIDER: forward-proxy</code>。当用户访问<code>dozzle.zak.ee</code>的时候，Traefik会请求Authelia，将请求的信息包括cookie等提供给Authelia，因为是第一次登录，所以肯定没有有效的cookie，Authelia返回401状态，这个时候Traefik会告诉浏览器302跳转到Authelia提供的登录页面，用户成功登录后就获取到cookie，后续所有请求只要带上这个cookie就能认证通过了。cookie是整个域都有效的，后面再访问其他有Authelia保护的页面也可以直接访问，也就实现了单点登录。除此之外，Authelia这边也能为不同的地址设置不同的规则，有些页面可能仅密码登录安全性不够，还需要启用两步验证，Authelia也可以很方便地实现。</p>
<h2 id="那些自托管服务">那些自托管服务 <a href="#%e9%82%a3%e4%ba%9b%e8%87%aa%e6%89%98%e7%ae%a1%e6%9c%8d%e5%8a%a1" class="anchor tdln dimmer">#</a></h2><p>前面写了很多服务器的基础配置，现在再盘点下我究竟都部署了哪些程序。其实我有一个<a href="/uses/">Uses</a>页面，里面有简单列出我的自托管服务清单，这里还是稍微多写几句吧，先贴一下目前我的两台服务器运行的Docker容器的情况，资源占用仅供参考。</p>
<pre tabindex="0"><code># Aliyun
CONTAINER ID   NAME          CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O         PIDS
a37437483530   traefik       0.00%     52.82MiB / 1.835GiB   2.81%     0B / 0B           86.5MB / 11.5MB   9
40e86f627a2e   dozzle        0.00%     11.1MiB / 1.835GiB    0.59%     15MB / 14.8MB     33.1MB / 6.96MB   8
97c09b1fce41   pocketbase    0.00%     24.34MiB / 1.835GiB   1.30%     15.2MB / 4.34MB   152MB / 14MB      9
78de2abd2a4b   gobackup      0.00%     16.6MiB / 1.835GiB    0.88%     19.9MB / 324MB    1.88GB / 1.27GB   6
8a11b296eaa2   mc            8.76%     969.3MiB / 1.835GiB   51.60%    0B / 0B           1.63GB / 1.94GB   45
f1e1e11da5ae   vaultwarden   0.00%     24.55MiB / 1.835GiB   1.31%     17.6MB / 78.1MB   236MB / 29MB      14
30e0a285f2be   nginx         0.00%     1.82MiB / 1.835GiB    0.10%     18.8MB / 42.7MB   23.4MB / 2.7MB    3
67ceef5229cf   authelia      0.02%     24.43MiB / 1.835GiB   1.30%     22.4MB / 54.7MB   179MB / 42MB      8
7273cde82bc1   gitea         0.12%     111.4MiB / 1.835GiB   5.93%     15.3MB / 3.47MB   520MB / 118MB     15
90a1a8c1ee13   filebrowser   0.00%     13.91MiB / 1.835GiB   0.74%     15.1MB / 41.2MB   101MB / 5.06MB    8
c7bc9c5da0cf   yarr          0.00%     19.74MiB / 1.835GiB   1.05%     19.3MB / 1.05MB   207MB / 27.9MB    6
6f67fd32cd70   syncthing     0.01%     39.76MiB / 1.835GiB   2.12%     0B / 0B           203MB / 17.3MB    29
13c28da014c4   dufs          0.00%     388KiB / 1.835GiB     0.02%     16MB / 15.2MB     26.4MB / 635kB    3

# CloudCone
CONTAINER ID   NAME         CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O         PIDS
8490169bc6d6   alps         0.00%     13.46MiB / 472.3MiB   2.85%     304kB / 479kB     21.1MB / 2.1MB    10
3aefd375f5bf   maddy        0.03%     14.99MiB / 472.3MiB   3.17%     3.58MB / 5.8MB    32.5MB / 2.27MB   7
684903dc1743   gotosocial   0.00%     65.67MiB / 472.3MiB   13.91%    47.1MB / 18.3MB   458MB / 239MB     9
0a0049dbf231   caddy        0.05%     29.78MiB / 472.3MiB   6.30%     0B / 0B           101MB / 2.64MB    12
</code></pre><p>阿里云服务器上部署的服务如下，这些程序正常运行占的物理内存+swap接近于VPS的物理内存容量：</p>
<ol>
<li><strong>Minecraft服务器</strong>：这个就不用多说了，是整个服务器上最占资源的程序。没办法，傲慢的Java程序，给多少内存它都能吃完，一启动它就有很多内存被挤进swap，我限制了JVM最多用1G，玩得不凶还是够用的。使用的镜像是<code>itzg/minecraft-server:java21-alpine</code>，直接运行在host网络。</li>
<li><a href="https://github.com/dani-garcia/vaultwarden" target="_blank" rel="noopener"><strong>Vaultwarden</strong></a>：Bitwarden密码管理器的第三方服务端实现，是用Rust写的，比官方的Bitwarden服务端省资源，对个人服务器来说要友好得多。</li>
<li><a href="https://syncthing.net/" target="_blank" rel="noopener"><strong>Syncthing</strong></a>：一个P2P文件同步工具，之前有写过文章<a href="/writing/2023/syncthing/">介绍</a>。</li>
<li><a href="https://filebrowser.org/" target="_blank" rel="noopener"><strong>File Browser</strong></a>：一个Web文件管理器，使用Go编写。我用它管理服务器上的文件，同时也作为网盘使用。因为我的博客文章源文件是用Syncthing同步到服务器，所以也可以用它来临时编辑或发表新的博客文章。</li>
<li><a href="https://github.com/sigoden/dufs" target="_blank" rel="noopener"><strong>dufs</strong></a>：一个极简的文件列表程序，使用Rust编写。我用它来host我的<a href="https://public.zak.ee/" target="_blank" rel="noopener">公共文件目录</a>。它也可以作为WebDAV服务器来使用，可与File Browser互补，不过我暂时没这个需求。</li>
<li><a href="https://gitea.io/" target="_blank" rel="noopener"><strong>Gitea</strong></a>：自托管Git服务，使用Go编写。我用它存一些代码，并且会镜像一些GitHub上的仓库，方便访问。</li>
<li><a href="https://github.com/nkanaev/yarr" target="_blank" rel="noopener"><strong>yarr!</strong></a>：一个简单的RSS阅读器，使用Go编写。它没有花里胡哨的功能，但是该有的功能都有，完美符合我的需求。</li>
<li><a href="https://pocketbase.io/" target="_blank" rel="noopener"><strong>PocketBase</strong></a>：现正驱动博客的评论功能，详见<a href="/writing/2023/pocketbase-comment/">这篇文章</a>。</li>
</ol>
<hr>
<p>CloudCone服务器上部署的服务比较少，就没有用Traefik，直接上Caddy够用了。盘点如下：</p>
<ol>
<li><a href="https://maddy.email/" target="_blank" rel="noopener"><strong>maddy</strong></a>：自建域名邮箱，也是Go写的。在用这个程序之前我一直以为自建域名邮箱系统非常吃配置。我的域名邮箱主要是给各种程序发送邮件通知用的，自己基本不用它来收发邮件。</li>
<li><a href="https://git.sr.ht/~migadu/alps" target="_blank" rel="noopener"><strong>alps</strong></a>：Web邮件客户端，后端也是Go编写，只是偶尔监看是否有邮件弹回，用这个就够了。</li>
<li><a href="https://gotosocial.org/" target="_blank" rel="noopener"><strong>GoToSocial</strong></a>：一个ActivityPub协议实现，正如它的名字，也是用Go写的，兼容Mastodon API，但是比Mastodon轻量得多。之前有段时间很多人都抛弃Twitter投入Fediverse的怀抱，也就是在那个时候我接触到了Fediverse，然后就发现了除Mastodon之外的Pleroma、Soapbox、GoToSocial等程序。然而实际部署了GoToSocial之后我就没有发过什么动态，也就关注了几个感兴趣的账号，偶尔看看。</li>
</ol>
<p>可以发现，我选择自部署程序一个最重要的因素就是它到底占多少内存，所以我爱死了Rust和Go写的程序。内存确实是没办法来虚的，不够就是不够，不得不说部署很多程序然后把服务器内存都占满，真的很有满足感。</p>
<h2 id="写在最后">写在最后 <a href="#%e5%86%99%e5%9c%a8%e6%9c%80%e5%90%8e" class="anchor tdln dimmer">#</a></h2><p>“数字基建”这个词很大，这里讨论的仅仅是一个方面的东西。我们活在互联网时代，与其依赖各种SaaS服务，倒不如将开源世界的资源为我所用，自己来host一些服务，把数据牢牢掌握在自己手里，也让自己网上冲浪更加舒适。希望以上的分享可以为你提供一些启发，囿于篇幅我没有细写配置，毕竟，自己折腾也是一种乐趣。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Keep it Simple and Stupid，保持简单和傻瓜。&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://gitlab.alpinelinux.org/alpine/aports/-/issues/15506" target="_blank" rel="noopener">https://gitlab.alpinelinux.org/alpine/aports/-/issues/15506</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content></entry><entry><title type="html">用PocketBase实现网站评论</title><link href="https://zak.ee/writing/2023/pocketbase-comment/" rel="alternate" type="text/html"/><id>https://zak.ee/writing/2023/pocketbase-comment/</id><published>2023-06-15T21:09:42+08:00</published><updated>2023-10-03T15:46:40+08:00</updated><summary>自从换用静态博客系统以来，我的就在折腾评论系统的道路上一直没有停过。这篇文章就简单记录下我折腾博客评论系统的历程以及我最近的折腾成果——使用PocketBase来驱动评论。由于重点只是我自己的折腾记录，所以不要指望能有什么干货就是了。</summary><content type="html"><![CDATA[<p>自从换用静态博客系统以来，我的就在折腾评论系统的道路上一直没有停过。这篇文章就简单记录下我折腾博客评论系统的历程以及我最近的折腾成果——使用PocketBase来驱动评论。由于重点只是我自己的折腾记录，所以不要指望能有什么干货就是了。</p>
<h2 id="背景">背景 <a href="#%e8%83%8c%e6%99%af" class="anchor tdln dimmer">#</a></h2><p>最早用的是Valine<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>——所谓的“无后端评论系统”，后来事实证明“无后端”并不是一个好主意。自从Valine暴露出一些XSS安全问题和隐私泄露问题后，我就在积极寻找各类替代品。</p>
<p>首先我排除了各种商业服务，投入到了self-hosted的怀抱。不为别的，就是说数据一定要掌握在咱自己手中。<a href="https://lisakov.com/projects/open-source-comments/" target="_blank" rel="noopener">这个网站</a>收录了几十个开源自托管的评论系统，我还真的一个一个地过了一遍，有些是本地搭了个试了下，有的只是阅读了下介绍或者文档，然而，完全符合我心意的一个都没有。我的需求很简单，就是要类似WordPress那样的评论，不用登录，任何人都可以发送评论。另外我很喜欢WordPress评论系统有一个“网址”的栏位，评论者可以在这里留下自己博客的网址，方便回访，虽然有时候会有人发广告链接，不过我还是觉得这个利大于弊，遗憾是的很多评论系统没有这个栏位。最后，最重要的一点，一定要简洁轻量，不需要那些花里胡哨的功能，加载一定要快，有些评论系统动辄几百KB的js脚本就离谱。最终比来比去，我觉得isso是最符合我需求的，就把Valine换成了isso，就这样凑合用着了。</p>
<p>其实早多年以前，我把博客从WordPress转到Hugo的时候就萌生了自己做评论系统的念头，可是受限于技术水平，一直都没有实现。曾多次尝试，有的是写了个开头就写不下去了，有的是实现了但又觉得写得太过丑陋及粗糙，没法用。感觉自己不懂的还是太多了，尤其是后端方面。评论系统说简单真的非常简单，说复杂，需要注意的地方确实有很多。</p>
<h2 id="遇见pocketbase">遇见PocketBase <a href="#%e9%81%87%e8%a7%81pocketbase" class="anchor tdln dimmer">#</a></h2><p>大概是去年年底的时候看到了<a href="https://limboy.me/posts/my-blog-system/" target="_blank" rel="noopener">一篇博客</a>，很受启发。从博主这里了解到了PocketBase<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>这个神器。这个简直强无敌，Go写的，编译成单一可执行文件，就实现了数据库CRUD、实时数据订阅、用户管理、文件上传等功能，还有一个颜值很高的管理后台，而更让我震惊的是这个项目竟然是一个人完成的。不得不说作者的审美真的很在线，技术品味也很高。PocketBase使用内嵌的SQLite数据库，对于中小型的应用，性能完全是hold得住的，更不用说我这种没有什么访问量的博客的评论系统了。</p>
<p>不过只有PocketBase本体还不足以作为评论的后端直接与前端交互。由于来自前端的请求默认都不可信任，而且我们要做不用登录的评论系统，这样直接让前端操作数据源风险太大了。PocketBase虽然目前支持设置一些API访问规则，但是现在还不支持把某个字段设置为只有认证通过才返回到响应中。我指的就是“电子邮件地址”这个字段，评论的电邮地址属于评论者的隐私，主要是用来显示Gravatar头像的，前端显示头像只需要电邮地址的md5就行了，原始的邮箱是绝对不能出现在API响应中的。由此可见做一个运行在PockerBase和浏览器之间的服务端非常有必要。而且这个服务端不光起到过滤敏感信息的作用，还可以实现一些其他功能，比如发送电邮通知、Webhook等。</p>
<p>要实现这个服务端，最容易想到的就是各种Serverless云函数了。受到那篇博客的影响，我也很想尝试用Deno来实现。早就听说过Deno的大名，据说是Node.js创始人Ryan Dahl受不了Node.js的种种缺陷重新打造的JavaScript运行时。Deno比较吸引我的地方是项目没有<code>node_modules</code>文件夹，包都通过url的形式引入，非常像在浏览器中引入js脚本那样。一个Deno脚本拷贝到任意主机上，只要安装了Deno环境，就可以直接运行，不像Node那样要还需要一个<code>package.json</code>，还要先<code>npm install</code>。Deno有官方的Serverless平台“Deno Deploy<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>”，可以非常方便地部署Deno项目。</p>
<h2 id="开始动手">开始动手 <a href="#%e5%bc%80%e5%a7%8b%e5%8a%a8%e6%89%8b" class="anchor tdln dimmer">#</a></h2><p>我的需求非常简单，就是只需要处理一个GET请求，一个POST请求，一个用于获取某个页面的评论，一个用于创建新评论。所以也用不上什么框架，直接就用Deno的<code>std/http</code>标准库了，对着文档copy，还是很简单的。然后由于PocketBase有js-sdk<sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>，所以与之连接非常简单，直接导入ES模块（也就是SDK中的<code>dist/pocketbase.es.mjs</code>这个文件），然后想要什么操作，对着文档copy就行。至此，前端请求的处理和后端PocketBase的操作都已打通，现在重点来了，handler函数里的“业务逻辑”要怎么写？也就是在这里我才深刻明白我真的很菜。花费我时间最多的就是嵌套评论的实现了。首先我在PocketBase中的创建评论的collection，这个基本上参考了isso的数据表字段，也是为了方便我迁移评论数据过来。然后我查了很多教程，终于是写出来了，无非就是把一个扁平数组转成树形对象，明白了原理其实也没有很难。</p>
<p>到这里，我的服务端就能实现显示评论和发评论的基本功能了，然后就是客户端部分了。本来想直接用原生js来做，毕竟我的网站非常轻量，我不想因为一个评论功能就引入巨大的js脚本。之前我就听说过Svelte这个框架，它是把组件编译成原生js，编译时就实现了Reactive，所以不需要把自己也打包进去，于是当组件不是很多的时候打包的js体积会非常小。在看过了官方教程<sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup>之后更是爱上了这个框架，这语法是真的简洁啊，写起来太符合直觉了。Svelte这么舒服，还写什么原生js，MVVM不香吗？基本功能实现后我就想加上Markdown支持，然后我就找到了这个Snarkdown<sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup>这个库，能实现基础的Markdown解析，而体积竟然只有3KB大小。然后由于评论会渲染成html，所以要考虑XSS问题，就用了insane<sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup>这个库来过滤XSS。insane用的是白名单机制，所以体积非常小，才几KB。最终，我的评论模块脚本打包也只有20多KB，gzip后只有10KB左右。</p>
<p>在实现了基本功能跟之后我就迫不及待地把评论数据迁移到PocketBase。迁移我用了个笨办法，就是把isso的sqlite数据库导出成csv格式，然后用Excel手改😅。因为一共也就170来条，而且基本上需要修改的就是两个地方：一是评论的时间格式，isso存的是unix时间戳，PocketBase要用ISO 8601格式，这个用Excel函数即可，方法网上搜索就有。二是评论id要从连续数字改成10位随机字符串。改好后用DB Browser for SQLite<sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup>打开PocketBase对应的表，直接全部复制进去。然后在服务器部署好PocketBase，Deno服务端部署到Deno Deploy，网站文章页面引入打包好的js脚本就能实现评论了。</p>
<p>后面又陆陆续续完善了一些功能，比如Deno这边加了个Webhook通知，我用这个来通知自己有新评论。前端做了一点微小的改进。目前这部分的代码都开源在<a href="https://github.com/Track3/pocketbase-comment" target="_blank" rel="noopener">GitHub</a>上了，我自建的<a href="https://git.zak.ee/Track3/pocketbase-comment" target="_blank" rel="noopener">Gitea</a>上也镜像了一份，方便国内访问。这部分的代码请留意需要切换到<code>legacy</code>分支。</p>
<p>你可能会问为什么这部分叫legacy？是的，后面我又重构了后端。</p>
<hr>
<p>PocketBase可以作为一个Go框架来使用，之前由于我不会Go，所以就没有考虑过这种用法。大概几个月之前，我一时兴起看了下Go的官方教程<sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup>，突然对Go很感兴趣。看完了教程的基础部分就开始研究PocketBase用作框架了。主要还是考虑到作为一个简单的评论系统，还要同时运行一个PocketBase实例和一个Deno server，实在是太过麻烦，能把PocketBase和Deno server合在一起就最好不过了。PocketBase的文档在这方面还不够完善，这对于我这种Go新手来说真的非常痛苦。由于PocketBase使用了Echo这个框架，所以很多地方要看Echo的文档，还好Echo的文档有很多例子可供参考，终于东拼西凑把它写出来了。由于之前已经用Deno实现过一遍了，所以用Go重写的时候思路都已经有了，只是要研究下怎么把对应的逻辑用Go语法写出来。</p>
<p>这一百来行的Go代码，我真的写了好久，无论如何，我写出来了，而且它能用，其他什么的都不重要了。虽然很多功能都没有，因为是自己写的，所以有不完善的地方也都会包容，就不像用别人写的评论系统那样挑挑剔剔地了。</p>
<p>目前已经实现的功能有：</p>
<ul>
<li>可以显示评论和发评论（废话）;</li>
<li>可使用PocketBase后台界面管理评论数据（废话）；</li>
<li>支持简单的Markdown语法，可实时预览；</li>
<li>新评论邮件通知管理员；</li>
<li>没有了……</li>
</ul>
<h2 id="如何部署使用">如何部署使用 <a href="#%e5%a6%82%e4%bd%95%e9%83%a8%e7%bd%b2%e4%bd%bf%e7%94%a8" class="anchor tdln dimmer">#</a></h2><p>目前这个项目还只是自用，暂时不想去维护一个开源项目，而且很多地方都像个半成品，截至这篇文章发布时，前端还不带样式，所以要自行手写CSS，当然可参考我网站的样式<sup id="fnref:10"><a href="#fn:10" class="footnote-ref" role="doc-noteref">10</a></sup>。无论如何，这里还是简单写下如何部署使用吧。</p>
<p>目前Docker镜像发布到Docker Hub上了，直接拉过来就可以使用。可使用以下命令启动，注意修改命令中的数据存储路径，然后可以访问<code>http://&lt;hostname or ip&gt;:8090/_</code>打开PocketBase的后台管理页面。关于PocketBase的更多使用方法请参考<a href="https://pocketbase.io/docs/" target="_blank" rel="noopener">官方文档</a>。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">docker run -d <span class="se">\
</span></span></span><span class="line"><span class="cl">  --name<span class="o">=</span>pocketbase <span class="se">\
</span></span></span><span class="line"><span class="cl">  -p 8090:8090 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -v /path/to/data:/pb_data <span class="se">\
</span></span></span><span class="line"><span class="cl">  --restart unless-stopped <span class="se">\
</span></span></span><span class="line"><span class="cl">  track3/pocketbase-comment:latest
</span></span></code></pre></div><p>如果不用Docker，就要自己编译了，这个请参考PocketBase README页面的<a href="https://github.com/pocketbase/pocketbase#running-and-building" target="_blank" rel="noopener">Running and building</a>部分，然后运行方法参考PocketBase的文档。</p>
<p>在PocketBase管理页面，打开设置，然后选择“Import collections”，然后导入<a href="comments_schema.json">这段JSON</a>，PocketBase就配置好了。</p>
<figure class="big">
		<img alt="PocketBase Import collections" height="741" loading="lazy" src="/writing/2023/pocketbase-comment/img039.webp" width="1314">
		
	</figure>
	<p>然后前端只用在需要评论的地方加上以下html代码。当然生产环境建议配置HTTPS以及使用反向代理设置访问速率限制和CORS header等等，这里不作讨论。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;comments&#34;</span> <span class="na">data-url</span><span class="o">=</span><span class="s">&#34;http://&lt;hostname or ip&gt;:8090/api/comment&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;module&#34;</span> <span class="na">crossorigin</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;http://&lt;hostname or ip&gt;:8090/comment.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>如果看到下面这样的界面，不要怀疑是哪里出了问题，正常就是没有样式的，不信可以发个评论测试一下。</p>
<figure>
		<img alt="demo" height="449" loading="lazy" src="/writing/2023/pocketbase-comment/img040.webp" width="493">
		
	</figure>
	<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://valine.js.org/" target="_blank" rel="noopener">介绍 | Valine 一款快速、简洁且高效的无后端评论系统。</a>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p><a href="https://pocketbase.io/" target="_blank" rel="noopener">PocketBase - Open Source backend in 1 file</a>&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p><a href="https://deno.com/deploy" target="_blank" rel="noopener">Deno Deploy</a>&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p><a href="https://github.com/pocketbase/js-sdk" target="_blank" rel="noopener">pocketbase/js-sdk: PocketBase JavaScript SDK (github.com)</a>&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p><a href="https://svelte.dev/tutorial/basics" target="_blank" rel="noopener">Introduction / Basics • Svelte Tutorial</a>&#160;<a href="#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p><a href="https://github.com/developit/snarkdown" target="_blank" rel="noopener">developit/snarkdown: A snarky 1kb Markdown parser written in JavaScript (github.com)</a>&#160;<a href="#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:7">
<p><a href="https://github.com/bevacqua/insane" target="_blank" rel="noopener">bevacqua/insane: Lean and configurable whitelist-oriented HTML sanitizer (github.com)</a>&#160;<a href="#fnref:7" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:8">
<p><a href="https://sqlitebrowser.org/" target="_blank" rel="noopener">DB Browser for SQLite (sqlitebrowser.org)</a>&#160;<a href="#fnref:8" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:9">
<p><a href="https://tour.go-zh.org/welcome/1" target="_blank" rel="noopener">Go 语言之旅 (go-zh.org)</a>&#160;<a href="#fnref:9" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:10">
<p>本站是完全开源的，当前版本的评论CSS源码可在<a href="https://github.com/Track3/blog/blob/fa29d322234de32b3f7fc6664d7c91f561bd1c0f/assets/scss/partials/_comments.scss" target="_blank" rel="noopener">GitHub</a>上查看&#160;<a href="#fnref:10" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content></entry><entry><title type="html">用Syncthing同步我的文件</title><link href="https://zak.ee/writing/2023/syncthing/" rel="alternate" type="text/html"/><id>https://zak.ee/writing/2023/syncthing/</id><published>2023-02-05T12:37:21+08:00</published><updated>2023-03-12T12:48:33+08:00</updated><summary>一直以来我都是用OneDrive跨设备同步文件，得益于和Windows系统的深度集成，OneDrive在两台Windows电脑互相同步的时候效果十分完美，可是一旦需要同步的设备不是Windows，就比较难受了。</summary><content type="html"><![CDATA[<p>一直以来我都是用OneDrive跨设备同步文件，得益于和Windows系统的深度集成，OneDrive在两台Windows电脑互相同步的时候效果十分完美，可是一旦需要同步的设备不是Windows，就比较难受了。</p>
<h2 id="onedrive的问题">OneDrive的问题 <a href="#onedrive%e7%9a%84%e9%97%ae%e9%a2%98" class="anchor tdln dimmer">#</a></h2><p>我的博客的Markdown源文件是放在OneDrive上的，平时写完文章，就运行服务器上的一个Shell脚本，大概流程如下图，首先用Rclone同步最新的文章数据到服务器上，然后触发一个Deno脚本，删除所有的草稿文件<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>，最后打包成压缩文件，丢进Caddy的root目录。这样一来Vercel就可以直接通过http下载文件后完成博客的部署了。</p>
<figure class="big">
		<img alt="原来的博客部署流程" height="629" loading="lazy" src="/writing/2023/syncthing/img038.webp" title="原来的博客部署流程" width="1739">
		<figcaption>
			<p>原来的博客部署流程</p>
		</figcaption>
	</figure>
	<p>OneDrive没有官方的Linux客户端，有第三方的客户端我也没有尝试过，一直用的都是<a href="https://rclone.org/" target="_blank" rel="noopener">Rclone</a>，而Rclone没有实时同步的功能。对于博客文章的源文件来说，如果你一个月都不写博客、不修改，那这个文件夹的内容就完全不会变，如果设置一个定时同步，就感觉没什么必要。而且，一般写好了一篇新博客，肯定是想马上就发布，所以还得手动触发，不会去等定时同步。所以以上流程虽不是太完美，我也没有觉得太麻烦。而OneDrive真正让我觉得非常不方便的就是其在Windows以外的平台编辑并同步Markdown文件的体验了。</p>
<p>首先说说Android端。官方客户端根本就没有Markdown编辑功能，只能查看渲染出来的效果（而且还不支持中文字体，全是框框）。除此之外，它似乎就没有文件夹同步的功能，我想要把OneDrive上的一个文件夹与Android文件系统中的一个文件夹保持同步都实现不了，还要借助第三方软件比如FolderSync、OneSync等才可以。</p>
<p>其次，我一直都希望有一个可以临时编辑博客文章的Web端。然而，OneDrive的网页版在国内已经无法正常访问。由于需要临时编辑博客文章的时候一般用的都不是自己常用的设备，这就意味着我们可能无法科学上网，因此这个需求就无法实现或者非常麻烦。</p>
<p>想想我当初把文章全部移到OneDrive上的一个重要原因就是想要在各个平台无缝编辑并同步，而这样的使用体验只能说离差强人意还差点。正好最近在寻找Logseq同步的方案，看到大家都推荐用Syncthing，就尝试了下，果断就换了。</p>
<h2 id="什么是syncthing">什么是Syncthing <a href="#%e4%bb%80%e4%b9%88%e6%98%afsyncthing" class="anchor tdln dimmer">#</a></h2><p><a href="https://syncthing.net/" target="_blank" rel="noopener">Syncthing</a>是一个开源免费跨平台的P2P文件同步工具。由于是基于P2P技术，所以它是去中心化的。所有的文件都不会存储在第三方系统中，而且设备与设备间的通讯都经过TLS加密，所以是非常私密且安全的。</p>
<p>请设想以下场景：我有一台Windows电脑和一台Android手机，连的是同一个WiFi，如果用OneDrive同步，我在电脑上修改了文件，文件需要先上传到OneDrive服务器，然后手机再从OneDrive服务器上下载。即使两台设备位于同一个局域网中，也需要像这样绕一个大圈。如果改用Syncthing同步，则电脑与手机就能直接走局域网同步，不仅同步速度更快，更摆脱了对第三方服务依赖。</p>
<p>当电脑和手机不在一个局域网时候，在发现服务器的协助下，它们会尝试广域网直连或者经中继服务器同步。发现服务器和中继服务器都可以自己部署，官方和社区都有贡献公共节点可用。使用公共的中继服务器同步的话速度会比较慢，Syncthing会不断尝试直连，直到连接上为止。另外，大家可能会担心同步经过中继服务器是否会有隐私泄露的风险，实际上，中继服务器传送的只是加密后的文件，任何第三方都没办法获取文件的真正内容，中继服务器只知道连接设备的ID以及其IP地址。</p>
<h2 id="使用syncthing">使用Syncthing <a href="#%e4%bd%bf%e7%94%a8syncthing" class="anchor tdln dimmer">#</a></h2><p>Syncthing本身是一个命令行软件，运行后浏览器访问<code>http://localhost:8384/</code>即可打开自带的web管理界面，一般推荐使用一个wrapper程序来方便程序的开机启动并在系统托盘添加一个图标。Windows上官方推荐的是<a href="https://github.com/canton7/SyncTrayzor" target="_blank" rel="noopener">SyncTrayzor</a>，不过我自己用的是<a href="https://github.com/Martchus/syncthingtray" target="_blank" rel="noopener">Syncthing Tray</a>。软件的安装使用都非常简单，官方文档很好而且网上也有很多教程，所以这里不再赘述。需要注意的是系统防火墙要允许Syncthing的连接，而且路由器要开启UPnP<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>。</p>
<p>Android端有官方app，直接下载安装，然后打开两个设备的web管理界面，在一个设备上添加另一个设备的ID，并选择要共享的文件夹，然后在另一台设备上选择同意，连接成功之后就可以同步了。文件是直接同步到设备的文件系统中，所以我可以自由使用各种软件来查看、编辑我的文件，不管是博客文章的Markdown，还是Logseq的图谱。</p>
<p>如果我们只需要简单的文件同步，这样其实就足够了。不过有一个问题：想要同步，两台设备必须同时都在线。所以为了随时随地都能够同步 ，我们需要有一台设备能一直开着Syncthing，没错，我们在云服务器或者NAS上也跑一个Syncthing就行了。可直接用Docker部署，<code>docker-compose.yml</code>配置如下，更详细的指引可参考：<a href="https://github.com/syncthing/syncthing/blob/main/README-Docker.md" target="_blank" rel="noopener">Docker Container for Syncthing</a>。</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">syncthing</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">syncthing/syncthing</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">syncthing</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">my-syncthing</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PUID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">PGID=1000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/wherever/st-sync:/var/syncthing</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">network_mode</span><span class="p">:</span><span class="w"> </span><span class="l">host</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span></code></pre></div><p>在云服务器上部署Syncthing后，云服务器同时也充当一个备份节点的角色，可以开启版本控制。另外我们还可以结合<a href="https://filebrowser.org/" target="_blank" rel="noopener">File Browser</a>这样的Web文件管理器实现一个真正意义上的自托管同步云盘。前面提到我一直希望终于有一个web界面来临时编辑博客，而OneDrive又有种种不方便，现在，我在File Browser的根目录中建一个子目录，作为Syncthing的同步文件夹，就能通过File Browser网页端直接编辑Markdown文件，然后实时同步了。</p>
<p>以下视频展示了在File Browser中编辑页面后保存、Syncthing检测到文件更改后完成同步、Hugo检测到本地文件变更后重新渲染页面，并触发浏览器刷新的过程。服务器与本地是通过广域网TCP直连的，这个流程用了十多秒，不算快，不过也没有必要很快。</p>
<p><video controls src="syncthing_test.mp4" loading="lazy" width="1918" height="1028"></video></p>
<p>有了Syncthing与File Browser这套组合，我的博客部署流程又能优化一下了。现在不管任何时候Vercel需要部署博客都可以通过File Browser的api直接取得<code>content.tar.gz</code>打包文件，再也不用操心文章数据是否已经先同步好了。这个下载链接是不公开的所以不用跑那个删除草稿的脚本，而为了公开没有草稿版本的<code>content.tar.gz</code>，目前我还是单独设置了每天凌晨3点定时更新，这次才会删除草稿。大家可以访问<a href="https://public.zak.ee/" target="_blank" rel="noopener">我的公共文件目录</a>，下载博客文章的源码。</p>
<p>这篇文章只是简单分享下我对Syncthing的用法以及使用体验，更多功能大家可自行探索，个人认为Syncthing是关于个人文件同步方面很好的解决方案了，还是非常值得一试的。</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>脚本已经分享在<a href="https://gist.github.com/Track3/a44e438e6618c7830c40c1399c76e1be" target="_blank" rel="noopener">GitHub Gist</a>上，写这个脚本主要是因为我希望把博客的Markdown源文件分享出来，但又不想让别人看到我的草稿。😅&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Universal Plug and Play（通用即插即用），一般路由器都会默认开启。当本地网络需要和公网建立对等访问的时候，可以借助UPnP自动映射端口出去。&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content></entry></feed>