0%

Docker 单容器升级实录:n8n 1.123.40 到 2.33.7(SQLite 备份与可回滚迁移)

最近需要把一套 Docker 单容器运行的 n8n,从 1.123.40 升级到 2.33.7。乍看只是换一个镜像标签,真正动手后才发现:跨大版本升级的重点从来不是启动新容器,而是确认数据边界、提前暴露兼容问题,以及准备一条真的能走通的回滚路径。

这篇记录一套适用于“小型自建实例 + SQLite + Docker named volume”的稳妥迁移方法。PostgreSQL、queue mode、多 worker 等部署不在本文范围内。文中的目录、容器名和数据卷都是通用示例,不包含真实服务器信息。

先给结论:

1
2
3
4
5
6
7
1.123.40
↓ 完整离线备份 A
1.123.69
↓ Migration Report + 兼容性验证 + 完整离线备份 B
2.33.7
↓ 业务验证;失败则恢复旧卷
完成

官方没有要求逐个 2.x 小版本升级,也没有为每一对跨版本组合单独背书。本文选择 1.123.69 作为预检过渡点,再进入 2.33.7。这不是数据库迁移的强制分段,而是为了先用 1.x 的 Migration Report 和 task runner 行为做一次低成本预演。

先定义本文用到的变量

先把示例值改成自己的实际配置;后续命令都引用这些变量:

1
2
3
4
5
6
7
8
9
10
export N8N_CONTAINER=n8n
export N8N_VOLUME=n8n_data
export N8N_ROLLBACK_VOLUME=n8n_data_rollback
export N8N_BACKUP_DIR=/path/to/secure/backup
export N8N_PORT_BIND=127.0.0.1:5678:5678

# 必须填写服务器上真实存在的旧镜像标签
export N8N_OLD_IMAGE=n8nio/n8n:1.123.40
export N8N_PRECHECK_IMAGE=docker.n8n.io/n8nio/n8n:1.123.69
export N8N_TARGET_IMAGE=docker.n8n.io/n8nio/n8n:2.33.7

这些值的含义如下:

变量 含义
N8N_CONTAINER 当前 n8n 容器名
N8N_VOLUME 挂载到 /home/node/.n8n 的原数据卷
N8N_ROLLBACK_VOLUME 发生回滚时创建的新卷,不能与原卷同名
N8N_BACKUP_DIR 宿主机上的私有备份目录,不要放进网站目录或公共对象存储
N8N_PORT_BIND 端口映射示例;同机反向代理可只绑定 127.0.0.1,其他场景按实际网络拓扑调整
N8N_OLD_IMAGE 本机已经缓存、能够提供 shtar 的旧 n8n 镜像

先确认关键对象都存在:

1
2
docker volume inspect "$N8N_VOLUME"
docker image inspect "$N8N_OLD_IMAGE"

先弄清楚:数据不在容器里

这类部署最容易混淆三个东西:

  • 镜像:例如 n8nio/n8n:1.123.40,只提供程序和运行环境。
  • 容器:镜像启动后的进程实例,可以删除和重建。
  • named volume:真正持久化数据的地方,例如变量 N8N_VOLUME 指向的卷。

典型挂载如下:

1
-v "${N8N_VOLUME}:/home/node/.n8n"

/home/node/.n8n 中通常包含:

  • database.sqlite:工作流、用户、凭据、执行记录等数据;
  • config:实例配置和自动生成的凭据加密密钥;
  • 实例日志、源代码控制资源及部分文件数据。

因此,新容器是否能“继承旧数据”,不取决于镜像名称,而取决于它是否继续挂载同一个 N8N_VOLUME。答案是:只要原 named volume 完整,并且 2.33.7 继续把它挂载到 /home/node/.n8n,新版就会读取 1.123.40 的数据,并在首次启动时执行所需的数据库迁移。

还有一个常见误解:

1
-e N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true

这个设置只负责收紧配置文件权限,不会自动备份工作流,也不能替代数据卷备份。

升级前的四条红线

开始前先把边界讲清楚:

  1. SQLite 必须在 n8n 停止后做完整卷备份。
  2. 1.x 和 2.x 不能同时读写同一个数据卷。
  3. 2.x 启动并迁移数据库后,不能只把镜像标签改回 1.x。
  4. 不要删除原卷,也不要对 N8N_VOLUME 运行 docker volume rm

如果旧容器使用了 --rmdocker stop "$N8N_CONTAINER" 后容器对象会自动删除,但 named volume 不会被删除。这也是为什么“容器没了”和“数据没了”不是一回事。本文在迁移窗口沿用 --rm 以匹配这一场景;稳定运行后,更适合改用 Compose 或显式重启策略,这应当作为迁移之外的独立变更。

