主题
ZMBlog · 安装文档
适用版本:ZMBlog v2.3.1 阅读对象:部署运维人员
目录
1. 环境要求
1.1 必需
| 项目 | 要求 | 说明 |
|---|---|---|
| Node.js | >= 22.5.0 | 依赖内置的 node:sqlite,低版本会启动失败 |
| 操作系统 | Linux / Windows / macOS | 无原生模块,跨平台一致 |
| 内存 | >= 256 MB | 常规访问场景 |
| 磁盘 | 视内容量而定 | 数据库 + 上传文件 + 日志 |
零第三方依赖:不需要 npm install,不含需要编译的原生模块,也不依赖外部数据库服务。
1.2 检查 Node 版本
bash
node -v
# 期望输出:v22.5.0 或更高若版本过低,请升级 Node.js。也可使用 nvm 管理多版本:
bash
nvm install 22
nvm use 221.3 可选反向代理
生产环境建议置于 Nginx / Caddy / Apache 之后,由代理处理 TLS 与静态文件。程序本身已内置:
- 安全响应头(CSP、X-Frame-Options、X-Content-Type-Options、Referrer-Policy、Permissions-Policy)
- gzip 不做,压缩交给代理
2. 获取程序
将程序目录放到服务器任意位置,例如:
bash
/opt/zmblog目录应包含 server.js、src/、themes/、plugins/、public/。
权限要求:运行用户需对程序目录有读权限,对数据目录有读+写+创建文件权限。
3. 安装
3.1 首次安装
bash
cd /opt/zmblog
node server.js看到类似以下输出即启动成功:
ZMBlog v2.3.0
→ http://localhost:3000
数据目录: /opt/zmblog/data浏览器访问 http://<服务器地址>:3000,会自动跳转至安装向导。
3.2 以 systemd 常驻(推荐)
创建服务文件 /etc/systemd/system/zmblog.service:
ini
[Unit]
Description=ZMBlog
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/zmblog
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
Environment=PORT=3000
Environment=ZMBLOG_SECURE_COOKIE=1
# 部署在 Nginx/Caddy 等反向代理之后时,设 1 才能正确识别访客 IP(登录限流依赖它)
Environment=ZMBLOG_TRUST_PROXY=1
# 若使用独立数据目录,取消下一行注释
# Environment=ZMBLOG_DATA_DIR=/var/lib/zmblog
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target启用:
bash
sudo systemctl daemon-reload
sudo systemctl enable --now zmblog
sudo systemctl status zmblog查看日志:journalctl -u zmblog -f
User=www-data需与数据目录属主一致:sudo chown -R www-data:www-data /opt/zmblog/data
3.3 Windows 服务
用 nssm 注册:
cmd
nssm install ZMBlog "C:\Program Files\nodejs\node.exe" "C:\zmblog\server.js"
nssm set ZMBlog AppDirectory C:\zmblog
nssm set ZMBlog AppEnvironmentExtra PORT=3000 ZMBLOG_SECURE_COOKIE=1
nssm start ZMBlog3.4 Docker
dockerfile
FROM node:22-alpine
WORKDIR /app
COPY . .
ENV PORT=3000 \
ZMBLOG_DATA_DIR=/app/data \
ZMBLOG_SECURE_COOKIE=1
EXPOSE 3000
CMD ["node", "server.js"]bash
docker build -t zmblog .
docker run -d --name zmblog -p 3000:3000 \
-v zmblog-data:/app/data \
-v zmblog-uploads:/app/data/uploads \
-e ZMBLOG_SECURE_COOKIE=1 \
zmblog容器部署务必挂载
data目录为卷,否则容器重建会丢数据。
4. 安装向导详解
首次访问自动进入五步向导。
4.1 第 1 步:环境检测
自动检查四项,全部通过才会进入下一步:
| 检查项 | 通过条件 |
|---|---|
| Node 版本 | >= 22.5.0 |
node:sqlite 模块 | 可成功 require 并打开内存库 |
| 目录可写 | 数据目录及上传目录可写 |
| 数据库可读写 | 能建表、写入、读取 |
任一不通过会显示具体阻塞原因,请按提示修复后刷新页面。
4.2 第 2 步:创建管理员
| 字段 | 规则 |
|---|---|
| 用户名 | 2–32 位,允许字母、数字、下划线、点、@、短横线 |
| 昵称 | 显示名,留空默认同用户名 |
| 邮箱 | 可选,格式须合法 |
| 密码 | 至少 8 位,且强度需达中等(建议混合大小写字母、数字、符号) |
| 确认密码 | 须与密码一致 |
创建成功后立即建立登录会话并下发 CSRF 令牌,后续步骤无需重复登录。
密码强度规则:长度 ≥8、含大小写字母、含数字、含符号,各计一分,总分低于 2 会被拒绝。
4.3 第 3 步:站点信息
| 字段 | 说明 |
|---|---|
| 站点标题 | 显示在浏览器标题与页头,必填 |
| 站点描述 | 用于首页描述与 RSS |
| 站点地址 | 必须以 http:// 或 https:// 开头;影响 RSS、站点地图、规范链接 |
| 作者昵称 | 文章默认作者 |
| 作者邮箱 | 供RSS 使用 |
| 页脚文案 | 页脚附加文字 |
4.4 第 4 步:伪静态
选择预设方案。默认方案为完整的 32 条规则,覆盖首页、文章、独立页面、分类、标签、归档、搜索、RSS、Atom、站点地图、robots.txt。
此处仅是初始化,后续可在「设置 → 伪静态」自由增删改。
4.5 第 5 步:完成
- 标记
installed,安装入口随即关闭 - 补齐缺失的默认配置项(不会覆盖已填写的值)
- 加载默认主题
zm-default - 可勾选「生成示例内容」:创建若干示例文章、页面、分类、标签与评论,便于熟悉功能。正式建站前建议清空
4.6 重新安装
删除 data/zmblog.db 后重启会重新进入向导。此操作会清空全部数据,务必先备份。
5. Web 服务器配置
程序本身可直接对外提供服务,但生产环境建议置于反向代理之后。
5.1 Nginx
nginx
server {
listen 80;
server_name blog.example.com;
# TLS 由 certbot 自动改写
# return 301 https://$host$request_uri;
client_max_body_size 12m; # 需略大于程序的 10 MB 请求体上限
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
proxy_read_timeout 60s;
}
access_log /var/log/nginx/zmblog.access.log;
error_log /var/log/nginx/zmblog.error.log;
}
server {
listen 443 ssl http2;
server_name blog.example.com;
ssl_certificate /etc/letsencrypt/live/blog.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem;
client_max_body_size 12m;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
}
}关键点
client_max_body_size必须大于程序的 10 MB 上限,否则大文件会先被代理拒绝(返回 413)- 必须透传
X-Forwarded-For,否则登录限流会把所有请求视为同一 IP;同时需在服务环境里设ZMBLOG_TRUST_PROXY=1(否则程序不信任该头) - 程序仅监听
3000,建议同时在防火墙上限制该端口只允许本机访问
启用 HTTPS 后,把 ZMBLOG_SECURE_COOKIE=1 加入服务环境并重启。
5.2 Caddy
blog.example.com {
request_body {
max_size 12MB
}
reverse_proxy 127.0.0.1:3000 {
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}Caddy 自动申请并续期证书。
5.3 Apache
启用 proxy_http 与 proxy 模块,在 VirtualHost 中:
apache
<VirtualHost *:443>
ServerName blog.example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/blog.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/blog.example.com/privkey.pem
LimitRequestBody 12582912
ProxyPreserveHost On
ProxyRequests Off
ProxyTimeout 60
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
<Location />
Require all granted
</Location>
</VirtualHost>同时需转发客户端 IP:
apache
RequestHeader set X-Forwarded-For "expr=%{REMOTE_ADDR}"
RequestHeader set X-Forwarded-Proto "expr=%{REQUEST_SCHEME}"Apache 的
AcceptPathInfo与AllowEncodedSlashes需允许 URL 中出现%2F,否则部分别名路径会 404。
6. 伪静态与路径配置
程序内置完整的伪静态路由引擎,无需服务器层面的 rewrite 规则:请求路径由内核的规则表匹配并分发。
6.1 为什么不需要服务器 rewrite
请求 /archives/hello-world/ 时,程序自身按规则表识别它属于「文章:别名」,加载对应文章并渲染。反向代理只需把请求原样转发即可。
6.2 验证方式
安装完成后依次访问下列地址,均应正常返回:
/ 首页
/page/2 首页第 2 页
/archives/ 归档列表
/archive/ 文章归档
/category/<别名> 分类页
/tag/<别名> 标签页
/search/?keyword=关键词 搜索
/feed RSS
/atom Atom
/sitemap 站点地图
/robots.txt robots若有 404,多为反向代理未透传完整 REQUEST_URI,请检查 proxy_pass 是否带了尾部 /。
6.3 自定义路径
「设置 → 伪静态」新增规则,正则与路由表达式双栏填写。完整语法见《使用文档》第 13 节。
7. HTTPS 与安全
7.1 强制 Secure Cookie
生产环境必须设置:
bash
ZMBLOG_SECURE_COOKIE=1否则会话 Cookie 会随 HTTP 请求明文传输。
7.2 内置安全措施
无需额外配置即生效:
| 措施 | 说明 |
|---|---|
| 密码哈希 | scrypt(N=16384, r=8, p=1),常量时间比较 |
| 会话Cookie | HttpOnly + SameSite=Lax,登录态不出现在 JS 可访问范围 |
| CSRF | 会话绑定的随机令牌,全部写操作强制校验 |
| 登录限流 | 按 IP 计数,超阈值锁定 900 秒;锁过期后计数自动归零 |
| 评论防垃圾 | 按 IP 频率限制(默认每 IP 每分钟 10 条,后台可调)+ 链接数判垃圾 |
| 上传白名单 | 仅允许明确列出的扩展名,拒绝可执行类型 |
| 请求体上限 | 10 MB,超出返回 413 |
| 路径校验 | 归一化 + 前缀校验,阻断路径穿越 |
| 模板编辑器 | 禁止写入可被 require 的类型,阻断远程代码执行 |
| 响应头 | CSP、X-Frame-Options、X-Content-Type-Options、Referrer-Policy、Permissions-Policy |
7.3 建议的额外措施
- 用防火墙限制 3000 端口只监听本机
- 定期查看「操作日志」,关注异常登录
- 定期导出备份并存放到异地
- 及时更新 Node.js 安全补丁
8. 数据目录与多实例
8.1 目录结构
data/
├─ zmblog.db SQLite 数据库
├─ uploads/ 上传文件
└─ logs/ 操作日志8.2 自定义数据目录
bash
ZMBLOG_DATA_DIR=/var/lib/zmblog node server.js放到程序目录之外便于权限隔离与备份。
8.3 环境变量一览
| 变量 | 默认值 | 说明 |
|---|---|---|
PORT | 3000 | 监听端口 |
ZMBLOG_DATA_DIR | ./data | 数据目录 |
ZMBLOG_THEMES_DIR | ./themes | 主题目录 |
ZMBLOG_PLUGINS_DIR | ./plugins | 插件目录 |
ZMBLOG_SECURE_COOKIE | 未设 | 设为 1 追加 Secure |
ZMBLOG_TRUST_PROXY | 未设 | 设为 1 信任反向代理的 X-Forwarded-For |
ZMBLOG_DEBUG | 未设 | 设为 1 开启调试(错误页输出堆栈,仅限开发环境) |
8.4 多实例
同一台服务器跑多个博客,只需给每个实例分配不同的端口与数据目录:
bash
PORT=3001 ZMBLOG_DATA_DIR=/var/lib/blog-a node /opt/zmblog/server.js
PORT=3002 ZMBLOG_DATA_DIR=/var/lib/blog-b node /opt/zmblog/server.js各实例之间数据、主题配置、伪静态规则完全隔离。若需要共享主题或插件目录,可显式设置 ZMBLOG_THEMES_DIR / ZMBLOG_PLUGINS_DIR 指向同一位置——但要注意在线模板编辑会影响所有引用该目录的实例。
9. 备份与恢复
9.1 备份内容
完整备份需包含数据目录与自定义代码两部分:
bash
# 数据(数据库 + 上传 + 日志)
tar czf zmblog-data-$(date +%F).tar.gz data/
# 代码(含自定义主题与插件)
tar czf zmblog-code-$(date +%F).tar.gz --exclude=data --exclude=.testdata .9.2 数据库在线备份
服务运行中直接复制 .db 文件可能得到不一致快照,请用 SQLite 的在线备份能力:
bash
node -e "
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync('data/zmblog.db');
db.exec(\"VACUUM INTO 'data/backup.db'\");
db.close();
console.log('备份完成');
"9.3 定时备份示例
cron
# 每天凌晨 3:17 备份(错开整点避免 IO 峰值)
17 3 * * * cd /opt/zmblog && tar czf /backup/zmblog-$(date +\%F).tar.gz data/ && find /backup -name 'zmblog-*.tar.gz' -mtime +14 -delete9.4 恢复
bash
# 停止服务
sudo systemctl stop zmblog
# 覆盖数据目录
cd /opt/zmblog
rm -rf data && tar xzf /backup/zmblog-2026-10-01.tar.gz
# 修回属主
chown -R www-data:www-data data
# 启动
sudo systemctl start zmblog9.5 内容级备份
若只需文章内容而不需完整站点,可在后台「数据 → 导出」得到 JSON。该导出不含管理员密码哈希与 installed 标志,导入到新站点后需重新创建管理员。
10. 升级
10.1 标准流程
bash
# 1. 停止服务
sudo systemctl stop zmblog
# 2. 备份
tar czf /backup/before-upgrade-$(date +%F).tar.gz /opt/zmblog
# 3. 覆盖代码(保留 data/ 与自定义主题插件)
# 推荐用 git 拉取;否则仅覆盖 src/ server.js package.json themes/ plugins/ public/
cd /opt/zmblog && git pull
# 4. 启动(会自动执行数据库迁移)
sudo systemctl start zmblog
# 5. 验证
curl -I http://127.0.0.1:300010.2 数据库迁移
启动时自动执行,幂等且可重复运行:
- 补齐缺失的表
- 补齐缺失的列(如
posts.excerpt、sessions.ip、sessions.csrf、users.authSecret) - 补齐新增的默认选项
迁移只做增量,不会删除已有数据或列。
10.3 自定义代码保护
themes/ 与 plugins/ 是用户代码所在。若你修改过内置主题或插件,请:
- 升级前把自定义目录复制到程序外备份
- 升级后对比差异再合并
或把你的主题与插件放到独立目录,通过 ZMBLOG_THEMES_DIR / ZMBLOG_PLUGINS_DIR 指向它,与程序升级解耦。
11. 卸载
bash
sudo systemctl stop zmblog
sudo systemctl disable zmblog
rm /etc/systemd/system/zmblog.service
sudo systemctl daemon-reload
rm -rf /opt/zmblog12. 故障排查
12.1 启动即失败
| 现象 | 原因 | 处理 |
|---|---|---|
Cannot find module 'node:sqlite' | Node 版本过低 | 升级到 >= 22.5.0 |
EADDRINUSE | 端口被占用 | PORT=3001 node server.js,或 ss -lntp | grep 3000 找到占用进程 |
EACCES: permission denied | 目录权限不足 | chown -R www-data:www-data /opt/zmblog/data |
SQLITE_CANTOPEN | 数据目录不存在或不可写 | mkdir -p data && chown www-data data |
12.2 安装相关
进入向导后一直重定向 检查浏览器是否缓存了 Cookie;或 data/zmblog.db 处于半写入状态。停服后删除该文件重试。
向导第 1 步环境检测不通过 页面会列出具体阻塞项,按提示修复后刷新。
管理员创建失败:用户名已被占用 该用户名已存在。更换用户名,或先删除数据库重新安装。
安装完成站点标题仍是默认值 说明站点信息未写入。到「设置 → 站点信息」重新保存一次即可。
12.3 前台访问
全部页面 404 反向代理未透传完整路径。检查 Nginx 是否把 proxy_pass 写成了带尾部 / 的形式,导致前缀被吞掉。
中文别名 404 Apache 需允许编码斜杠:设置 AllowEncodedSlashes NoDecode,并确保 AllowOverride 允许该指令。
RSS / sitemap 中链接是内网地址 「站点信息」里的站点地址填错了。该值决定 RSS、站点地图、规范链接中的绝对 URL。
上传后图片不显示 确认引用的是上传接口返回的 /upload/<文件名> 完整路径。若经代理,检查是否额外配置了静态目录规则导致请求未转发到程序。
大文件上传返回 413 程序上限为 10 MB 请求体 / 8 MB 单文件。Nginx 的 client_max_body_size 需设为12 MB 以上,否则会被代理先行拒绝。
12.4 后台操作
登录后提示未登录或登录已过期 Cookie 未正常保存。检查:是否启用了 HTTPS 但未设置 ZMBLOG_SECURE_COOKIE=1(浏览器拒绝在 HTTP 上发送 Secure Cookie);或反向代理未透传协议。
写操作被拒绝:缺少 CSRF 令牌 后台页面与 API 同源时不会出现。若通过脚本调用,需在登录后读取 csrf 并放入 X-CSRF-Token 请求头。
登录被锁定 连续失败达阈值会锁定 900 秒,等待后重试;或停服后清理 login_attempts 表。
忘记管理员密码 停服后删除 data/zmblog.db 重新安装(会清空全部数据),或从备份恢复。更推荐的做法是用 user 表的 secret 与 password 字段配合 src/core/auth.js 的 hashPassword 离线生成新哈希后更新。
12.5 性能
数据库锁冲突 避免多进程同时写同一数据目录。备份请用 VACUUM INTO(在线备份)而非直接复制文件。
日志表过大 后台「日志 → 清空」定期清理。