目标:不是只跑起来,而是长期可维护
这次搭 iotlabx,不是想做一个“能访问的首页”就结束,而是希望它能成为一个长期维护的个人技术实验室网站。
基本要求是:
- 内容主站用 Hugo,尽量静态化,方便部署和备份。
- 主题用 PaperMod,在它的基础上做有限定制,而不是完全重写一套主题。
- 文章结构要适合技术博客:协议分析、嵌入式、Qt 工程、项目记录、实验室入口都能分区沉淀。
- 动态能力尽量作为独立轻服务挂进来,例如评论、设备状态、Office UI 等。
- 部署环境是树莓派,同机还跑其他服务,所以不能上重型 SSR、复杂数据库或一堆常驻进程。
- 评论系统要支持访客自定义昵称和邮箱,不强制读者登录 GitHub。
最终选型是:
Hugo -> 静态内容生成
PaperMod -> 基础主题
nginx -> 静态文件服务 + 反向代理
Artalk -> 自托管评论系统
SQLite -> 评论数据存储
systemd -> 让 Artalk 常驻运行
Cloudflare/Tunnel -> 公网入口与边缘防护
这篇按真实实施过程写,不只贴最终配置,也记录几个容易踩的坑。
站点目录与部署方式
站点源码目录:
/home/yi/blog
Hugo 构建输出目录:
/home/yi/blog/public
nginx 对外服务目录:
/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输出类似:
aarch64
Debian GNU/Linux 13 (trixie)
Hugo 推荐安装 extended 版本。PaperMod 本身不一定强依赖 extended,但技术博客后续如果要用 Sass/SCSS、图片处理或主题扩展,直接装 extended 版本更省心。
先确认本机架构:
uname -m常见对应关系:
aarch64 / arm64 -> Linux ARM64
x86_64 -> Linux AMD64
到 Hugo GitHub Releases 找对应包,例如 ARM64:
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本次实际环境版本是:
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,也可以直接下载主题到:
themes/PaperMod
这次站点目录本身不是 git 仓库,所以维护方式更偏本地文件部署。无论哪种方式,Hugo 只要能找到:
theme = "PaperMod"就可以工作。
基础 hugo.toml
主配置文件是:
/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这里有一个经验:技术站不要只按“文章/关于”分。协议、项目、工具、实验室这些入口能帮助后续内容持续归档。
内容结构
当前站点内容目录大致是:
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 会自动合并:
assets/css/extended/*.css
所以可以把自定义样式放到:
assets/css/extended/z-article.css
这样不需要直接改 PaperMod 主题源码。
实践里有两条经验:
- 能用 Hugo layout 覆盖的,就不要改 theme 目录。
- 能放到
assets/css/extended/的,就不要在模板里堆内联样式。
配置 nginx 静态站点
nginx 的职责有两个:
- 服务 Hugo 构建出来的静态文件。
- 后面把
/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”,而是:
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。也就是说:
域名注册商:阿里云
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
大致流程是:
- 在 Cloudflare 添加站点,例如
iotlabx.com。 - Cloudflare 会分配两条 Nameserver,例如:
xxx.ns.cloudflare.com
yyy.ns.cloudflare.com
- 回到阿里云域名控制台,找到域名的 DNS / Nameserver 设置。
- 把阿里云默认 NS 替换成 Cloudflare 给出的两条 NS。
- 等待 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本次实际环境是:
cloudflared version 2026.3.0
创建 Tunnel
Cloudflare Zero Trust 控制台路径大致是:
Zero Trust -> Networks -> Tunnels -> Create a tunnel
选择 Cloudflared 类型,创建一个 tunnel,例如:
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正常状态应为:
Active: active (running)
配置 Public Hostname
在 tunnel 的 Public Hostnames 里添加路由:
Hostname: iotlabx.com
Service: http://localhost:80
如果还需要子域名,可以继续添加:
Hostname: office.iotlabx.com
Service: http://localhost:19000
Hostname: status.iotlabx.com
Service: http://localhost:80
本篇主站只需要:
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 规则:
URI Path equals /artalk/api/v2/comments
策略可以从温和开始:
10 requests / 1 minute / IP -> Managed Challenge
如果被打得很明显,再改成 Block。
原则是:普通垃圾评论交给审核,瞬时攻击尽量挡在 Cloudflare,别让树莓派扛。
为什么评论系统选 Artalk
最初也考虑过 GitHub Discussions 类评论方案。它维护简单,适合开发者读者多的站点。但这类方案通常要求读者使用 GitHub 身份,不适合希望访客直接填昵称、邮箱、网站来留言的独立博客。
我希望评论区更像传统技术博客:
昵称:读者自己填
邮箱:用于头像、通知或身份识别
网站:可选
内容:直接留言
所以最终选了 Artalk:
- 支持昵称/邮箱/网站留言。
- 可自托管。
- Go 单二进制,适合 ARM64 树莓派。
- 可以用 SQLite。
- 能接入审核、关键词、验证码、通知等能力。
- 前端可以直接挂进 Hugo 模板。
Artalk 不是“零运维”,但对于个人站来说,它在功能和复杂度之间比较平衡。
加入 Artalk 后的整体架构
加入评论后,完整链路变成:
浏览器
|
| 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。这里使用站点自己的覆盖模板:
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 包名类似:
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 + 本地监听
配置文件:
/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 服务:
/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正常应该返回:
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。
如果请求是:
浏览器 -> 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;
}当前策略是:
真实客户端 IP 每分钟 5 次评论提交
突发最多 3 次
真实客户端 IP 并发最多 3 个连接
单次请求体最大 500K
压测时连续 POST 十次,后面会出现 503,说明 nginx 限流已经触发。若压测结果表现为不同公网 IP 互相影响,优先检查 nginx 是否正确还原了真实客户端 IP。
这里有个小坑:备份 nginx 配置时,不要把备份文件放在 /etc/nginx/sites-enabled/ 里。
Debian 系 nginx 会加载这个目录下的所有文件。如果你把备份放成:
/etc/nginx/sites-enabled/default.bak.20260509-221208
nginx 会把它也当成站点配置加载,结果可能报:
a duplicate default server for 0.0.0.0:80
备份应该放到不会被 nginx include 的目录,例如:
/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,于是访问后台接口时服务端一直返回:
需要管理员权限
页面表现就是:反复让你输入密码,或者删除评论时又跳回登录。
排查方法:
- 看 Artalk 日志:
/home/yi/artalk/data/artalk.log - 如果是密码错,会看到:
密码错误 - 如果是旧 token/非管理员身份,会看到:
需要管理员权限
解决办法:
- 避免同一个邮箱既做普通测试用户,又做管理员。
- 管理员邮箱只用于 Dashboard 审核。
- 普通评论测试换另一个邮箱,例如
[email protected]。 - 如果已经混用了,清浏览器站点数据,或直接用无痕窗口重新登录。
- 必要时清理数据库里重复邮箱的普通用户。
另外建议明确设置登录有效期:
login_timeout: 604800这表示管理员 token 有效期 7 天。
运维与回滚建议
这套部署跑起来以后,还需要留几个维护入口。
备份
至少备份这些文件:
/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.lognginx 日志通常在:
/var/log/nginx/access.log
/var/log/nginx/error.log
user systemd 服务日志:
journalctl --user -u artalk.service -fcloudflared 服务日志:
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 技术实验室”来说,这套组合比较平衡:
Hugo 负责长期内容
PaperMod 负责基础主题
Artalk 负责轻量评论
SQLite 降低运维成本
nginx/Cloudflare 负责挡流量
默认审核负责内容安全
最重要的经验是:评论系统不是只把 JS 挂上去就完事。真正上线时,至少要同时考虑四件事:
身份体验
数据存储
反向代理
滥用防护
少任何一环,都会在真实使用时变成坑。
回复 / 留言