第一步:先把目标镜像准备好

尽量在停机前拉取目标镜像:

1
2
docker pull "$N8N_PRECHECK_IMAGE"
docker pull "$N8N_TARGET_IMAGE"

大版本迁移不要使用会漂移的 lateststable 等标签,固定版本能让部署和回滚都更可控。

如果服务器无法直接访问镜像仓库,可以在另一台联网机器上先拉取再打包:

1
2
3
4
5
6
docker pull "$N8N_PRECHECK_IMAGE"
docker pull "$N8N_TARGET_IMAGE"

docker save -o n8n-upgrade-images.tar \
"$N8N_PRECHECK_IMAGE" \
"$N8N_TARGET_IMAGE"

把文件复制到目标服务器后导入:

1
docker load -i n8n-upgrade-images.tar

第二步:停止服务并完整备份 SQLite 卷

先确认数据卷存在:

1
docker volume inspect "$N8N_VOLUME"

停止旧容器:

1
docker stop -t 120 "$N8N_CONTAINER"

如果提示 No such container,通常表示容器此前已经停止,并因为使用 --rm 被自动删除。只要 docker volume inspect "$N8N_VOLUME" 仍能找到数据卷,就可以继续。

创建一个通用备份目录:

1
sudo install -d -m 0700 "$N8N_BACKUP_DIR"

确认旧数据库和配置文件存在:

1
2
3
4
5
docker run --rm \
--entrypoint sh \
-v "${N8N_VOLUME}:/data:ro" \
"$N8N_OLD_IMAGE" \
-c 'ls -lh /data/database.sqlite /data/config'

然后使用本机已有的旧 n8n 镜像,把数据卷以只读方式挂载并打包:

1
2
3
4
5
6
7
docker run --rm \
--user 0:0 \
--entrypoint sh \
-v "${N8N_VOLUME}:/source:ro" \
-v "${N8N_BACKUP_DIR}:/backup" \
"$N8N_OLD_IMAGE" \
-c 'tar -czf /backup/n8n_data-before-v2.tgz -C /source .'

验证压缩包不是“看起来存在,实际上不可读”:

1
2
3
4
5
6
7
8
9
sudo ls -lh "${N8N_BACKUP_DIR}/n8n_data-before-v2.tgz"
sudo sha256sum "${N8N_BACKUP_DIR}/n8n_data-before-v2.tgz"

docker run --rm \
--user 0:0 \
--entrypoint sh \
-v "${N8N_BACKUP_DIR}:/backup:ro" \
"$N8N_OLD_IMAGE" \
-c 'tar -tzf /backup/n8n_data-before-v2.tgz >/dev/null && echo BACKUP_OK'

只有看到 BACKUP_OK,这份备份才算通过最基本的可读性检查。

安全提醒: 这个 .tgz 没有加密。它包含 n8n 数据库,并且通常也包含用于解密凭据的实例配置;如果原实例通过外部 N8N_ENCRYPTION_KEY 提供密钥,还必须单独安全保存同一个值。备份目录应限制访问,不要把归档、校验和、docker inspect 输出、完整日志或未脱敏截图提交到公开仓库。

为什么不需要专门安装 Alpine

网上常见的数据卷备份命令会临时启动 alpine

1
2
Unable to find image 'alpine:...' locally
Client.Timeout exceeded while awaiting headers

这个错误表示 Docker 访问镜像仓库超时,并不代表数据卷有问题。alpine:3.22 只是一个临时工具镜像,不是 n8n 迁移依赖,也不需要专门安装。既然服务器本地已经有旧 n8n 镜像,就可以直接复用其中的 shell 和 tar,少一次网络依赖,也少一个故障点。

第三步:先在 1.123.69 做迁移预检

启动 1.123.69,并继续挂载原数据卷:

1
2
3
4
5
6
7
8
9
docker run -d --rm \
--name "$N8N_CONTAINER" \
-p "$N8N_PORT_BIND" \
-v "${N8N_VOLUME}:/home/node/.n8n" \
-e DB_TYPE=sqlite \
-e DB_SQLITE_POOL_SIZE=2 \
-e N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true \
-e N8N_RUNNERS_ENABLED=true \
"$N8N_PRECHECK_IMAGE"

