目标:不是只跑起来,而是长期可维护

这次搭 iotlabx,不是想做一个“能访问的首页”就结束,而是希望它能成为一个长期维护的个人技术实验室网站。

基本要求是:

  • 内容主站用 Hugo,尽量静态化,方便部署和备份。
  • 主题用 PaperMod,在它的基础上做有限定制,而不是完全重写一套主题。
  • 文章结构要适合技术博客:协议分析、嵌入式、Qt 工程、项目记录、实验室入口都能分区沉淀。
  • 动态能力尽量作为独立轻服务挂进来,例如评论、设备状态、Office UI 等。
  • 部署环境是树莓派,同机还跑其他服务,所以不能上重型 SSR、复杂数据库或一堆常驻进程。
  • 评论系统要支持访客自定义昵称和邮箱,不强制读者登录 GitHub。

最终选型是:

protocol trace
Hugo      -> 静态内容生成
PaperMod  -> 基础主题
nginx     -> 静态文件服务 + 反向代理
Artalk    -> 自托管评论系统
SQLite    -> 评论数据存储
systemd   -> 让 Artalk 常驻运行
Cloudflare/Tunnel -> 公网入口与边缘防护

这篇按真实实施过程写,不只贴最终配置,也记录几个容易踩的坑。

站点目录与部署方式

站点源码目录:

protocol trace
/home/yi/blog

Hugo 构建输出目录:

protocol trace
/home/yi/blog/public

nginx 对外服务目录:

protocol trace
/var/www/iotlabx

发布方式很直接:

cd /home/yi/blog
hugo --minify
cp -a public/. /var/www/iotlabx/

这个方式不花哨,但很适合树莓派上的个人站:可控、可观察、出问题时容易回滚。

安装 Hugo

这台机器是 Debian 13 / ARM64:

uname -m
cat /etc/os-release

输出类似:

protocol trace
aarch64
Debian GNU/Linux 13 (trixie)

Hugo 推荐安装 extended 版本。PaperMod 本身不一定强依赖 extended,但技术博客后续如果要用 Sass/SCSS、图片处理或主题扩展,直接装 extended 版本更省心。

先确认本机架构:

uname -m

常见对应关系:

protocol trace
aarch64 / arm64 -> Linux ARM64
x86_64          -> Linux AMD64

到 Hugo GitHub Releases 找对应包,例如 ARM64:

protocol trace
hugo_extended_<version>_linux-arm64.tar.gz

下载并安装到 /usr/local/bin:

cd /tmp
curl -L -o hugo.tar.gz   https://github.com/gohugoio/hugo/releases/download/v0.160.1/hugo_extended_0.160.1_linux-arm64.tar.gz

tar -xzf hugo.tar.gz
sudo install -m 0755 hugo /usr/local/bin/hugo
hugo version

如果 GitHub 下载慢,可以先在电脑上下载好再传到树莓派,或者临时使用可信的镜像代理。关键是最后要验证:

hugo version

本次实际环境版本是:

protocol trace
hugo v0.160.1 ... linux/arm64

如果只是快速试用,也可以用系统包管理器:

sudo apt update
sudo apt install hugo

但 Debian 源里的 Hugo 版本可能偏旧。长期维护站点时,我更建议使用官方 release,版本可控。

安装基础依赖

除了 Hugo,还需要 nginx、curl、tar 这些基础工具:

sudo apt update
sudo apt install -y nginx curl tar ca-certificates

确认 nginx 能启动:

sudo systemctl enable --now nginx
sudo systemctl status nginx --no-pager

创建站点发布目录:

sudo mkdir -p /var/www/iotlabx
sudo chown -R "$USER":"$USER" /var/www/iotlabx

如果你希望发布目录继续由 root 管理,也可以不改 owner,每次发布时用 sudo cp -a public/. /var/www/iotlabx/。个人树莓派站点为了操作方便,我更倾向让当前维护用户拥有这个目录。

初始化 Hugo 站点

如果从零开始,可以这样创建站点:

mkdir -p /home/yi
cd /home/yi
hugo new site blog
cd blog

安装 PaperMod 主题常见有两种方式。

