Kutt 3.2.6 日常运维与升级指南

Kutt 3.2.6 生产运维实录:三服务栈日常巡检清单、pg_dump 备份与恢复演练、健康探针与日志轮转、v3.2.5↔v3.2.6 升级回滚实测(迁移幂等)、过期链接 cron 自动清理验证与资源水位基线。

KuttKutt自托管DockerNode.jsPostgreSQL2026/11/02

Kutt 3.2.6 日常运维与升级指南

目标:覆盖 Kutt v3.2.6 生产栈(应用 + PostgreSQL 16 + Redis 7)的日常巡检、备份恢复、监控告警、扩缩容、升级回滚与故障处置主线;所有操作已在 Ubuntu 24.04 宿主机实测栈(端口 8153)上执行并留存回显(2026-11-01)。栈的搭建过程见 《Kutt 3.2.6 Docker Compose 部署指南》。

一、环境信息

  • 操作系统:Ubuntu 24.04.5 LTS(x86_64),Docker 29.9.2 / Compose v5.6.1
  • 应用:kutt/kutt:v3.2.6(容器 kutt-3.2.6-server,宿主机端口 8153→3000)
  • 数据库:postgres:16-alpine(容器 kutt-3.2.6-postgres,库名 kutt,不对外暴露)
  • 缓存/队列:redis:7-alpine(容器 kutt-3.2.6-redis,不对外暴露)
  • 部署目录:/opt/kutt(compose 项目 + .env + data/pg、data/redis、custom 三处 bind mount)
  • 管理员:admin@kutt.local / q1w2e3r4;API key 走 x-api-key 头
  • 实测资源水位:应用 52.9MiB、Postgres 96.8MiB、Redis 33.4MiB(空闲态)

二、日常巡检

日常巡检回答四个问题:服务活着吗、数据在涨吗、队列堵了吗、磁盘够吗。以下命令按序执行即为一次完整巡检。

2.1 服务状态与健康检查

cd /opt/kutt && docker compose ps --format '{{.Name}} {{.Image}} {{.Status}}'
kutt-3.2.6-postgres postgres:16-alpine Up 12 minutes (healthy)
kutt-3.2.6-redis redis:7-alpine Up 12 minutes
kutt-3.2.6-server kutt/kutt:v3.2.6 Up 10 seconds
curl -s http://localhost:8153/api/v2/health; echo
OK

/api/v2/health 返回纯文本 OK 即应用就绪(该端点无认证,专为探针设计);Postgres 的 (healthy) 来自 compose 内置 pg_isready 健康检查。

2.2 业务数据水位

docker exec kutt-3.2.6-postgres psql -U postgres -d kutt -t -c \
  "SELECT (SELECT count(*) FROM links) AS links, (SELECT count(*) FROM users) AS users, (SELECT count(*) FROM visits) AS visit_rows;"
     3 |       1 |        1

links 为现存短链数(过期与已删不计)、visits 按"每链每天一行"聚合增长,长期远大于 365×链数说明异常(可查 countries/referrers 是否被爬虫灌入)。

2.3 队列与缓存

docker exec kutt-3.2.6-redis redis-cli keys '*'
s:1
u:7sGm14IdbxoknkpnItuKQVRTMBaucEQscN7kAXsJ
l:kutt-proj:
bull:visit:id

键含义:s:<链接ID> 统计缓存(TTL 60s)、u:<apikey> 用户缓存、l:<address>: 跳转缓存、bull:visit:* 访问事件队列。巡检重点看 bull:visit 积压:

docker exec kutt-3.2.6-redis redis-cli llen bull:visit:wait
(integer) 0

持续大于百级说明消费端(应用进程)异常,结合 2.4 日志定位。

2.4 日志与磁盘

docker compose logs server --since 10m | grep -iE "error|exit" | head
du -sh data/pg data/redis custom
(无输出——10 分钟内无错误)
63M	data/pg
8.0K	data/redis
4.0K	custom

日志轮转实测由宿主机 Docker daemon 默认策略接管:

docker inspect kutt-3.2.6-server --format '{{.HostConfig.LogConfig.Type}} {{.HostConfig.LogConfig.Config}}'
json-file map[max-file:3 max-size:10m]