这里仅展示迁移相关设置。实际部署时,要把原实例中的域名、Webhook、反向代理、区域、时区、执行超时等环境变量原样补回;大版本迁移期间不要顺手修改无关配置,也不要把真实值贴进公开文章。 如果原实例显式设置了 N8N_ENCRYPTION_KEY,新容器必须继续使用完全相同的值,否则已有凭据无法解密。

原命令中常见但不属于迁移核心的变量,可以按下面的原则处理:

  • N8N_DEFAULT_LOCALEGENERIC_TIMEZONETZEXECUTIONS_TIMEOUT:沿用原实例实际值,不要照抄别人的区域或超时设置;
  • N8N_HOSTN8N_PROTOCOLWEBHOOK_URL、反向代理相关变量:按原部署恢复,否则 Webhook 或 OAuth 回调地址可能变化;
  • N8N_SECURE_COOKIE=false:只适合明确受信任的纯 HTTP 测试环境;生产 HTTPS 应省略它或保持安全 Cookie;
  • 凭据、令牌和加密密钥:从私有配置恢复,不要直接写进 shell 历史、文章或公开仓库。

观察日志并确认版本:

1
2
docker logs -f "$N8N_CONTAINER"
docker exec "$N8N_CONTAINER" n8n --version

然后以全局管理员登录,进入:

1
Settings -> Migration Report

Migration Report 会把问题分成 Workflow IssuesInstance Issues。处理顺序很简单:

  • Critical:升级前必须解决,否则工作流可能直接失败;
  • Medium:确认行为变化是否影响现有流程;
  • Low:检查弃用项,安排后续清理;
  • 修改后刷新报告,理想状态是两页都没有未解决问题。

这一阶段还要手工抽测代表性工作流,尤其是 Code、Webhook、Schedule、OAuth 和子工作流。

v2 最值得检查的破坏性变化

完整清单应以官方文档为准。对单容器 SQLite 部署,最容易踩坑的是下面这些:

1. SQLite 改用 pooled/WAL 驱动

v2 删除旧 SQLite driver,只保留 pooled driver。先在 1.x 设置:

1
-e DB_SQLITE_POOL_SIZE=2

可以提前验证 WAL 模式下的运行情况。

2. Code 节点默认使用 task runner

1.x 可以先设置:

1
-e N8N_RUNNERS_ENABLED=true

到了 v2,task runner 默认启用,这个变量已经弃用,不再需要继续设置。

单容器默认使用 internal runner,能够运行 JavaScript Code,但官方不建议生产环境长期使用 internal mode。更严格的生产部署应为 runner 增加独立 sidecar。

3. Python Code 不是原样兼容

v2 移除了旧 Pyodide Python 实现。Python Code 和 Python Tool 需要 external task runner,以及与主容器完全同版本的 n8nio/runners 镜像。

如果实例中存在 Python Code,这应当视为迁移阻断项,而不是升级后再处理的小问题。

4. 文件、命令和环境变量访问更严格

  • Code 节点默认不能直接读取环境变量;
  • ExecuteCommandLocalFileTrigger 默认禁用;
  • 文件节点默认只能访问受允许的目录;
  • Git 节点默认禁止 bare repository;
  • Code 节点中的 $evaluateExpression() 在安全模式下不再正常工作。

不要为了“先跑起来”直接关闭所有安全限制。优先改造工作流,临时兼容开关只应当作为短期过渡。

5. 保存不等于发布

v2 引入更明确的 Save/Publish 语义。保存修改只更新草稿,必须执行 Publish 才会更新线上运行版本。

这类行为变化不会总是表现为启动报错,却可能造成“明明保存了,生产流程却没变化”的误判。

第四步:保存一份“已就绪”的 1.x 备份

完成 Migration Report 修复并验证 1.123.69 正常后,再停止容器:

1
docker stop -t 120 "$N8N_CONTAINER"

再次备份:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
docker run --rm \
--user 0:0 \
--entrypoint sh \
-v "${N8N_VOLUME}:/source:ro" \
-v "${N8N_BACKUP_DIR}:/backup" \
"$N8N_PRECHECK_IMAGE" \
-c 'tar -czf /backup/n8n_data-1.123.69-ready-for-v2.tgz -C /source .'

docker run --rm \
--user 0:0 \
--entrypoint sh \
-v "${N8N_BACKUP_DIR}:/backup:ro" \
"$N8N_PRECHECK_IMAGE" \
-c 'tar -tzf /backup/n8n_data-1.123.69-ready-for-v2.tgz >/dev/null && echo BACKUP_B_OK'

确认看到 BACKUP_B_OK。为什么要做第二份备份?因为 Migration Report 阶段可能已经修改了工作流或实例配置。回滚时我们真正想恢复的是“兼容问题已经处理完、但还没被 v2 改写数据库”的状态。

