<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Certificate on Sisy's Blog</title><link>https://blog.sisy.cc/tags/certificate/</link><description>Recent content in Certificate on Sisy's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Fri, 17 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.sisy.cc/tags/certificate/index.xml" rel="self" type="application/rss+xml"/><item><title>自部署 Moe-Counter</title><link>https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/</link><pubDate>Fri, 17 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/</guid><description>&lt;img src="https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/cover.png" alt="Featured image of post 自部署 Moe-Counter" /&gt;&lt;!-- markdownlint-disable MD033 --&gt;
&lt;h2 id="前言"&gt;前言
&lt;/h2&gt;&lt;p&gt;起因是看到 osu! 玩家 &lt;a class="link" href="https://osu.ppy.sh/users/1856463" target="_blank" rel="noopener"
 &gt;IamKwaN&lt;/a&gt; 的 profile 上有一个很像 shields.io 风格的访问计数器，让我想起非常多人都喜欢在自己的博客或者 GitHub Profile 上挂一个类似的 hit counter 来统计访问量。&lt;/p&gt;
&lt;p&gt;&lt;img alt="IamKwaN’s profile" class="gallery-image" data-flex-basis="640px" data-flex-grow="267" height="197" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/bitly.png" width="526"&gt;&lt;/p&gt;
&lt;p&gt;他这个计数器点击之后会跳转到一个 &lt;code&gt;bit.ly&lt;/code&gt; 托管的短链接（估计是为了隐藏真实链接防止屏蔽之类的），最终重定向落在一个叫 &lt;a class="link" href="https://s05.flagcounter.com/more/T31K/" target="_blank" rel="noopener"
 &gt;Flag Counter&lt;/a&gt; 的服务上，显示他这个 profile 关于访问次数和访客所属地区分布的详细信息。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Flag Counter" class="gallery-image" data-flex-basis="205px" data-flex-grow="85" height="1146" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/flag-counter.png" srcset="https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/flag-counter_hu_f4ac3c2a35b33450.png 800w, https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/flag-counter.png 981w" width="981"&gt;&lt;/p&gt;
&lt;p&gt;我还是蛮喜欢这个小巧的 mini counter 的，比起他们普通的 flag counter 要美观很多。这个服务免费、很完善，且似乎自带一定的流量控制，短时间内重复访问不会多次计数————美中不足之处就是这个服务不是开源的（）。于是我就自己注册账号生成了一个唯一链接，结果又发现一个头疼的问题：&lt;/p&gt;
&lt;p&gt;&lt;img alt="My Pageviews Case" class="gallery-image" data-flex-basis="723px" data-flex-grow="301" height="77" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/my-pageviews.png" width="232"&gt;&lt;/p&gt;
&lt;p&gt;访问次数很少的时候，这个图片的长度居然与访问次数 6 位数时看起来差不多长！这样右边就有一坨空白，而且官方给的 url 链接配置参数里也没有什么能控制图片宽度的选项，感觉有点逆天。。。&lt;/p&gt;
&lt;p&gt;于是我试图找到类似的 website hit counter 的开源替代品，结果回想起来很多 booru 站点甚至包括 Steam，都有类似 n 个二次元人物手举数字牌子的访问计数器，最后终于找到了 &lt;a class="link" href="https://github.com/journey-ad/Moe-Counter" target="_blank" rel="noopener"
 &gt;Moe-Counter&lt;/a&gt; 这个项目（没想到居然是国人写的项目）。&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://github.com/journey-ad/Moe-Counter" target="_blank" rel="noopener"
 &gt;Moe-Counter&lt;/a&gt; 是一个支持多种主题的萌萌计数器，支持直接用他们自己的服务器和已部署到公网的 WebUI 生成一个唯一 id 的链接来用，另外 README 里也写了本身支持 Docker 部署。&lt;/p&gt;
&lt;p&gt;看起来搭建不算复杂，但当我真正想把它跑在自己的服务器上、对公网开放访问、挂上自定义域名、提供 HTTPS 安全连接的时候，才发现从&amp;quot;能跑&amp;quot;到&amp;quot;能用&amp;quot;之间还有不少坑要踩。不过正好能顺便学一下用 Nginx 反代一个服务，以及用 Certimate 来自动化管理 HTTPS 证书，另外似乎还有一些 Cloudflare DNS 配置的细节也需要注意，还能练练 Docker compose ———— 嗯嗯。。。不错的实践机会。&lt;/p&gt;
&lt;p&gt;那么下面就开始吧！&lt;/p&gt;
&lt;h2 id="预备步骤"&gt;预备步骤
&lt;/h2&gt;&lt;p&gt;先拉下来熟悉一下结构。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone https://github.com/journey-ad/Moe-Counter.git
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; Moe-Counter
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ll
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="项目结构"&gt;项目结构
&lt;/h3&gt;&lt;p&gt;项目结构比较清晰，几个关键目录和文件：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;路径&lt;/th&gt;
 &lt;th&gt;用途&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;views/&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;页面模板（Pug），首页展示逻辑在这里&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;assets/&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;静态资源，包括各种主题的数字图片&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;utils/&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;工具函数，主题加载逻辑&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;db/&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;数据库适配层（SQLite / MongoDB）&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;index.js&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;入口文件，路由定义&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;.env.example&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;环境变量配置模板&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;Dockerfile&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Docker 构建文件&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Docker Compose 配置&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;看样子还能自己加主题，只要在 &lt;code&gt;assets/&lt;/code&gt; 里放一套 1-9 的数字图片，然后在 &lt;code&gt;views/index.pug&lt;/code&gt; 里加个选项提供到 WebUI 首页上就行了。&lt;/p&gt;
