Skip to content

ZMBlog · 安装文档 ​

适用版本:ZMBlog v2.3.1  阅读对象:部署运维人员


目录 ​

  1. 环境要求
  2. 获取程序
  3. 安装
  4. 安装向导详解
  5. Web 服务器配置
  6. 伪静态与路径配置
  7. HTTPS 与安全
  8. 数据目录与多实例
  9. 备份与恢复
  10. 升级
  11. 卸载
  12. 故障排查

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 22

1.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 ZMBlog

3.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 与安全 ​

生产环境必须设置:

bash
ZMBLOG_SECURE_COOKIE=1

否则会话 Cookie 会随 HTTP 请求明文传输。

7.2 内置安全措施 ​

无需额外配置即生效:

措施说明
密码哈希scrypt(N=16384, r=8, p=1),常量时间比较
会话CookieHttpOnly + 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 环境变量一览 ​

变量默认值说明
PORT3000监听端口
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 -delete

9.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 zmblog

9.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:3000

10.2 数据库迁移 ​

启动时自动执行,幂等且可重复运行:

  • 补齐缺失的表
  • 补齐缺失的列(如 posts.excerpt、sessions.ip、sessions.csrf、users.authSecret)
  • 补齐新增的默认选项

迁移只做增量,不会删除已有数据或列。

10.3 自定义代码保护 ​

themes/ 与 plugins/ 是用户代码所在。若你修改过内置主题或插件,请:

  1. 升级前把自定义目录复制到程序外备份
  2. 升级后对比差异再合并

或把你的主题与插件放到独立目录,通过 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/zmblog

12. 故障排查 ​

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(在线备份)而非直接复制文件。

日志表过大 后台「日志 → 清空」定期清理。


相关文档 ​

ZMBlog · MIT License