即单容器日志最多 3×10MB,无需额外配置 logrotate;若宿主机 daemon 无此默认值,应在 compose 的 server 服务显式加 logging: { driver: json-file, options: { max-size: "10m", max-file: "3" } }。

💡 说明:data/pg 63MB 中大部分是 Postgres 初始集群骨架,Kutt 自身数据(3 链 1 用户)占比极小;短链业务天然写多读少但行极小,磁盘增长主要来自 visits 表,按年清理旧行即可(见 7.4)。

三、备份与恢复

Kutt 栈的全部状态 = Postgres 库 + custom 静态目录(Redis 仅缓存与瞬态队列,可不备)。

3.1 备份三件套

cd /opt/kutt
mkdir -p backup/$(date +%F)
docker exec kutt-3.2.6-postgres pg_dump -U postgres -d kutt > backup/$(date +%F)/kutt.sql
tar czf backup/$(date +%F)/custom.tar.gz custom/
cp .env docker-compose.yml backup/$(date +%F)/
ls -lh backup/$(date +%F)/
-rw-r--r-- 1 root root  46K Nov  1 16:45 kutt.sql
-rw-r--r-- 1 root root  167 Nov  1 16:45 custom.tar.gz
-rw------- 1 root root  176 Nov  1 16:45 .env
-rw-r--r-- 1 root root 1.5K Nov  1 16:45 docker-compose.yml

实测说明两点:① pg_dump 默认输出 COPY 格式(本次 796 行、8 个 COPY 数据段),恢复速度快、体积小,无需 --inserts;② 备份 .env 时注意其含 JWT_SECRET 与库口令,chmod 600 并纳入密管,泄露等同泄露全部登录态。

3.2 恢复演练(实测通过)

恢复到新库验证备份可用性(不影响生产库):

docker exec kutt-3.2.6-postgres psql -U postgres -c "CREATE DATABASE kutt_restore;"
docker exec -i kutt-3.2.6-postgres psql -U postgres -d kutt_restore < backup/$(date +%F)/kutt.sql
docker exec kutt-3.2.6-postgres psql -U postgres -d kutt_restore -t -c "SELECT count(*) FROM links;"
CREATE DATABASE
ALTER TABLE
ALTER TABLE
ALTER TABLE
     3

链接数与生产一致,演练成功后清理:

docker exec kutt-3.2.6-postgres psql -U postgres -c "DROP DATABASE kutt_restore;"

整栈迁移(换机)流程:新机建好 /opt/kutt 目录结构 → 放入 docker-compose.yml、.env、custom/ → docker compose up -d postgres redis → docker exec -i <pg容器> psql -U postgres < kutt.sql(首次启动空库自动建表结构由 compose 的 POSTGRES_DB 保证,恢复前需确认 kutt 库存在且为空)→ docker compose up -d server。

⚠️ 注意:不要用"直接拷贝 data/pg 目录"跨版本迁移——Postgres 数据目录与镜像小版本绑定,逻辑备份(pg_dump)才是可移植路径。同版本整目录拷贝则可行(本文 bind mount 即为此设计)。

3.3 SQLite 形态备份

若采用单容器 SQLite 形态(DB_CLIENT=better-sqlite3),备份退化为复制数据文件(实测位于容器 /kutt/db/data,106KB 起步;官方 compose 挂载路径为 /var/lib/kutt):

docker exec kutt-server sqlite3 /kutt/db/data ".backup '/tmp/kutt-backup.sqlite'"
docker cp kutt-server:/tmp/kutt-backup.sqlite backup/$(date +%F)/

用 .backup 而非 cp:前者走 SQLite 在线备份协议,避免写入中途拷贝出撕裂页。

四、监控告警

最小可用监控三件套(均可直接落到 Prometheus/Blackbox 或 crontab 脚本):

  1. 存活探针:GET http://<host>:8153/api/v2/health 期望 200 + 正文 OK,建议 30s 间隔、连续 3 次失败告警;
  2. 队列积压:redis-cli llen bull:visit:wait 持续 >100 告警(消费端异常);
  3. 证书/域名:短链服务域名与证书过期是最高危故障(所有跳转失效),纳入现有域名到期监控即可。