&lt;p&gt;现代的 Docker Compose 已经（详见&lt;a class="link" href="https://docs.docker.com/compose/intro/compose-application-model/#the-compose-file" target="_blank" rel="noopener"
 &gt;这篇&lt;/a&gt;和&lt;a class="link" href="https://docs.docker.com/reference/compose-file/" target="_blank" rel="noopener"
 &gt;这篇&lt;/a&gt;）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;弃用了 &lt;code&gt;docker-compose.yml&lt;/code&gt; 文件名，改使用 &lt;code&gt;compose.yaml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;compose 文件中也不推荐再使用 &lt;code&gt;version&lt;/code&gt; 字段了，直接写服务定义就行了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不过这个项目里还是用的一些旧标准，不过也不影响使用————虽然我还是把这两个都改掉了。&lt;/p&gt;
&lt;h3 id="配置一下"&gt;配置一下
&lt;/h3&gt;&lt;p&gt;env 先复制一份：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp .env.example .env
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;.env&lt;/code&gt; 文件的主要配置项：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;span class="lnt"&gt;9
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 这个 URL 是给生成的访问计数器图片链接用的，必须是公网可访问的 URL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# WebUI 中的各个示例计数器图片也都是基于这个 URL 拼接的&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 后面会用到 Cloudflare 反代，所以这里直接填最终访问的 URL 就行了&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;APP_SITE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://moe.sisy.cc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;APP_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;DB_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sqlite&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;DB_INTERVAL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;60&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="na"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;debug&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;它还支持 MongoDB，不过如果只是个人用，感觉 SQLite 完全够了。&lt;/p&gt;
&lt;p&gt;因为我考虑后续需要编辑项目内容（增加访问次数去重逻辑、添加自定义主题之类的），所以不能直接 &lt;code&gt;docker pull&lt;/code&gt; 官方镜像，而是要从源码本地构建。另外，他这个 &lt;code&gt;docker-compose.yml&lt;/code&gt; 的内容本来也唐完，因此 &lt;code&gt;compose.yaml&lt;/code&gt; 也得改，先看看原先的 &lt;code&gt;docker-compose.yml&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;3&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 已经不推荐写 version 了&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;moe-counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 得加一条 image 指定构建目标是本地镜像&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;3000:3000&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;./data:/app/data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 额，首先这不优雅，其次你不是有 .env 文件了么&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;APP_PORT=3000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;DB_TYPE=sqlite&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;改成下面这样：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;moe-counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;moe-counter:local&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;moe-counter&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;3000:3000&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 注意这里，后面用 Nginx 反代后会更优雅一些&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;./data:/app/data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;.env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这样就差不多能起了。&lt;/p&gt;
&lt;h2 id="实现公网可达"&gt;实现公网可达
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker logs -f moe-counter &lt;span class="c1"&gt;# 也可以不看日志，这个服务比较简单。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://localhost:3000 &lt;span class="c1"&gt;# 应该返回 HTTP/1.1 200 OK&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl http://localhost:3000 &lt;span class="c1"&gt;# 或者用 Termius 的 Port Forwarding 直接在浏览器看效果也行&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;此时不仅能在 VPS 本地 curl 访问，而且其实已经可以成功从远端使用 &lt;code&gt;http://&amp;lt;服务器 IP&amp;gt;:3000&lt;/code&gt; 访问到 WebUI 了，只不过还没有域名和 HTTPS，比较丑陋。&lt;/p&gt;
&lt;p&gt;而且直接用 3000 端口将服务暴露出去也不太漂亮，后面用 Nginx 反代一下，把 80/443 的流量转到 3000，这样就能在单端口开多个服务，非常优雅。&lt;/p&gt;
&lt;p&gt;不过在 Nginx 配置之前，先把这个服务的 3000 端口绑定在回环地址上，这样外网就无法直接访问了，所有流量都得走 Nginx 反代进来，也更安全。调整一下 compose 配置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;moe-counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;127.0.0.1:3000:3000&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;依旧验证：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:3000 &lt;span class="c1"&gt;# return HTTP/1.1 200 OK&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h2 id="实现域名解析"&gt;实现域名解析
&lt;/h2&gt;&lt;p&gt;既然已经能从公网访问了，下一步就是把域名指过来。&lt;/p&gt;
&lt;h3 id="cloudflare-dns-配置"&gt;Cloudflare DNS 配置
&lt;/h3&gt;&lt;p&gt;DNS 服务用的是 Cloudflare，所以在 Cloudflare 里添加一条 A 记录：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;类型&lt;/th&gt;
 &lt;th&gt;名称&lt;/th&gt;
 &lt;th&gt;内容&lt;/th&gt;
 &lt;th&gt;代理状态&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;A&lt;/td&gt;
 &lt;td&gt;moe&lt;/td&gt;
 &lt;td&gt;114.514.91.69&lt;/td&gt;
 &lt;td&gt;仅 DNS&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;访问 &lt;code&gt;http://moe.sisy.cc&lt;/code&gt;，发现无法连接。这时反应过来浏览器访问域名时默认连的是 80 端口，而我的服务跑在 3000。自建服务跑在非标端口时，中间必须有一层反代来转发流量。&lt;/p&gt;