如果站点是 git 仓库,可以用 submodule:

git init
git submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperMod

如果不想用 submodule,也可以直接下载主题到:

protocol trace
themes/PaperMod

这次站点目录本身不是 git 仓库,所以维护方式更偏本地文件部署。无论哪种方式,Hugo 只要能找到:

theme = "PaperMod"

就可以工作。

基础 hugo.toml

主配置文件是:

protocol trace
/home/yi/blog/hugo.toml

核心配置如下:

baseURL = "https://iotlabx.com/"
languageCode = "zh-cn"
defaultContentLanguage = "zh-cn"
title = "iotlabx"
theme = "PaperMod"

[pagination]
  pagerSize = 10

[params]
  description = "一个运行在 Raspberry Pi 上的个人 IoT 技术实验室:记录协议分析、设备接入、抓包排障、边缘自动化与轻量动态实验。"
  author = "Yi"
  env = "production"
  defaultTheme = "auto"
  disableThemeToggle = false
  ShowReadingTime = true
  ShowShareButtons = false
  ShowPostNavLinks = true
  ShowBreadCrumbs = true
  ShowCodeCopyButtons = true
  ShowToc = true
  TocOpen = true
  ShowWordCount = true
  ShowRssButtonInSectionTermList = true
  UseHugoToc = true
  DateFormat = "2006-01-02"

PaperMod 本身已经提供了文章页、列表页、目录、深浅色切换、代码复制等基础能力。第一版不要急着重写主题,优先把内容结构和发布链路跑通。

首页与导航

首页可以先用 PaperMod 的 homeInfoParams 起步:

[params.homeInfoParams]
  Title = "协议细节、抓包排障、真实设备接入。"
  Content = "运行在 Raspberry Pi 上的个人 IoT 技术实验室。主站用 Hugo 沉淀长期技术内容,动态能力通过轻量服务接入。"

导航菜单按栏目组织:

[menu]
  [[menu.main]]
    name = "协议库"
    pageRef = "/protocols"
    weight = 10
  [[menu.main]]
    name = "嵌入式"
    pageRef = "/embedded"
    weight = 15
  [[menu.main]]
    name = "Qt 工具"
    pageRef = "/qt"
    weight = 18
  [[menu.main]]
    name = "随记"
    pageRef = "/notes"
    weight = 20
  [[menu.main]]
    name = "项目"
    pageRef = "/projects"
    weight = 30
  [[menu.main]]
    name = "实验室"
    pageRef = "/lab"
    weight = 40
  [[menu.main]]
    name = "设备状态"
    pageRef = "/status"
    weight = 50
  [[menu.main]]
    name = "Office"
    url = "https://office.iotlabx.com"
    weight = 55
  [[menu.main]]
    name = "关于"
    pageRef = "/about"
    weight = 60

这里有一个经验:技术站不要只按“文章/关于”分。协议、项目、工具、实验室这些入口能帮助后续内容持续归档。

内容结构

当前站点内容目录大致是:

protocol trace
content/
  _index.md
  about.md
  posts/
  protocols/
  embedded/
  qt/
  notes/
  projects/
  lab/
  status/
  office/

其中:

  • posts/:综合技术文章。
  • protocols/:协议专题,例如 MQTT、BLE、Modbus、802.11。
  • embedded/:嵌入式相关内容。
  • qt/:Qt 工程工具、上位机、调试界面、控件实践。
  • projects/:项目展示。
  • lab/:实验室入口。
  • status/:设备状态页入口。
  • office/:独立 Office UI 入口。

创建栏目页示例:

mkdir -p content/qt
cat > content/qt/_index.md <<'EOF'
---
title: "Qt 工程工具"
description: "记录 Qt 上位机、串口/网络调试、配置界面、状态面板与自定义控件实践。"
---
EOF

写新文章:

hugo new posts/my-first-post.md

文章 front matter 示例:

---
title: "802.11 RTS/CTS"
date: 2026-05-07T08:15:00+08:00
draft: false
summary: "从抓包排障角度整理 802.11 RTS/CTS。"
tags: ["Wi-Fi", "802.11", "协议分析", "抓包"]
---