第五步:启动 2.33.7

继续使用同一个 N8N_VOLUME

1
2
3
4
5
6
7
8
docker run -d --rm \
--name "$N8N_CONTAINER" \
-p "$N8N_PORT_BIND" \
-v "${N8N_VOLUME}:/home/node/.n8n" \
-e DB_TYPE=sqlite \
-e DB_SQLITE_POOL_SIZE=2 \
-e N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true \
"$N8N_TARGET_IMAGE"

同样需要把原实例的其他环境变量补回。与 1.x 命令相比,这里不再设置 N8N_RUNNERS_ENABLED,因为 v2 已默认启用 task runners。

首次启动会在原 SQLite 数据库上依次执行未完成的迁移。此时持续观察日志,不要中断:

1
docker logs -f "$N8N_CONTAINER"

启动完成后确认版本:

1
docker exec "$N8N_CONTAINER" n8n --version

预期输出:

1
2.33.7

升级后不要只看“页面能打开”

UI 能登录,只能证明进程和数据库基本可用。真正的验证至少包括:

  • 工作流数量、名称和历史执行记录符合预期;
  • 凭据能够正常解密并连接外部服务;
  • Webhook 生产 URL 正常;
  • Schedule Trigger 正常;
  • JavaScript Code 节点正常;
  • 子工作流经过 Wait、Webhook、Form 或人工审批后,父流程收到的结果符合预期;
  • 社区节点和自定义节点正常加载;
  • 修改工作流后执行了 Publish
  • 日志中没有数据库迁移、凭据解密或 task runner 错误。

验证顺序也很重要:先跑只读、低风险流程,再验证会发消息、扣费、写数据库或修改外部系统的流程。

回滚:恢复到新卷,不要覆盖现场

如果 2.33.7 验证失败,先停止它:

1
docker stop -t 120 "$N8N_CONTAINER"

不要直接用 1.123.69 打开已经被 v2 迁移过的 N8N_VOLUME。创建一个新的回滚卷:

1
docker volume create "$N8N_ROLLBACK_VOLUME"

将第二次备份恢复进去:

1
2
3
4
5
6
7
docker run --rm \
--user 0:0 \
--entrypoint sh \
-v "${N8N_ROLLBACK_VOLUME}:/restore" \
-v "${N8N_BACKUP_DIR}:/backup:ro" \
"$N8N_PRECHECK_IMAGE" \
-c 'tar -xzf /backup/n8n_data-1.123.69-ready-for-v2.tgz -C /restore'

然后用旧版本挂载回滚卷:

1
2
3
4
5
6
7
8
9
docker run -d --rm \
--name "$N8N_CONTAINER" \
-p "$N8N_PORT_BIND" \
-v "${N8N_ROLLBACK_VOLUME}:/home/node/.n8n" \
-e DB_TYPE=sqlite \
-e DB_SQLITE_POOL_SIZE=2 \
-e N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true \
-e N8N_RUNNERS_ENABLED=true \
"$N8N_PRECHECK_IMAGE"

启动回滚实例时,也要恢复原实例的其他环境变量以及相同的外部 N8N_ENCRYPTION_KEY(如果使用过)。

这样做有两个好处:

  1. 回滚实例使用的是明确的升级前数据;
  2. 迁移失败后的原 N8N_VOLUME 仍然保留,可以继续排查。

官方也提供 n8n db:revert,但它一次只回退最后一条数据库迁移。跨大版本可能包含多条迁移,因此完整快照恢复更适合作为主要回滚方案。

这次迁移最值得记住的几件事

第一,容器是耗材,数据卷才是资产。 只要边界搞清楚,重建容器并不可怕。

第二,备份的标准不是“文件生成了”,而是“文件能够被读取,并且知道如何恢复”。 没演练过的回滚只能算愿望。

第三,大版本升级不要顺手重构部署。 域名、反向代理、Cookie、时区、重启策略等无关变量,等迁移稳定后再单独调整。

第四,不要为了减少步骤而省掉中间检查。 1.123.69 不是数据库迁移的强制跳板,却是把兼容问题提前暴露出来的便宜保险。

第五,外网依赖本身也是风险。 一个临时 Alpine 镜像就可能因为仓库超时卡住维护窗口;优先复用已有镜像,或者提前准备离线镜像包。

参考资料

  • n8n 2.33.7 Release
  • Docker 安装与更新
  • v2.0 Migration Tool
  • v2.0 Breaking Changes
  • Task runners
  • n8n CLI