&lt;p&gt;不过 Cloudflare DNS 侧已经无需再动，接下来把 Nginx 反代调通就完成域名解析了。&lt;/p&gt;
&lt;h3 id="nginx-反代"&gt;Nginx 反代
&lt;/h3&gt;&lt;p&gt;解决端口问题的标准做法是用 Nginx 做反向代理：浏览器访问 80/443 → Nginx 接住 → 转发到本地 3000 → Moe-Counter 处理 → 原路返回。由于暂时没有证书，我们的思路就是先监听 80，确认反代没问题后再配置证书，然后把 Nginx 配置也同步升级到 443 与 HTTPS 支持。&lt;/p&gt;
&lt;p&gt;先安装一下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sudo apt install -y nginx
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;新建 &lt;code&gt;/etc/nginx/sites-available/moe-counter&lt;/code&gt; 来放本站的 Nginx 配置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;moe.sisy.cc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s"&gt;.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;upgrade&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;启用并重载：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ln -s /etc/nginx/sites-available/moe-counter /etc/nginx/sites-enabled/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nginx -t &lt;span class="c1"&gt;# 测试配置正确&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl reload nginx &lt;span class="c1"&gt;# 热重载新配置&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;此时浏览器访问 &lt;code&gt;http://moe.sisy.cc&lt;/code&gt; 就能返回 WebUI了，HTTP 就通了，接着往下做 HTTPS。&lt;/p&gt;
&lt;h2 id="https-证书与-acme-自动化"&gt;HTTPS: 证书与 ACME 自动化
&lt;/h2&gt;&lt;p&gt;&lt;img alt="Certimate Workflow" class="gallery-image" data-flex-basis="246px" data-flex-grow="102" height="806" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/certimate-workflow.png" srcset="https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/certimate-workflow_hu_ef745fdd8656b323.png 800w, https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/certimate-workflow.png 827w" width="827"&gt;&lt;/p&gt;
&lt;p&gt;HTTPS 这一步，感觉真是懒得渐进式优化了，直接上 ACME 自动化证书维护吧。横向对比了一圈，最终还是决定使用 &lt;a class="link" href="https://github.com/usual2970/certimate" target="_blank" rel="noopener"
 &gt;Certimate&lt;/a&gt; 来管理证书。瞧瞧这优雅的工作流！（虽然我由于时间原因把工作流中的错误处理节点删了，等后续有机会再加回来吧）&lt;/p&gt;
&lt;h3 id="为什么不用-certbot"&gt;为什么不用 Certbot？
&lt;/h3&gt;&lt;p&gt;Certbot 是最常用的 Let&amp;rsquo;s Encrypt 客户端，单机单域名场景下非常省事——一条命令就能申请证书并自动改写 Nginx 配置。但我选择了 &lt;a class="link" href="https://github.com/usual2970/certimate" target="_blank" rel="noopener"
 &gt;Certimate&lt;/a&gt;，主要出于几个考虑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DNS-01 验证&lt;/strong&gt;：HTTP-01 验证需要 CA 直接访问 80 端口，而且没法签发通配符证书。DNS-01 只需要在 DNS 里加一条 TXT 记录，完全不走 HTTP 流量，还支持通配符证书。并且 Certimate 对 Cloudflare 的 DNS API 支持得很好，能自动增删改查 TXT 记录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;集中管理&lt;/strong&gt;：其他域名、服务器需要证书，Certimate 可以统一管理，不需要每台机器都装一遍 Certbot。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web 面板&lt;/strong&gt;：比起 Certbot 的命令行，有个面板看着更直观，工作流的触发记录、证书状态一目了然。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定时任务&lt;/strong&gt;：据可靠消息，Certimate 的定时任务应该比 Certbot 的更可靠一些。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然 Certbot 本身完全够用，如果你只有一个域名、一台服务器，用 Certbot 反而更简单。&lt;/p&gt;
