最近需要把一套 Docker 单容器运行的 n8n,从 1.123.40 升级到 2.33.7。乍看只是换一个镜像标签,真正动手后才发现:跨大版本升级的重点从来不是启动新容器,而是确认数据边界、提前暴露兼容问题,以及准备一条真的能走通的回滚路径。
这篇记录一套适用于“小型自建实例 + SQLite + Docker named volume”的稳妥迁移方法。PostgreSQL、queue mode、多 worker 等部署不在本文范围内。文中的目录、容器名和数据卷都是通用示例,不包含真实服务器信息。
先给结论:
1 | 1.123.40 |
官方没有要求逐个 2.x 小版本升级,也没有为每一对跨版本组合单独背书。本文选择 1.123.69 作为预检过渡点,再进入 2.33.7。这不是数据库迁移的强制分段,而是为了先用 1.x 的 Migration Report 和 task runner 行为做一次低成本预演。
先定义本文用到的变量
先把示例值改成自己的实际配置;后续命令都引用这些变量:
1 | export N8N_CONTAINER=n8n |
这些值的含义如下:
| 变量 | 含义 |
|---|---|
N8N_CONTAINER |
当前 n8n 容器名 |
N8N_VOLUME |
挂载到 /home/node/.n8n 的原数据卷 |
N8N_ROLLBACK_VOLUME |
发生回滚时创建的新卷,不能与原卷同名 |
N8N_BACKUP_DIR |
宿主机上的私有备份目录,不要放进网站目录或公共对象存储 |
N8N_PORT_BIND |
端口映射示例;同机反向代理可只绑定 127.0.0.1,其他场景按实际网络拓扑调整 |
N8N_OLD_IMAGE |
本机已经缓存、能够提供 sh 和 tar 的旧 n8n 镜像 |
先确认关键对象都存在:
1 | docker volume inspect "$N8N_VOLUME" |
先弄清楚:数据不在容器里
这类部署最容易混淆三个东西:
- 镜像:例如
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 |
这个设置只负责收紧配置文件权限,不会自动备份工作流,也不能替代数据卷备份。
升级前的四条红线
开始前先把边界讲清楚:
- SQLite 必须在 n8n 停止后做完整卷备份。
- 1.x 和 2.x 不能同时读写同一个数据卷。
- 2.x 启动并迁移数据库后,不能只把镜像标签改回 1.x。
- 不要删除原卷,也不要对
N8N_VOLUME运行docker volume rm。
如果旧容器使用了 --rm,docker stop "$N8N_CONTAINER" 后容器对象会自动删除,但 named volume 不会被删除。这也是为什么“容器没了”和“数据没了”不是一回事。本文在迁移窗口沿用 --rm 以匹配这一场景;稳定运行后,更适合改用 Compose 或显式重启策略,这应当作为迁移之外的独立变更。
第一步:先把目标镜像准备好
尽量在停机前拉取目标镜像:
1 | docker pull "$N8N_PRECHECK_IMAGE" |
大版本迁移不要使用会漂移的 latest、stable 等标签,固定版本能让部署和回滚都更可控。
如果服务器无法直接访问镜像仓库,可以在另一台联网机器上先拉取再打包:
1 | docker pull "$N8N_PRECHECK_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 | docker run --rm \ |
然后使用本机已有的旧 n8n 镜像,把数据卷以只读方式挂载并打包:
1 | docker run --rm \ |
验证压缩包不是“看起来存在,实际上不可读”:
1 | sudo ls -lh "${N8N_BACKUP_DIR}/n8n_data-before-v2.tgz" |
只有看到 BACKUP_OK,这份备份才算通过最基本的可读性检查。
安全提醒: 这个
.tgz没有加密。它包含 n8n 数据库,并且通常也包含用于解密凭据的实例配置;如果原实例通过外部N8N_ENCRYPTION_KEY提供密钥,还必须单独安全保存同一个值。备份目录应限制访问,不要把归档、校验和、docker inspect输出、完整日志或未脱敏截图提交到公开仓库。
为什么不需要专门安装 Alpine
网上常见的数据卷备份命令会临时启动 alpine:
1 | Unable to find image 'alpine:...' locally |
这个错误表示 Docker 访问镜像仓库超时,并不代表数据卷有问题。alpine:3.22 只是一个临时工具镜像,不是 n8n 迁移依赖,也不需要专门安装。既然服务器本地已经有旧 n8n 镜像,就可以直接复用其中的 shell 和 tar,少一次网络依赖,也少一个故障点。
第三步:先在 1.123.69 做迁移预检
启动 1.123.69,并继续挂载原数据卷:
1 | docker run -d --rm \ |
这里仅展示迁移相关设置。实际部署时,要把原实例中的域名、Webhook、反向代理、区域、时区、执行超时等环境变量原样补回;大版本迁移期间不要顺手修改无关配置,也不要把真实值贴进公开文章。 如果原实例显式设置了 N8N_ENCRYPTION_KEY,新容器必须继续使用完全相同的值,否则已有凭据无法解密。
原命令中常见但不属于迁移核心的变量,可以按下面的原则处理:
N8N_DEFAULT_LOCALE、GENERIC_TIMEZONE、TZ、EXECUTIONS_TIMEOUT:沿用原实例实际值,不要照抄别人的区域或超时设置;N8N_HOST、N8N_PROTOCOL、WEBHOOK_URL、反向代理相关变量:按原部署恢复,否则 Webhook 或 OAuth 回调地址可能变化;N8N_SECURE_COOKIE=false:只适合明确受信任的纯 HTTP 测试环境;生产 HTTPS 应省略它或保持安全 Cookie;- 凭据、令牌和加密密钥:从私有配置恢复,不要直接写进 shell 历史、文章或公开仓库。
观察日志并确认版本:
1 | docker logs -f "$N8N_CONTAINER" |
然后以全局管理员登录,进入:
1 | Settings -> Migration Report |
Migration Report 会把问题分成 Workflow Issues 和 Instance 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 节点默认不能直接读取环境变量;
ExecuteCommand、LocalFileTrigger默认禁用;- 文件节点默认只能访问受允许的目录;
- 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 | docker run --rm \ |
确认看到 BACKUP_B_OK。为什么要做第二份备份?因为 Migration Report 阶段可能已经修改了工作流或实例配置。回滚时我们真正想恢复的是“兼容问题已经处理完、但还没被 v2 改写数据库”的状态。
第五步:启动 2.33.7
继续使用同一个 N8N_VOLUME:
1 | docker run -d --rm \ |
同样需要把原实例的其他环境变量补回。与 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 | docker run --rm \ |
然后用旧版本挂载回滚卷:
1 | docker run -d --rm \ |
启动回滚实例时,也要恢复原实例的其他环境变量以及相同的外部 N8N_ENCRYPTION_KEY(如果使用过)。
这样做有两个好处:
- 回滚实例使用的是明确的升级前数据;
- 迁移失败后的原
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