无专用 exporter;如需指标化,Postgres/Redis 用通用 exporter(postgres_exporter、redis_exporter)采集连接数与内存,Kutt 应用层指标以 health + 日志关键字(error)为准。

五、扩容与缩容

Kutt 应用进程无本地会话状态(登录态为签名 JWT Cookie,API key 存库),天然支持水平多副本:

  1. 将 server 服务副本数提升(Postgres/Redis 保持单实例共享):
docker compose up -d --scale server=2

⚠️ 注意:--scale 与固定 container_name 互斥,多副本前需从 compose 中删除 server 的 container_name 行,并把 ports 改为交给前置 Nginx 负载均衡(宿主机 8153 只能被一个副本占用)。

  1. 多副本共享同一 DEFAULT_DOMAIN 与同一 JWT_SECRET(必须一致,否则 A 节点签发的 JWT 在 B 节点验签失败);
  2. 官方 compose 的本地构建镜像内含 custom 挂载,各副本共享同一 custom 卷。

缩容即反向操作(--scale server=1)。单机场景无需扩缩——实测空闲内存水位不足 200MB(三容器合计),垂直扩容(加 CPU/内存)即可覆盖到数十万链接量级;真正的容量瓶颈在 Postgres 单库写入,届时优先清理 visits 历史行(见 7.4)而非加副本。

六、升级与回滚

6.1 升级流程(实测 v3.2.5 → v3.2.6)

Kutt 的镜像启动命令自带 knex migrate:latest,升级 = 换 tag + 重建应用容器,数据库迁移自动完成且幂等。实测步骤:

cd /opt/kutt
docker pull kutt/kutt:v3.2.6
sed -i 's|kutt/kutt:v3.2.5|kutt/kutt:v3.2.6|' docker-compose.yml
cp .env backup/pre-upgrade-$(date +%F).env   # 配置留档
docker compose up -d server
docker compose logs server --since 1m | tail -5
> kutt@3.2.6 start
> node server/server.js --production

> Ready on http://localhost:3000

验证升级成功且数据无损:

curl -s http://localhost:8153/api/v2/health && echo
curl -s "http://localhost:8153/api/v2/links" -H "x-api-key: <APIKEY>" | python3 -c "import json,sys; print('links total:', json.load(sys.stdin)['total'])"
OK
links total: 2

若目标版本含新迁移,日志会出现 Batch N run: M migrations(首启实测 Batch 1 run: 10 migrations);无新迁移时输出 Already up to date(实测 v3.2.5/v3.2.6 间迁移列表一致,均为 10 个迁移、最新 20250106070444_remove_cooldown.js)。

6.2 回滚

knex 迁移只前进不回退(镜像 CMD 无 migrate:rollback),因此回滚策略取决于新迁移是否改表:

  • 3.2.x 补丁版之间(如 v3.2.6 → v3.2.5):迁移集合相同,直接改 tag 重建即可,实测双向切换均正常、数据完整;
sed -i 's|kutt/kutt:v3.2.6|kutt/kutt:v3.2.5|' docker-compose.yml
docker compose up -d server
Already up to date
> kutt@3.2.5 start
> Ready on http://localhost:3000
  • 跨小版本回滚(新迁移已执行):先 pg_dump 恢复升级前备份(3.2 流程),再换旧镜像——这就是 6.1 中"配置留档 + 备份先行"的原因。

6.3 版本节奏与渠道

GitHub Releases 实测近 5 版:v3.2.6(2026-07-05)、v3.2.5 / v3.2.4(2026-05-17)、v3.2.3(2025-02-06)、v3.2.2(2025-01-24)。3.2.x 为稳定维护线;不要从 2.x 直升 3.x——2.x 用 MongoDB、3.x 用关系库,数据模型完全不同,需按官方迁移指引重建数据(或选 D4 类迁移方案)。镜像 tag 与 Release 同名(带 v 前缀),生产固定版本号、避免 latest。

七、故障排查手册

按"现象 → 定位 → 处置"速查(完整问答见下一章与常见问题篇):

7.1 应用容器反复重启