&lt;h3 id="部署-certimate"&gt;部署 Certimate
&lt;/h3&gt;&lt;p&gt;Certimate 本身也是一个 Web 应用，为优雅性，自然是用 Docker 跑。因为它能 SSH 你的服务器、调用 Cloudflare API 改 DNS，权限很大，并且 README 里提供了初始 Admin 账户的邮箱和密码，所以&lt;strong&gt;绝对不能把管理面板暴露到公网&lt;/strong&gt;，否则被攻击就惨了。&lt;/p&gt;
&lt;p&gt;先拉一下 Docker Compose。注意最好不要用官网的一键起容器的脚本，因为它会把面板暴露在 8090 端口上，直接暴露在公网是非常危险的。我们要改成只绑定在回环地址上，需要拉下来之后先改 compose 文件，这样外网就无法访问了，所有流量都得走 SSH 隧道进来。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p ~/certimate &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/certimate &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -O https://raw.githubusercontent.com/certimate-go/certimate/refs/heads/main/docker/docker-compose.yml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv docker-compose.yml compose.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vim compose.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;改一下回环和版本号，其他默认：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# version: &amp;#34;3.0&amp;#34; 不要这个&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;certimate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;certimate/certimate:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;certimate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# - 8090:8090 不要这个&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;127.0.0.1:8090:8090&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/etc/timezone:/etc/timezone:ro&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;./data:/app/pb_data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;这里 &lt;code&gt;-p 127.0.0.1:8090:8090&lt;/code&gt; 保证面板只能从服务器本机访问。管理时，通过 SSH 端口转发就行了。保存之后直接起面板：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose up -d
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker logs -f certimate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:8090
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;我用的访问终端是 Termius，在 Port Forwarding 里加一条 Local 规则：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;字段&lt;/th&gt;
 &lt;th&gt;值&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Label&lt;/td&gt;
 &lt;td&gt;Certimate&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Local Port number&lt;/td&gt;
 &lt;td&gt;8090&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Local address&lt;/td&gt;
 &lt;td&gt;127.0.0.1&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Intermediate Host&lt;/td&gt;
 &lt;td&gt;Host Name&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Destination Host&lt;/td&gt;
 &lt;td&gt;127.0.0.1&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Destination Port&lt;/td&gt;
 &lt;td&gt;8090&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;保持 SSH 连接后，在本地浏览器打开 &lt;code&gt;http://127.0.0.1:8090&lt;/code&gt; 就能进入 Certimate 面板。断开 SSH 后面板自动不可达，安全性拉满。&lt;/p&gt;
&lt;h3 id="熟悉一下-certimate-面板"&gt;熟悉一下 Certimate 面板
&lt;/h3&gt;&lt;p&gt;进来之后第一件事当然是先改掉默认的 Admin 账户和密码了。设置里还有一些全局的 ACME 配置项，基本不太需要动，之后写新的工作流的时候，直接细调工作流内局部的配置就行。&lt;/p&gt;
&lt;p&gt;Certimate 的核心功能是&lt;strong&gt;工作流&lt;/strong&gt;，它把申请证书、部署证书、续期检查这些操作都抽象成一个个节点，然后串成一个流程。我们的思路就是写一个工作流来实现以下流程的自动化：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;申请证书 → 部署到 Nginx → 定时续期 (→ 检查和续签时发现错误 → 告警)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;至于其他页面，都可以视为工作流涉及的其他附属内容了。比如证书页面能看到每个证书的详细信息和状态，仪表盘页面能看到每次工作流执行的日志，授权凭证页面能看到工作流中涉及的各种第三方服务的授权信息（Cloudflare API Token、SSH 密钥之类的），等等。&lt;/p&gt;
&lt;h3 id="工作流各节点配置"&gt;工作流各节点配置
&lt;/h3&gt;&lt;p&gt;先从一个默认模板开始就好。&lt;/p&gt;
&lt;h4 id="开始节点"&gt;开始节点
&lt;/h4&gt;&lt;p&gt;&lt;img alt="Cron" class="gallery-image" data-flex-basis="332px" data-flex-grow="138" height="530" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/certimate-cron.png" width="735"&gt;&lt;/p&gt;
&lt;p&gt;工作流的触发方式有分手动和自动，那显然是自动啊！所以选一下定时触发。Certimate 的定时任务使用 Cron 表达式，先设置成 &lt;code&gt;30 1 * * *&lt;/code&gt;，每天凌晨 1:30 执行一次。至于为什么每天走一遍工作流，可以看下面的 &lt;a class="link" href="#tls-%e7%bb%ad%e6%9c%9f%e7%ad%96%e7%95%a5" &gt;TLS 续期策略&lt;/a&gt; 部分。&lt;/p&gt;
&lt;h4 id="申请节点"&gt;申请节点
&lt;/h4&gt;&lt;p&gt;&lt;img alt="Apply Certificate" class="gallery-image" data-flex-basis="157px" data-flex-grow="65" height="1060" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/certimate-application.png" width="696"&gt;&lt;/p&gt;
&lt;p&gt;首先配置证书颁发给哪些域：这里除了用通配符 &lt;code&gt;*.sisy.cc&lt;/code&gt; 之外，别忘了要加上根域 &lt;code&gt;sisy.cc&lt;/code&gt;。&lt;/p&gt;
&lt;h5 id="验证质询"&gt;验证质询
&lt;/h5&gt;&lt;p&gt;由于 HTTP-01 质询需要 CA 直接访问 80 端口，而且不支持通配符证书，所以质询方式一般选 DNS-01，配合 Cloudflare API 来自动添加 TXT 记录。因此 DNS 提供商选 Cloudflare。&lt;/p&gt;
&lt;p&gt;此时还需要添加一个 Cloudflare 颁发的 API Token 来授权 Certimate 通过这个 API Token 修改 Cloudflare 上的 DNS 记录。&lt;/p&gt;
&lt;h6 id="配置-cloudflare-api-token"&gt;配置 Cloudflare API Token
&lt;/h6&gt;&lt;p&gt;&lt;em&gt;主要还是参考这一篇文章：&lt;a class="link" href="https://developers.cloudflare.com/fundamentals/api/get-started/create-token/" target="_blank" rel="noopener"
 &gt;Cloudflare Fundamentals - Create API token&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;a class="link" href="https://dash.cloudflare.com/profile/api-tokens" target="_blank" rel="noopener"
 &gt;Cloudflare → My Profile → API Tokens → Create Token&lt;/a&gt; 里用 &amp;ldquo;Edit zone DNS&amp;rdquo; 模板创建：&lt;/p&gt;