PaperMod 定制原则

PaperMod 的优势是主题完整,但技术站通常还需要更强的文章阅读体验,例如:

  • 更稳定的正文宽度。
  • 更清晰的右侧目录。
  • 更适合协议 trace 的代码块。
  • 更适合 C++/Qt 的普通代码块。
  • 首页模块化入口。

Hugo 会自动合并:

protocol trace
assets/css/extended/*.css

所以可以把自定义样式放到:

protocol trace
assets/css/extended/z-article.css

这样不需要直接改 PaperMod 主题源码。

实践里有两条经验:

  1. 能用 Hugo layout 覆盖的,就不要改 theme 目录。
  2. 能放到 assets/css/extended/ 的,就不要在模板里堆内联样式。

配置 nginx 静态站点

nginx 的职责有两个:

  1. 服务 Hugo 构建出来的静态文件。
  2. 后面把 /artalk/ 反向代理到本机 Artalk 服务。

基础站点配置可以放在 Debian 默认站点里,也可以新建独立文件。这里用最直观的方式说明:

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    root /var/www/iotlabx;
    index index.html index.htm;

    server_name _;

    location / {
        try_files $uri $uri/ =404;
    }
}

检查配置并 reload:

sudo nginx -t
sudo systemctl reload nginx

如果你的域名直接 DNS 到服务器公网 IP,这样就能访问静态站了。

但我的实际环境是树莓派在内网,通过 Cloudflare Tunnel 暴露公网入口,所以公网访问链路不是“公网 IP -> nginx”,而是:

protocol trace
Cloudflare -> cloudflared tunnel -> 本机 nginx :80

这一点后面单独展开。

本地预览与构建

开发时可以用 Hugo server:

hugo server -D --bind 127.0.0.1 --port 1313

如果只是预览已经构建好的静态文件,也可以在 public 下起一个简单 HTTP server:

cd /home/yi/blog/public
python3 -m http.server 1313 --bind 127.0.0.1

注意工作目录必须是 public。如果在 /home/yi/blog 起 Python server,打开的会是源码目录,不是最终站点。

构建发布:

cd /home/yi/blog
hugo --minify
cp -a public/. /var/www/iotlabx/

到这里,一个 Hugo + PaperMod 静态博客已经可以正常上线。接下来才是评论系统。

配置 Cloudflare Tunnel 公网入口

如果服务器有公网 IP,可以直接把域名 A/AAAA 记录指向服务器,再在 nginx 上处理 80/443。

本次域名是在阿里云购买的,但公网入口没有直接走阿里云 DNS 解析到家里路由器,而是把域名 DNS 托管切到 Cloudflare。也就是说:

protocol trace
域名注册商:阿里云
DNS 托管:Cloudflare
公网入口:Cloudflare Tunnel
源站服务:树莓派上的 nginx

域名在哪里买和 DNS 托管在哪里是两件事。域名可以继续在阿里云续费管理,但 NS 服务器切到 Cloudflare 后,解析记录、Tunnel 路由、WAF 和限流都在 Cloudflare 里配置。

对这个场景来说,Cloudflare 免费套餐已经够用:

  • 免费 DNS 托管。
  • 免费 HTTPS 入口。
  • 免费 Cloudflare Tunnel。
  • 基础 WAF / Bot 防护能力。
  • 可以添加 Public Hostname,把 iotlabx.com、office.iotlabx.com 等路由到内网服务。

但树莓派通常在家里或内网环境,公网 IP、端口映射、运营商限制都可能带来麻烦。Cloudflare Tunnel 的好处是:

  • 本机主动连 Cloudflare,不需要公网入站端口。
  • 域名仍然走 Cloudflare。
  • 后续可以继续挂多个子域名,例如 office.iotlabx.com、status.iotlabx.com。
  • 可以在 Cloudflare 边缘做 WAF、Bot 防护和限流。

把阿里云域名接入 Cloudflare

大致流程是:

  1. 在 Cloudflare 添加站点,例如 iotlabx.com。
  2. Cloudflare 会分配两条 Nameserver,例如:
protocol trace
xxx.ns.cloudflare.com
yyy.ns.cloudflare.com
  1. 回到阿里云域名控制台,找到域名的 DNS / Nameserver 设置。
  2. 把阿里云默认 NS 替换成 Cloudflare 给出的两条 NS。
  3. 等待 DNS 托管生效。

生效可以用命令确认:

dig NS iotlabx.com +short

如果返回的是 Cloudflare 的 NS,说明域名已经由 Cloudflare 托管。

后续 A、CNAME、Tunnel Public Hostname、WAF、限流等都在 Cloudflare 配置;阿里云主要负责域名注册和续费。

安装 cloudflared

Debian/Ubuntu 上可以按 Cloudflare 官方仓库安装。大致流程是导入 key、添加源、安装包:

sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg   | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null

echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main'   | sudo tee /etc/apt/sources.list.d/cloudflared.list

sudo apt update
sudo apt install cloudflared

如果仓库访问不稳定,也可以去 Cloudflare release 页面下载对应架构的 .deb 包,ARM64 机器选择 arm64 版本。

安装后确认:

cloudflared --version

本次实际环境是:

protocol trace
cloudflared version 2026.3.0

创建 Tunnel

Cloudflare Zero Trust 控制台路径大致是:

protocol trace
Zero Trust -> Networks -> Tunnels -> Create a tunnel

选择 Cloudflared 类型,创建一个 tunnel,例如:

protocol trace
iotlabx

Cloudflare 会给出一条安装/运行命令,通常长这样:

sudo cloudflared service install <token>

或者 systemd 服务中类似:

ExecStart=/usr/bin/cloudflared --no-autoupdate tunnel run --token <token>

注意:<token> 是敏感信息,不要写进博客、截图或公开仓库。

确认服务运行:

systemctl status cloudflared.service --no-pager

正常状态应为:

protocol trace
Active: active (running)

配置 Public Hostname

在 tunnel 的 Public Hostnames 里添加路由:

protocol trace
Hostname: iotlabx.com
Service:  http://localhost:80

如果还需要子域名,可以继续添加:

protocol trace
Hostname: office.iotlabx.com
Service:  http://localhost:19000

Hostname: status.iotlabx.com
Service:  http://localhost:80

本篇主站只需要:

protocol trace
iotlabx.com -> http://localhost:80

Cloudflare 会自动处理外侧 HTTPS,内侧 tunnel 到本机 nginx 使用 HTTP 即可。

验证公网入口

本地验证 nginx:

curl -I http://127.0.0.1/

公网验证域名:

curl -I https://iotlabx.com/

如果本地 200,公网不通,优先检查:

  • cloudflared 服务是否 active。
  • Tunnel Public Hostname 是否指向 http://localhost:80。
  • DNS 是否已经由 Cloudflare 接管。
  • Cloudflare 页面是否显示 tunnel Healthy。

Cloudflare 侧限流建议

nginx 本地限流能保护 Artalk 和 SQLite,但请求已经到树莓派了。更好的方式是在 Cloudflare 边缘也做一层。

建议加一条 WAF / Rate Limiting 规则:

protocol trace
URI Path equals /artalk/api/v2/comments

策略可以从温和开始:

protocol trace
10 requests / 1 minute / IP -> Managed Challenge

如果被打得很明显,再改成 Block。

原则是:普通垃圾评论交给审核,瞬时攻击尽量挡在 Cloudflare,别让树莓派扛。

为什么评论系统选 Artalk

最初也考虑过 GitHub Discussions 类评论方案。它维护简单,适合开发者读者多的站点。但这类方案通常要求读者使用 GitHub 身份,不适合希望访客直接填昵称、邮箱、网站来留言的独立博客。

我希望评论区更像传统技术博客:

protocol trace
昵称:读者自己填
邮箱:用于头像、通知或身份识别
网站:可选
内容:直接留言

所以最终选了 Artalk:

  • 支持昵称/邮箱/网站留言。
  • 可自托管。
  • Go 单二进制,适合 ARM64 树莓派。
  • 可以用 SQLite。
  • 能接入审核、关键词、验证码、通知等能力。
  • 前端可以直接挂进 Hugo 模板。

Artalk 不是“零运维”,但对于个人站来说,它在功能和复杂度之间比较平衡。

加入 Artalk 后的整体架构

加入评论后,完整链路变成:

protocol trace
浏览器
  |
  |  https://iotlabx.com/posts/...
  v
Cloudflare / Tunnel
  |
  v
nginx :80
  |
  |-- /                  -> /var/www/iotlabx/  Hugo 静态站点
  |
  |-- /artalk/           -> http://127.0.0.1:23366/  Artalk 后端
  |
  |-- /artalk/api/v2/comments
          nginx 限流 + 500K body 限制
          -> http://127.0.0.1:23366/api/v2/comments

关键原则是:Artalk 只监听本地地址,不直接暴露公网。

host: 127.0.0.1
port: 23366

公网访问统一经过 nginx 和 Cloudflare。

Hugo 侧配置

先在 hugo.toml 增加 Artalk 参数:

[params.artalk]
  enabled = true
  server = "https://iotlabx.com/artalk"
  site = "iotlabx"

这里的 server 是浏览器访问 Artalk API 和前端资源的地址。因为我们后面会把 nginx 的 /artalk/ 反向代理到本地 Artalk,所以这里直接写主站路径。

PaperMod 评论模板

PaperMod 会在文章页调用评论 partial。这里使用站点自己的覆盖模板:

protocol trace
layouts/_partials/comments.html

模板内容如下:

{{- $artalk := site.Params.artalk -}}
{{- if and $artalk $artalk.enabled $artalk.server $artalk.site -}}
<section class="artalk-comments" id="comments" aria-label="文章留言">
  <h2>回复 / 留言</h2>
  <div id="ArtalkComments"></div>
  <link rel="stylesheet" href="{{ $artalk.server }}/dist/Artalk.css">
  <script src="{{ $artalk.server }}/dist/Artalk.js"></script>
  <script>
    window.addEventListener('DOMContentLoaded', function () {
      if (!window.Artalk) return;
      Artalk.init({
        el: '#ArtalkComments',
        pageKey: {{ .RelPermalink | jsonify | safeJS }},
        pageTitle: {{ .Title | plainify | jsonify | safeJS }},
        server: {{ $artalk.server | jsonify | safeJS }},
        site: {{ $artalk.site | jsonify | safeJS }},
        useBackendConf: true,
        darkMode: document.documentElement.dataset.theme === 'dark'
      });
    });
  </script>
</section>
{{- else -}}
<section class="comment-placeholder" id="comments">
  <h2>回复 / 留言</h2>
  <p>Artalk 评论区已预留。配置 Artalk 服务地址和站点名后即可开放留言。</p>
</section>
{{- end -}}

几个细节:

  • pageKey 使用 .RelPermalink,这样文章 URL 稳定时,评论和文章绑定关系也稳定。
  • pageTitle、server、site 这些值用 Hugo 的 jsonify | safeJS 输出,避免标题里出现引号、反斜杠等字符时破坏 JS 字符串。
  • site 要和 Artalk 后端的 site_default 对上。
  • useBackendConf: true 让前端读取后端配置。这个后来对登录有效期、验证码状态等问题排查很有帮助。
  • darkMode 在评论区初始化时读取站点当前主题;如果要做到切换主题后评论区实时跟随,还需要额外监听主题切换事件。

如果服务端还没准备好,可以先把:

enabled = false

这样页面只显示占位,不会去请求不存在的 /artalk/dist/Artalk.js。

安装 Artalk ARM64 二进制

树莓派是 Linux ARM64,官方 release 包名类似:

protocol trace
artalk_v2.9.1_linux_arm64.tar.gz

如果 GitHub 直连很慢,可以多试几次,或使用代理镜像。下载后解压:

mkdir -p /home/yi/artalk/bin /home/yi/artalk/data /home/yi/artalk/downloads

tar -xzf /home/yi/artalk/downloads/artalk_v2.9.1_linux_arm64.tar.gz -C /tmp/artalk-extract
install -m 0755 /tmp/artalk-extract/artalk_v2.9.1_linux_arm64/artalk /home/yi/artalk/bin/artalk

/home/yi/artalk/bin/artalk version

部署到用户目录而不是 /usr/local/bin,好处是不用依赖 root 安装。后面用 user systemd 拉起服务。

Artalk 配置:SQLite + 本地监听

配置文件:

protocol trace
/home/yi/artalk/artalk.yml

核心配置如下,app_key 请自己生成随机字符串,不要照抄:

app_key: "<your-random-app-key>"
locale: zh-CN
site_default: iotlabx
host: 127.0.0.1
port: 23366
timezone: Asia/Shanghai
login_timeout: 604800

trusted_domains:
  - https://iotlabx.com
  - http://127.0.0.1:1313
  - http://127.0.0.1

db:
  type: sqlite
  file: ./data/artalk.db

http:
  body_limit: 1
  proxy_header: X-Forwarded-For

log:
  enabled: true
  filename: ./data/artalk.log

cache:
  enabled: true
  type: builtin
  expires: 30

这里有几个工程选择:

只监听 127.0.0.1

host: 127.0.0.1

不要让 Artalk 直接监听公网地址。对外入口交给 nginx 和 Cloudflare。

SQLite 足够个人站使用

db:
  type: sqlite
  file: ./data/artalk.db

个人博客评论量不会很夸张,SQLite 足够。它的优势是维护成本低,一个文件就能备份。

body_limit 不是 500K

Artalk 的 http.body_limit 单位是 MB。这里设成:

body_limit: 1

也就是 Artalk 服务端最大接受 1MB 请求体。

如果想精确限制到 500K,更适合放在 nginx:

client_max_body_size 500k;

这次就采用了这个方案。

默认审核:不要让评论直接公开

评论系统上线后,最先要考虑的不是功能,而是垃圾内容。

Artalk 可以让新评论默认进入待审核:

moderator:
  pending_default: true
  api_fail_block: true
  keywords:
    enabled: false
    pending: true
    files:
      - ./data/keywords.txt
    file_sep: "\n"
    replace_to: x

这样即使有人成功提交评论,也不会立刻出现在页面上。

我最开始还尝试过启用 Artalk 图片验证码:

captcha:
  enabled: true
  always: false
  captcha_type: image
  action_limit: 3
  action_reset: 60

但真实使用里踩了一个坑:Artalk 会把评论、投票、登录、删除等动作都算进验证码触发条件。管理员在调试登录和审核时,很容易陷入“输密码 -> 要验证码 -> 验证码失败 -> 又输密码”的循环。

最后这套站点选择关闭 Artalk 内置验证码:

captcha:
  enabled: false
  always: false
  captcha_type: image
  action_limit: 20
  action_reset: 60

防刷评论主要交给 Cloudflare / nginx,内容安全交给默认待审核。

用 user systemd 启动 Artalk

创建用户级 systemd 服务:

protocol trace
/home/yi/.config/systemd/user/artalk.service

内容:

[Unit]
Description=Artalk comments for iotlabx (user service)
After=network.target

[Service]
Type=simple
WorkingDirectory=/home/yi/artalk
ExecStart=/home/yi/artalk/bin/artalk -w /home/yi/artalk -c /home/yi/artalk/artalk.yml server
Restart=always
RestartSec=3

[Install]
WantedBy=default.target

启用并启动:

systemctl --user daemon-reload
systemctl --user enable --now artalk.service
systemctl --user status artalk.service --no-pager

本机验证:

curl -I http://127.0.0.1:23366/dist/Artalk.css

正常应该返回:

protocol trace
HTTP/1.1 200 OK
Content-Type: text/css

nginx 反向代理 /artalk/

Artalk 本地通了以后,下一步是 nginx。

基础反代:

location /artalk/ {
    client_max_body_size 500k;
    proxy_pass http://127.0.0.1:23366/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

然后检查并 reload:

sudo nginx -t
sudo systemctl reload nginx

外网验证:

curl -I https://iotlabx.com/artalk/dist/Artalk.css
curl -I https://iotlabx.com/artalk/dist/Artalk.js

两个都返回 200 后,Hugo 文章页里的评论区才算真正接通。

对评论 API 做 nginx 限流

如果只做默认审核,刷评论的人虽然不能污染页面,但仍然能把请求打到树莓派和 SQLite。这里要在 nginx 层限制评论提交接口。

在 /etc/nginx/conf.d/iotlabx-artalk-rate-limit.conf 中定义限流区:

limit_req_zone $binary_remote_addr zone=artalk_comment:10m rate=5r/m;
limit_conn_zone $binary_remote_addr zone=artalk_conn:10m;

这里要特别注意真实客户端 IP。

如果请求是:

protocol trace
浏览器 -> Cloudflare Tunnel -> 本机 nginx

nginx 看到的直接来源可能是本机 cloudflared,例如 127.0.0.1,而不一定是真实访客 IP。这样 $binary_remote_addr 就会退化成“所有公网访客共用一个来源”,限流语义会变成全站共享限流,不是每个访客独立限流。

更严谨的做法是在 server 或 http 级别信任本机 tunnel 入口,并让 nginx 使用 Cloudflare 传来的真实客户端 IP:

set_real_ip_from 127.0.0.1;
set_real_ip_from ::1;
real_ip_header CF-Connecting-IP;
real_ip_recursive on;

如果你的 nginx 不是只通过本机 cloudflared 接收流量,而是直接暴露给公网或局域网,还需要更谨慎地限制 set_real_ip_from 的可信来源;不要无条件信任任意客户端传来的 CF-Connecting-IP / X-Forwarded-For。

然后在站点 server 里加一个更精确的 comments API location,放在普通 /artalk/ 前面:

location = /artalk/api/v2/comments {
    client_max_body_size 500k;
    limit_req zone=artalk_comment burst=3 nodelay;
    limit_conn artalk_conn 3;

    proxy_pass http://127.0.0.1:23366/api/v2/comments;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

location /artalk/ {
    client_max_body_size 500k;
    proxy_pass http://127.0.0.1:23366/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

当前策略是:

protocol trace
真实客户端 IP 每分钟 5 次评论提交
突发最多 3 次
真实客户端 IP 并发最多 3 个连接
单次请求体最大 500K

压测时连续 POST 十次,后面会出现 503,说明 nginx 限流已经触发。若压测结果表现为不同公网 IP 互相影响,优先检查 nginx 是否正确还原了真实客户端 IP。

这里有个小坑:备份 nginx 配置时,不要把备份文件放在 /etc/nginx/sites-enabled/ 里。

Debian 系 nginx 会加载这个目录下的所有文件。如果你把备份放成:

protocol trace
/etc/nginx/sites-enabled/default.bak.20260509-221208

nginx 会把它也当成站点配置加载,结果可能报:

protocol trace
a duplicate default server for 0.0.0.0:80

备份应该放到不会被 nginx include 的目录,例如:

protocol trace
/home/yi/blog/ops/nginx-backups/

创建管理员账号

Artalk 管理员账号可以用命令创建:

/home/yi/artalk/bin/artalk \
  -w /home/yi/artalk \
  -c /home/yi/artalk/artalk.yml \
  admin --name "Yi" --email "[email protected]" --password "<your-admin-password>"

更推荐交互输入密码,避免密码出现在 shell history 或进程参数里:

/home/yi/artalk/bin/artalk \
  -w /home/yi/artalk \
  -c /home/yi/artalk/artalk.yml \
  admin

按提示输入昵称、邮箱和密码。

管理员创建后重启服务:

systemctl --user restart artalk.service

然后在评论区填同样的昵称和邮箱,打开通知中心/侧边栏,输入管理员密码。登录成功后,侧边栏会升级成 Dashboard,可以审核、删除、编辑评论。

管理员登录反复要求密码的坑

这次最绕的坑不是部署,而是登录态。

一开始我用同一个邮箱先发了普通测试评论,后来又把同一个邮箱升级成管理员。浏览器本地还保留着旧的普通用户 token,于是访问后台接口时服务端一直返回:

protocol trace
需要管理员权限

页面表现就是:反复让你输入密码,或者删除评论时又跳回登录。

排查方法:

  • 看 Artalk 日志:/home/yi/artalk/data/artalk.log
  • 如果是密码错,会看到:密码错误
  • 如果是旧 token/非管理员身份,会看到:需要管理员权限

解决办法:

  1. 避免同一个邮箱既做普通测试用户,又做管理员。
  2. 管理员邮箱只用于 Dashboard 审核。
  3. 普通评论测试换另一个邮箱,例如 [email protected]。
  4. 如果已经混用了,清浏览器站点数据,或直接用无痕窗口重新登录。
  5. 必要时清理数据库里重复邮箱的普通用户。

另外建议明确设置登录有效期:

login_timeout: 604800

这表示管理员 token 有效期 7 天。

运维与回滚建议

这套部署跑起来以后,还需要留几个维护入口。

备份

至少备份这些文件:

protocol trace
/home/yi/blog/                 # Hugo 源站
/home/yi/artalk/artalk.yml     # Artalk 配置,注意 app_key 不要公开
/home/yi/artalk/data/artalk.db # 评论 SQLite 数据库
/etc/nginx/sites-enabled/default
/etc/nginx/conf.d/iotlabx-artalk-rate-limit.conf

SQLite 备份可以很简单:

cp /home/yi/artalk/data/artalk.db /home/yi/artalk/data/artalk.db.bak.$(date +%Y%m%d-%H%M%S)

日志

Artalk 日志:

tail -f /home/yi/artalk/data/artalk.log

nginx 日志通常在:

protocol trace
/var/log/nginx/access.log
/var/log/nginx/error.log

user systemd 服务日志:

journalctl --user -u artalk.service -f

cloudflared 服务日志:

journalctl -u cloudflared.service -f

回滚

如果 Artalk 出问题,先不要影响主站。可以临时在 hugo.toml 里关闭评论:

[params.artalk]
  enabled = false

然后重新构建发布:

hugo --minify
cp -a public/. /var/www/iotlabx/

这样文章页会显示评论占位,不再加载 Artalk 前端资源。

如果 nginx 反代配置出错,优先回滚 nginx 配置备份,然后:

sudo nginx -t
sudo systemctl reload nginx

注意不要把备份文件放在 /etc/nginx/sites-enabled/ 目录里,否则 nginx 可能把备份也当作站点配置加载。

最终验证清单

部署完成后,我会按这个顺序验:

# 1. Artalk 本地服务
systemctl --user status artalk.service --no-pager
curl -I http://127.0.0.1:23366/dist/Artalk.css

# 2. nginx 配置
sudo nginx -t
sudo nginx -T | grep -E 'real_ip_header|set_real_ip_from|limit_req_zone|artalk_comment'
sudo systemctl reload nginx

# 3. 外网资源
curl -I https://iotlabx.com/artalk/dist/Artalk.css
curl -I https://iotlabx.com/artalk/dist/Artalk.js

# 4. Hugo 构建
hugo --minify

# 5. 发布静态文件
cp -a public/. /var/www/iotlabx/

再检查文章页 HTML:

grep -o 'ArtalkComments\|/artalk/dist/Artalk.css\|/artalk/dist/Artalk.js' public/posts/<slug>/index.html

期望结果:

  • ArtalkComments 存在
  • /artalk/dist/Artalk.css 存在
  • /artalk/dist/Artalk.js 存在

这套方案适合什么场景

适合:

  • 个人技术博客
  • Hugo / PaperMod 静态站
  • 树莓派、低功耗主机、小 VPS
  • 希望访客用昵称/邮箱留言
  • 不想强制 GitHub 登录
  • 能接受评论先审核再公开

不适合:

  • 高并发社区
  • 大量实时通知
  • 多管理员复杂权限
  • 希望评论完全免维护

对 iotlabx 这种“个人 IoT 技术实验室”来说,这套组合比较平衡:

protocol trace
Hugo 负责长期内容
PaperMod 负责基础主题
Artalk 负责轻量评论
SQLite 降低运维成本
nginx/Cloudflare 负责挡流量
默认审核负责内容安全

最重要的经验是:评论系统不是只把 JS 挂上去就完事。真正上线时,至少要同时考虑四件事:

protocol trace
身份体验
数据存储
反向代理
滥用防护

少任何一环,都会在真实使用时变成坑。