docker compose logs server | grep -A3 "Missing environment"
================================
 Missing environment variables:
    JWT_SECRET: undefined
================================
 Exiting with error code 1

生产模式缺 JWT_SECRET 即退出(实测)。补齐 .env 后 docker compose up -d。

7.2 短链跳转 502/挂起

依次检查:docker compose ps(server 是否 Up)→ pg_isready(库健康)→ redis-cli ping。实测故障矩阵:REDIS_ENABLED=true 时 Redis 宕机会让跳转与建链请求直接挂起直至客户端超时(ioredis 重试阻塞请求线程,实测 8 秒超时无响应),Postgres 宕机同样挂起——即开启 Redis 后它是跳转链路的强依赖而非软依赖。应急两种:恢复 Redis;或临时把 REDIS_ENABLED 置 "false" 重建 server(跳转退回纯 Postgres 路径,统计失去队列)。两库任一恢复后应用自动自愈,无需重启(实测 stop/start postgres、stop/start redis 后跳转恢复 302)。

7.3 统计计数不动

三查:① 访客 UA 是否被 isbot 过滤(curl 默认 UA 不计数,实测 5 次访问计数为 0);② 队列积压(llen bull:visit:wait);③ stats 接口 60 秒缓存。处置:带浏览器 UA 复测 → 重启 server 消费积压 → 等 1 分钟复查。

7.4 visits 表膨胀

docker exec kutt-3.2.6-postgres psql -U postgres -d kutt -t -c \
  "SELECT pg_size_pretty(pg_total_relation_size('visits'));"

超阈值时清理 N 天前聚合行(不删 links,仅历史统计):

docker exec kutt-3.2.6-postgres psql -U postgres -d kutt -c \
  "DELETE FROM visits WHERE created_at < now() - interval '365 days';"

7.5 过期链接未清理

内置 cron 每 30 秒删除 expire_in 已过期的链接(实测 1 分钟有效期链接在 90 秒后自动消失,见 8.3)。若未生效,检查 server 进程是否存活(cron 在应用进程内,无独立服务)与日志报错。

八、常见问题

8.1 更换 JWT_SECRET 的后果

所有已登录会话与已签发 JWT 立即失效(用户需重新登录),API key 不受影响(存库)。仅密钥泄露时更换,并同步所有副本。

8.2 忘记密码 / 找回

POST /api/v2/auth/reset-password 依赖 SMTP(MAIL_ENABLED=true)。未配邮件时唯一路径:直接改库——

docker exec kutt-3.2.6-postgres psql -U postgres -d kutt -c \
  "UPDATE users SET password='\$2a\$12\$<新bcrypt哈希>' WHERE email='admin@kutt.local';"

哈希在应用容器内生成(bcryptjs 已在依赖中):

docker exec kutt-3.2.6-server node -e "require('bcryptjs').hash('q1w2e3r4', 12).then(h=>console.log(h))"

8.3 有效期链接的自动删除实测

curl -s -X POST http://localhost:8153/api/v2/links -H "x-api-key: <APIKEY>" \
  -H 'Content-Type: application/json' \
  -d '{"target":"https://example.com/expire-test","expire_in":"1m"}' | \
  python3 -c "import json,sys; d=json.load(sys.stdin); print(d['address'], d['expire_in'])"
sleep 90
curl -s "http://localhost:8153/api/v2/links" -H "x-api-key: <APIKEY>" | \
  python3 -c "import json,sys; d=json.load(sys.stdin); print('total:', d['total']); [print(x['address']) for x in d['data']]"
8ecnU2 2026-11-01T16:47:58.085Z
total: 2
kutt-proj
6ZErgk

8ecnU2 已不在列表——30 秒周期 cron 生效。另实测 expire_in 下限 1 分钟:"30s" 返回 Expire time should be more than 1 minute.(HTTP 400)。

8.4 备份需要停服吗

不需要。pg_dump 使用一致性快照;Redis 数据为缓存+瞬态队列,可接受重建;custom 为静态文件。建议低峰期每日一次全量 pg_dump,保留 7–30 天。

九、参考链接

Kutt 3.2.6 日常运维与升级指南 - Quzuo