&lt;p&gt;&lt;img alt="Create Cloudflare API Token" class="gallery-image" data-flex-basis="252px" data-flex-grow="105" height="1263" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/cloudflare-api-token-create.png" srcset="https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/cloudflare-api-token-create_hu_15aee6af2883de40.png 800w, https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/cloudflare-api-token-create.png 1330w" width="1330"&gt;&lt;/p&gt;
&lt;p&gt;权限保持默认的 &lt;code&gt;Edit&lt;/code&gt; 权限，因为我们需要 Certimate 自动增删改查 TXT 记录来完成 DNS-01 验证。唯一需要改的就是 &lt;code&gt;Zone Resources&lt;/code&gt;，选择 &lt;code&gt;Specific zone&lt;/code&gt; 并且只选 &lt;code&gt;sisy.cc&lt;/code&gt; 这个域，而不是 &lt;code&gt;All zones&lt;/code&gt;，否则一旦 Token 泄露了，攻击者就能更改 Cloudflare 账号下所有域名的 DNS 记录。最小权限原则是几乎零成本的安全加固。&lt;/p&gt;
&lt;p&gt;不过客户端 IP 地址筛选可以不动他，限制一台部署 Certimate 的主机访问的确可以，但没什么必要。TTL 也不需要额外修改，此时点击 &lt;code&gt;继续以显示摘要&lt;/code&gt; 按钮，确认权限和资源范围都正确后，点击 &lt;code&gt;创建令牌&lt;/code&gt;，就能拿到这个 API Token 的值了。把它复制下来，填到 Certimate 的授权凭证里：&lt;/p&gt;
&lt;p&gt;&lt;img alt="Add Cloudflare API Token" class="gallery-image" data-flex-basis="222px" data-flex-grow="92" height="1380" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/cloudflare-api-token-add.png" srcset="https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/cloudflare-api-token-add_hu_96211188983e9778.png 800w, https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/cloudflare-api-token-add.png 1281w" width="1281"&gt;&lt;/p&gt;
&lt;h5 id="证书设置"&gt;证书设置
&lt;/h5&gt;&lt;p&gt;Certimate 的证书设置里有几个参数需要选择，简单说一下每个怎么填：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;私钥来源 → 随机生成&lt;/strong&gt;。每次续期都生成新的私钥，万一某一代私钥泄露，影响范围更小，具有向前安全性。&amp;ldquo;复用私钥&amp;quot;是给 HPKP（已废弃）和 DANE/TLSA 这类特殊场景用的，普通网站不需要。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;em&gt;&lt;strong&gt;注意，以下颁发机构均使用 Google Trust Services，情欲先了解ACME EAB 账号获取的相关信息，难以完成则使用 Let&amp;rsquo;s Encrypt 即可。&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;颁发机构 → Google Trust Services&lt;/strong&gt;。具体原因依旧可以看下面的 &lt;a class="link" href="#tls-%e7%bb%ad%e6%9c%9f%e7%ad%96%e7%95%a5" &gt;TLS 续期策略&lt;/a&gt; 部分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;颁发机构授权&lt;/strong&gt;：这个由于我们用的是 Google Trust Services，所以需要一个 ACME EAB 账号，拿到这个授权的 ID 和 Key 后把它们填到申请节点的对应字段里。关于如何获取，请参考 &lt;a class="link" href="https://cloud.google.com/certificate-manager/docs/public-ca-tutorial" target="_blank" rel="noopener"
 &gt;使用 Public CA 和 ACME 客户端请求证书&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;有效期 → 47 天&lt;/strong&gt;。具体原因依旧可以看下面的 &lt;a class="link" href="#tls-%e7%bb%ad%e6%9c%9f%e7%ad%96%e7%95%a5" &gt;TLS 续期策略&lt;/a&gt; 部分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;首选链 → 留空/默认&lt;/strong&gt;。以前有人为了兼容老旧 Android 手动选链到 DST Root CA X3，但那条链 2024 年 9 月已经过期了，现在默认的 ISRG Root X1 兼容所有主流设备。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ACME 配置文件 → classic（默认）&lt;/strong&gt;。classic 对应长有效期的传统证书。另一个选项 &lt;code&gt;shortlived&lt;/code&gt; 是 7 天有效期的超短期证书，对自动化系统稳定性要求极高，个人项目没必要用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;阻止 CSR 包含主题通用名称 → 不勾选&lt;/strong&gt;。这个选项会禁止在 CSR 里包含通配符证书的通用名称（CN），只把域名放在 Subject Alternative Name (SAN) 里。这个选项是为了兼容一些老旧 CA 的奇葩要求的，现代 CA 都没问题，所以不选。&lt;/p&gt;
