项目管理中的重复任务,这才是正确做法

升级 Specivo,就是切换到更新的 specivo/specivo Docker 镜像。容器入口脚本会在启动时自动执行新的数据库迁移,所以整个流程很直接:备份、固定目标版本、拉取镜像、重启、验证。本文以从 0.1.9 升级到 0.1.10 为例,操作环境与安装指南相同,是一台 1 vCPU / 2 GB 内存的 Ubuntu 24.04 云主机。

全文的工作目录都是 /opt/specivo,安装指南把 Compose 文件、.env 和 specivo-data/ 目录都放在这里。

你将完成的步骤

  • 动手前先确认整个服务栈运行正常
  • 导出数据库并打包数据目录
  • 在 .env 中固定目标版本
  • 拉取新镜像并重启服务栈
  • 验证所有组件都已在新版本上正常运行

开始之前

你需要一套已安装好的 Specivo(按安装指南部署的环境)、root 的 shell 访问权限,以及几分钟时间。拉取任何东西之前,先阅读目标版本的发布说明:CHANGELOG 在代码仓库里,每个 GitHub Release 也都链接到它。先弄清变更内容,再执行升级。

第 1 步:确认当前运行的版本

SSH 登录服务器,进入安装目录:

cd /opt/specivo

确认所有容器都处于健康状态:

docker compose ps

你应该看到六个容器:nginx、api、db、redis、celery-worker 和 celery-beat,状态全部为 Up (healthy)。如果有容器在反复重启或不健康,先解决这个问题再继续。

查看当前的数据库 schema 版本:

docker compose exec -T api alembic current

记下输出。本文示例中是 0020 (head)。升级完成后还要再查一次。

检查磁盘空间。拉取会下载新的镜像层,备份归档在转移到其他主机之前也会一直占用磁盘:

df -h /

本文用的这台主机 48 GB 磁盘还剩 43 GB,足够容纳拉取的镜像和大约 280 MB 的备份归档。

第 2 步:先做备份

每次升级都必须执行这一步。迁移可能会修改数据库 schema,而且不一定可逆。如果迁移已经执行、升级却出了问题,备份是唯一的退路。不要跳过。

如果还没有备份目录,先创建一个:

mkdir -p /opt/specivo-backups

导出数据库。必须加 -T 参数,这样才不会分配伪终端,输出才能正确通过管道写入文件:

docker compose exec -T db pg_dump -U specivo specivo \
  > /opt/specivo-backups/backup-$(date +%Y%m%d-%H%M%S).sql

本文导出的文件约 210 KB,是一份完整的纯 SQL 快照,包含每一个工作项(Issue)、维基(Wiki)页面、评论和历史记录。

打包数据目录。这会把附件、日志、主题、内置数据库文件以及磁盘上的嵌入模型一并打包:

tar czf /opt/specivo-backups/specivo-data-$(date +%Y%m%d-%H%M%S).tar.gz specivo-data

本文的归档约 279 MB,其中大部分是约 400 MB 的嵌入模型(在磁盘上本身已是压缩格式)和 PostgreSQL 数据文件。

继续之前,把这两个文件转移到主机之外:对象存储、外接硬盘或远程服务器都可以。只放在待升级这台机器上的备份,不算真正的备份。

第 3 步:固定目标版本

用编辑器打开 .env,把 SPECIVO_VERSION 从当前值改成你要升级到的版本:

SPECIVO_VERSION=0.1.10

如果你的 .env 还保留着初次安装时的 SPECIVO_VERSION=latest,现在正是改掉的好时机。固定到明确的标签,意味着每次升级的时机由你决定,而且你始终清楚运行的是哪个镜像。回滚也同样简单:重新固定到旧标签即可。

第 4 步:拉取新镜像

docker compose pull

Compose 只会下载有变化的镜像层。拉取结束时会输出类似这样的一行:

Image specivo/specivo:0.1.10 Pulled

db(pgvector)、redis 和 nginx 镜像不属于 Specivo 的发布周期,除非你改过它们固定的版本,否则这一步不会重新拉取。

如果你在 .env 中设置了 SPECIVO_IMAGE=ghcr.io/specivo/specivo(用于无法访问 Docker Hub 的服务器),同一镜像会从 GitHub Container Registry 拉取,输出行会显示为 Image ghcr.io/specivo/specivo:<version> Pulled。从 0.12.0 起,每个版本都会以相同的标签同时发布到两个镜像仓库。

第 5 步:重启服务栈

docker compose up -d

Compose 会把运行中的容器与 Compose 文件对比,只重建镜像发生变化的容器。本次升级重建了三个:specivo-api-1、specivo-celery-worker-1 和 specivo-celery-beat-1。db、redis 和 nginx 的镜像没有变化,容器原样保留。

Compose 会等 db 和 redis 报告健康后再启动 api,启动顺序不需要你自己管理。

迁移会自动执行。入口脚本每次启动都会运行 alembic upgrade head。你会在 API 日志中看到类似这样的输出:

Running database migrations...
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

如果某个版本确实包含 schema 变更,这些行之间还会出现 Running upgrade <rev> -> <rev>,也就是 alembic 按顺序逐个执行新迁移。

你永远不需要手动运行迁移命令。盯着日志,确认启动过程没有报错:

docker compose logs -f api

看到 Application startup complete. 后按 Ctrl-C 停止跟踪日志。

有一点需要说明:这次从 0.1.9 到 0.1.10 的升级没有新增 schema 迁移,alembic current 在升级前后都是 0020 (head)。入口脚本依然执行了迁移步骤,这一步是幂等的。其他版本之间的升级可能包含 schema 迁移,正因如此,无论你是否预期有 schema 变更,升级前的备份每次都是必做项。

第 6 步:验证

确认所有容器都已使用新镜像:

docker ps

api、celery-worker 和 celery-beat 三行都应显示 specivo/specivo:0.1.10。api 容器应报告 Up (healthy)。

检查登录页能否响应:

curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:9933/

返回 302 并重定向到登录页,就是正常的健康响应。

再次确认 schema 版本一致:

docker compose exec -T api alembic current

最后在浏览器中打开应用,登录,打开一个工作项,再执行一次搜索。三项都正常,升级就完成了。

回滚

如果新版本出现异常,在 .env 中重新固定到之前的标签即可回滚:

SPECIVO_VERSION=0.1.9

然后拉取并重启:

docker compose pull
docker compose up -d

如果失败的升级已经执行了 schema 迁移,仅重新固定标签是不够的。数据库 schema 可能已经领先于旧镜像,旧镜像会拒绝启动或运行异常。这种情况下,你必须恢复升级前的数据库导出文件和 specivo-data/ 归档,再启动旧镜像。先恢复数据目录,再把 SQL 导出文件载入数据库:

docker compose down
tar xzf /opt/specivo-backups/specivo-data-<timestamp>.tar.gz
docker compose up -d db
docker compose exec -T db psql -U specivo specivo \
  < /opt/specivo-backups/backup-<timestamp>.sql
docker compose up -d

这正是升级前备份每次都必须执行的原因。

好习惯

  • 每次只升级一个小版本,不要一次跨越多个版本。迁移序列是按逐步递增的方式设计的。
  • 拉取每个版本之前,都先阅读它的发布说明。
  • 每次都备份,哪怕看起来只是补丁版本。
  • 在 .env 中始终把 SPECIVO_VERSION 固定到明确的标签。生产实例绝不要跑在 latest 上。
  • 每次升级后做一次快速冒烟测试:登录、打开一个工作项、执行一次搜索。