&lt;h5 id="重复申请"&gt;重复申请
&lt;/h5&gt;&lt;p&gt;设置为当上次申请证书成功后、且证书剩余有效期大于 17 天，再次运行工作流时跳过此申请节点。具体原因依旧可以看下面的 &lt;a class="link" href="#tls-%e7%bb%ad%e6%9c%9f%e7%ad%96%e7%95%a5" &gt;TLS 续期策略&lt;/a&gt; 部分。&lt;/p&gt;
&lt;h4 id="踩坑容器文件隔离"&gt;踩坑：容器文件隔离
&lt;/h4&gt;&lt;p&gt;起初以为“部署节点”部分不怎么需要动，结果Certimate 第一次执行工作流后，虽然面板提示证书签发成功，路径是 &lt;code&gt;/etc/ssl/certimate/cert.crt&lt;/code&gt; 和 &lt;code&gt;/etc/ssl/certimate/cert.key&lt;/code&gt;，但是我直接在 Nginx 配置里填这两个路径，&lt;code&gt;nginx -t&lt;/code&gt; 直接报错：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cannot load certificate &amp;#34;/etc/ssl/certimate/cert.crt&amp;#34;: BIO_new_file() failed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(SSL: error:80000002:system library::No such file or directory)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;排查了一下，发现问题是，事实上证书写在了 Certimate 容器内部的文件系统里，宿主机根本看不到。Docker 容器的文件系统和宿主机是完全隔离的，除非做了 &lt;code&gt;volumes&lt;/code&gt; 挂载，否则两边的 &lt;code&gt;/etc/ssl/certimate/&lt;/code&gt; 是两个毫无关系的目录。然而即使挂载到宿主机，每次重新申请证书后，也无法触发宿主机上的 Nginx 重新加载，这样就需要在 Certimate 中配置一个 Webhook，还需要自己写一个脚本来监听这个 Webhook，收到通知后再执行 &lt;code&gt;nginx -t &amp;amp;&amp;amp; systemctl reload nginx&lt;/code&gt;，感觉极其麻烦。（理论上应该可以通过“部署后事件”来解决）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：在 Certimate 的工作流里改用 &lt;strong&gt;SSH 部署&lt;/strong&gt;节点，让 Certimate 通过 SSH 把证书从容器内部推送到宿主机的真实路径。操作步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在宿主机上生成一对 SSH 密钥：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh-keygen -t ed25519 -f ~/.ssh/certimate_deploy -N &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt; -C &lt;span class="s2"&gt;&amp;#34;certimate-deploy&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat ~/.ssh/certimate_deploy.pub &amp;gt;&amp;gt; ~/.ssh/authorized_keys
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat ~/.ssh/certimate_deploy &lt;span class="c1"&gt;# 用于填充到 Certimate 面板里&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Certimate 部署节点中，部署目标选择 SSH&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主机提供商授权中输入服务器 IP、端口、用户，以及上一步生成的 SSH 私钥内容。&lt;/p&gt;
&lt;p&gt;&lt;img alt="SSH Config" class="gallery-image" data-flex-basis="357px" data-flex-grow="149" height="937" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/certimate-ssh-config.png" srcset="https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/certimate-ssh-config_hu_fcb839aaa33633c4.png 800w, https://blog.sisy.cc/p/%E8%87%AA%E9%83%A8%E7%BD%B2-moe-counter/img/certimate-ssh-config.png 1397w" width="1397"&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;证书和证书密钥的输出路径还填 &lt;code&gt;/etc/ssl/certimate/cert.crt&lt;/code&gt; 和 &lt;code&gt;/etc/ssl/certimate/cert.key&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;再次执行工作流，这次证书成功落到宿主机的目录下，Nginx 终于能读到了。&lt;/p&gt;
&lt;h3 id="升级-nginx-配置为-https"&gt;升级 Nginx 配置为 HTTPS
&lt;/h3&gt;&lt;p&gt;证书到位后，把 Nginx 配置升级为 HTTPS:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;span class="lnt"&gt;20
&lt;/span&gt;&lt;span class="lnt"&gt;21
&lt;/span&gt;&lt;span class="lnt"&gt;22
&lt;/span&gt;&lt;span class="lnt"&gt;23
&lt;/span&gt;&lt;span class="lnt"&gt;24
&lt;/span&gt;&lt;span class="lnt"&gt;25
&lt;/span&gt;&lt;span class="lnt"&gt;26
&lt;/span&gt;&lt;span class="lnt"&gt;27
&lt;/span&gt;&lt;span class="lnt"&gt;28
&lt;/span&gt;&lt;span class="lnt"&gt;29
&lt;/span&gt;&lt;span class="lnt"&gt;30
&lt;/span&gt;&lt;span class="lnt"&gt;31
&lt;/span&gt;&lt;span class="lnt"&gt;32
&lt;/span&gt;&lt;span class="lnt"&gt;33
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Redirect HTTP to HTTPS
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;moe.sisy.cc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt; &lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="nv"&gt;$host$request_uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# HTTPS main service
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;moe.sisy.cc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_certificate&lt;/span&gt; &lt;span class="s"&gt;/etc/ssl/certimate/cert.crt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_certificate_key&lt;/span&gt; &lt;span class="s"&gt;/etc/ssl/certimate/cert.key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# Recommended SSL params
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_protocols&lt;/span&gt; &lt;span class="s"&gt;TLSv1.2&lt;/span&gt; &lt;span class="s"&gt;TLSv1.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_ciphers&lt;/span&gt; &lt;span class="s"&gt;ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_prefer_server_ciphers&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_session_cache&lt;/span&gt; &lt;span class="s"&gt;shared:SSL:10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;ssl_session_timeout&lt;/span&gt; &lt;span class="s"&gt;1d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s"&gt;.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;upgrade&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;测试并重载：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo nginx -t
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo systemctl reload nginx
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;访问 &lt;code&gt;https://moe.sisy.cc&lt;/code&gt;，浏览器里的证书锁和证书详情应该都会出现，这样 HTTPS 就大功告成了！&lt;/p&gt;
&lt;h2 id="tls-续期策略"&gt;TLS 续期策略
&lt;/h2&gt;&lt;p&gt;我申请到的证书有效期是 47 天，截至我写这篇博客的时候，只有 Google Trust Services 一家提供了 47 天有效期的证书。秒选啊秒选。&lt;/p&gt;
&lt;p&gt;关于为什么现代网络正在推行 90 -&amp;gt; 47 天的更短期证书，可以参考 &lt;a class="link" href="https://cabforum.org/working-groups/server/baseline-requirements/requirements/#632-certificate-operational-periods-and-key-pair-usage-periods" target="_blank" rel="noopener"
 &gt;CA/Browser Forum 上投票决定的结果&lt;/a&gt; ，在这之前提出并推动此事的&lt;a class="link" href="https://groups.google.com/a/groups.cabforum.org/g/servercert-wg/c/bvWh5RN6tYI" target="_blank" rel="noopener"
 &gt;这篇文章&lt;/a&gt;，以及&lt;a class="link" href="https://www.bleepingcomputer.com/news/security/ssl-tls-certificate-lifespans-reduced-to-47-days-by-2029/" target="_blank" rel="noopener"
 &gt;这篇分享此事的博客&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;简单来说，2025 年早些时候，Apple 公司提出了一项缩短证书有效期的动议，该动议得到了 Sectigo、Google Chrome 团队和 Mozilla 的支持。该提案将在未来四年内逐步缩短证书的有效期，从截至 2025 年的 398 天需求，在 2029 年 3 月之前缩短到 47 天需求。这个提案的目标是最大限度地降低因证书数据过期、加密算法过时以及长期暴露于泄露凭证而带来的风险。此外，它还鼓励企业和开发者利用自动化手段来更新和轮换 TLS 证书，从而降低网站运行在过期证书上的可能性。&lt;/p&gt;
&lt;p&gt;我的证书工作流的时间策略为：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;参数&lt;/th&gt;
 &lt;th&gt;值&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;工作流触发频率&lt;/td&gt;
 &lt;td&gt;每天 1 次&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;提前续期阈值&lt;/td&gt;
 &lt;td&gt;15 天&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;ldquo;每天触发&amp;quot;听起来很频繁，但 Certimate 会先检查证书的剩余有效期，没到阈值就跳过，不会真给 CA 发请求。这样设置的好处是&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;失败后有足够的重试窗口——如果某次续期因为网络抖动失败了，明天还会再试。设 15 天阈值意味着有 15 次重试机会。&lt;/li&gt;
&lt;li&gt;失败后证书还有足够的剩余有效期，可以有充足的时间手动介入修复问题，不至于证书过期了才发现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果把触发频率设成 30 天，万一到了续期的那一次恰好失败，下一次触发时证书已经过期了——47 天的证书根本扛不住 30 天的触发间隔。&lt;/p&gt;
&lt;p&gt;续期时间的把控则是另一种思路：既要保证充分利用证书的有效期，又要留足时间介入修复问题。根据大佬的经验法则，续期阈值设为&lt;strong&gt;证书有效期的 1/3&lt;/strong&gt; 左右，平衡利用率和安全缓冲。对我来讲，47 ÷ 3 ≈ 15 天，刚好。&lt;/p&gt;
&lt;p&gt;另外强烈建议在 Certimate 里配一个&lt;strong&gt;失败通知&lt;/strong&gt;（邮件、Webhook 等），这样续期失败时能第一时间收到告警，不至于证书悄悄过期了几天才发现（额，虽然我还没配）。&lt;/p&gt;
&lt;h2 id="最终架构"&gt;最终架构
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;span class="lnt"&gt;15
&lt;/span&gt;&lt;span class="lnt"&gt;16
&lt;/span&gt;&lt;span class="lnt"&gt;17
&lt;/span&gt;&lt;span class="lnt"&gt;18
&lt;/span&gt;&lt;span class="lnt"&gt;19
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[浏览器]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ HTTPS (443)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[Cloudflare DNS]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ HTTPS (443)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[Nginx] ── Listening 80/443, TLS managed by Certimate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ HTTP (Reverse Proxy to 3000)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[Docker: Moe-Counter] ── 127.0.0.1:3000, SQLite Persistent Volume
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[Docker: Certimate] ── 127.0.0.1:8090, managed by SSH Port Forwarding
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ DNS-01 (Cloudflare API) Certimate in Docker deploys certs to host via SSH
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼ ▼
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[Google Trust Services] [Host /etc/ssl/certimate/]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;整个架构里：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;公网只开放 80/443 和 22（SSH），Moe-Counter 的 3000 和 Certimate 的 8090 都只绑在回环地址上，外网不可达。&lt;/li&gt;
&lt;li&gt;HTTPS 证书由 Certimate 自动申请、部署、续期，不需要人工介入。&lt;/li&gt;
&lt;li&gt;Cloudflare 提供 CDN 和 DDoS 防护，SSL 模式用最安全的 Full (Strict)。&lt;/li&gt;
&lt;li&gt;通过 SSH 端口转发访问 Certimate 面板，管理界面零暴露。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;涉及到三种（理论上如果配了告警应该还有更多）第三方授权服务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cloudflare API Token：用于 Certimate 的 DNS-01 验证，权限最小化只允许编辑 &lt;code&gt;sisy.cc&lt;/code&gt; 的 DNS 记录。&lt;/li&gt;
&lt;li&gt;SSH 密钥：用于 Certimate 的 SSH 部署节点，用于把证书从容器内部推送到宿主机的真实路径。&lt;/li&gt;
&lt;li&gt;Google Trust Services EAB：CA 需要 EAB（外部账户绑定）来验证申请者的身份，Certimate 也支持配置 EAB 的 Key ID 和 Key Secret 来完成绑定。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从&amp;quot;能跑&amp;quot;到&amp;quot;能用&amp;rdquo;，确实比想象中多了不少环节。但每一层都有其存在的意义：Docker 隔离运行环境，Nginx 统一入口和端口管理，Certimate 自动化证书生命周期，Cloudflare 提供边缘防护&amp;hellip;&amp;hellip; 搞清楚每一层在干什么之后，以后再部署别的服务也是同一套模式，而且熟悉了会快很多，换个容器就行了。&lt;/p&gt;
&lt;p&gt;长吁。。终于把这个博客的访问计数器部署好了，虽然过程比想象中复杂了不少，不过成果是令人欣慰的。之后就可以在自部署的环境中生成各种主题的访问计数器图片了！&lt;/p&gt;
&lt;p&gt;喂，别忘了配置一下 Certimate 的续期失败通知啊（&lt;/p&gt;
&lt;img src="https://moe.sisy.cc/@:sisy-blog-moe?name=%3Asisy-blog-moe&amp;theme=gelbooru-h&amp;padding=2&amp;offset=0&amp;align=top&amp;scale=1&amp;pixelated=1&amp;darkmode=auto" alt=":sisy-blog-moe" /&gt;</description></item></channel></rss>