n8n 这两年可以说是自托管自动化工作流里的明星项目了。相比 Zapier 这类 SaaS 工具,n8n 的优势在于看得见摸得着:工作流逻辑全在自己的服务器上,数据不外流,节点随便写,社区集成又多。但自托管也有自托管的烦恼——工作流一旦多起来,单机部署的 n8n 会逐渐顶不住压力。你可能会遇到这种情况:某个跑数据清洗的工作流正占着 CPU,另一边等着响应的 Webhook 触发也跟着卡了。这篇文章要展开的,就是 n8n 2.9.2 在 Docker 环境下使用外部执行器(External Executor)的完整部署方案,把主服务和任务执行拆开,从架构层面根治性能互相干扰的问题。
这套方案适合谁?已经用 Docker 部署过 n8n、但现在觉得单节点越来越吃力的团队;打算从一开始就按企业级标准搭建、避免后面再推倒重来的朋友;以及被定时任务和复杂工作流折磨过、想找一个稳定可横向扩展方案的独立开发者。全文会从架构原理讲到 compose 文件落地,再到排障经验,你可以直接照着抄,也能从中理解每一步为什么这么做。
1. 先弄清外部执行器到底解决了什么问题
1.1 单机模式下最让人头疼的两件事
很多人第一次接触 n8n 都是用 docker run 一把梭,一个容器搞定一切:Webhook 接收、工作流解析、节点执行、数据存储,全在同一个进程里。这种方式在小规模场景下完全没问题,跑十几个工作流轻轻松松。但到 50 个、100 个以上,事情就开始变得微妙了。
我印象很深的一次经历是,一个团队在 n8n 里跑了两个很重的数据同步工作流,里面大量使用 Code 节点做字段映射和 API 循环。结果每周一早上业务方反馈说系统特别慢,一看监控,n8n 主进程的 CPU 占用长期在 80% 以上。更麻烦的是,那两个工作流执行期间,其他轻量级的 Webhook 自动化也被拖慢了。原因很简单:所有任务都在同一个 Node.js 进程里跑,事件循环被长任务堵住,其他的请求只能排队等着。
这是单机模式的第一个痛点:互相挤占资源。第二个痛点是扩缩容很尴尬。你不可能把整个 n8n 容器复制好几份来横向扩展,因为多个实例同时写同一个 SQLite 数据库、抢同一个队列任务,数据一致性和任务重复执行的问题马上就会冒出来。所以很多人会直接选择在单机上硬扛,把内存加到 16G、32G,但这只是把头痛往后推了。
1.2 外部执行器架构的核心思路
n8n 2.x 在架构上的一个重要演进,就是把"调度"和"执行"这两个职责拆开。
你可以把主服务想象成一个公司的前台加调度室:它负责接收外部请求、管理用户登录、读取工作流定义、把任务写入队列,然后把具体干活的工作交给后面的执行团队。而外部执行器就是那个执行团队,它不做任何对外的事情,只从队列里拿任务、跑工作流、把结果写回数据库。
这种设计的关键点在于两者是完全独立的进程,甚至可以跑在不同的机器上。主服务卡了,执行器还在消费队列;执行器崩了,主服务不受影响,重启后继续干活。而且执行器可以起多个,任务会自动分散到不同实例上处理。
在 n8n 2.9.2 里开启这种模式,核心手段是让主实例以队列模式运行,同时通过环境变量启用外部执行器能力,Redis 在这里充当任务队列的中转站。整个链路大致是:外部触发或者定时器触发主实例生成执行任务,主实例把任务塞进 Redis,执行器实例监听 Redis 拿到任务,执行器跑完把结果写回 PostgreSQL,之后前端页面就能看到执行日志了。
1.3 什么样的团队真正需要这种架构
我说点实在的。如果只是个人跑几个 Telegram 机器人、定时抓个网页、做点自动通知,那用默认的单机模式挺好的,没必要折腾外部执行器。引入 Redis 和独立执行器会让部署复杂度上一个台阶,维护成本也是实打实的。
但出现下面这些情况,你就该认真考虑切到外部执行器:
- 工作流数量超过 50 个,且高峰期并发执行频繁。
- 工作流里有 Code 节点、大量 HTTP 循环请求或者其他 CPU 密集型操作。
- 你需要保证 Webhook 类工作流的响应速度稳定,不愿意被别的任务拖累。
- 团队有横向扩容的需求,希望在高峰期临时多开几个执行器实例来应对。
我自己的判断标准很粗暴:如果单机模式已经让你在半夜起来重启过 n8n 容器,那说明架构升级的时机到了。外部执行器模式,本质上就是给 n8n 打了一个"可以水平扩展"的底子,现在多花两小时部署,后面能省下无数个加班的夜晚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 部署前的环境准备与方案选型
2.1 硬件和操作系统:最低多少才够用
先说结论,一台 4 核 8G 的 Linux 服务器,跑这套架构(主实例 + 1 个外部执行器 + Redis + PostgreSQL)是勉强够用的;4 核 16G 算是比较舒服的起步配置。为什么这么说,因为主实例虽然不跑业务代码了,但 Node.js 进程本身加上 Webhook 并发处理,日常也要吃掉 1~2G 内存;外部执行器才是真正的消耗大户,尤其是跑 Code 节点和复杂分支时,内存能冲到 3G 以上。再加上 Redis 和 PostgreSQL 各自占掉一部分,8G 内存的确会有点紧。
操作系统这边我推荐 Debian 12 或者 Ubuntu 22.04,原因没有别的,就是 Docker 生态支持最稳,网上资料也多。Windows 上虽然也能装 Docker Desktop 来跑这套架构,但我见过太多 Docker Desktop 的资源占用和虚拟化兼容性问题,生产环境请老老实实用 Linux 服务器。
Docker 版本要求不高,Docker 20.10 以上即可,但 Docker Compose 请尽量用 v2。老版本的 docker-compose(Python 版)在解析一些新语法时会有问题,没必要在这种地方给自己挖坑。
2.2 数据库选型:为什么默认换成 PostgreSQL
n8n 默认用的是 SQLite,零配置开箱即用,小规模场景非常好。但到了外部执行器模式,SQLite 基本就该告别舞台了。原因有两个:
第一,SQLite 是文件型数据库,并发写能力有限。外部执行器跑完任务要写执行数据、更新工作流状态,主实例也要读,两个进程同时操作同一个 SQLite 文件,很容易出现 database is locked 的错误。
第二,SQLite 不适合远程访问。如果你想把数据库独立出来,或者让多个 n8n 实例共享数据,SQLite 根本满足不了。
所以这里直接用 PostgreSQL 15,这不光是为了撑住并发,也是 n8n 在队列模式和外部执行器模式下比较标准的配套选择。MySQL 也能用,但我在实际使用中觉得 PostgreSQL 对 n8n 的数据类型和索引支持更友好一点,而且用 Docker 部署非常干净。
2.3 Redis 选型与持久化设置
Redis 在整套架构里承担的是任务队列职责,所有要执行的任务都通过它来传递。版本选择 redis:7-alpine 就够了,轻量且稳定。内存方面,任务信息本身很小,除非你有海量任务堆积,否则 256M 限制足够,我给容器默认设置的是 512M。
有一点我想特别提示:Redis 要做数据持久化,至少开启 AOF(Append Only File)。虽然任务队列里的数据丢了还能靠重新触发来补救,但很多时候你根本不知道哪些任务丢了。在 compose 文件里我给 Redis 加了 --appendonly yes,确保队列信息尽最大可能不丢。当然,Redis 本身不是这套架构的最终数据存储,真正的执行结果和日志都在 PostgreSQL 里,所以即使 Redis 数据完全清了,最多也就是丢一些当时正在排队但没有开始执行的任务记录。
2.4 数据卷规划:哪些数据必须留住
部署之前先把数据目录规划好,避免后期想迁移的时候手忙脚乱。
需要持久化的数据有三块:
- n8n 主实例的工作目录
/home/node/.n8n,里面存加密密钥、执行历史记录、数据库连接配置等。 - PostgreSQL 的数据目录
/var/lib/postgresql/data。 - Redis 的数据目录
/data,保存 AOF 和 RDB 快照。
外部执行器实例的工作目录要不要持久化?我的建议是也要,至少挂一个空卷。因为很多工作流会用到 n8n 的静态文件功能或者在执行时下载临时文件,如果执行器连本地磁盘都没有,某些节点会报错。不过执行器的工作目录不像主实例那样需要备份,它更像是一个运行时的临时桌面。
3. 完整落地:docker compose 部署外部执行器模式
3.1 先搭骨架:整体服务结构怎么看
直接动手写 compose 文件之前,我在脑子里先画了一张部署结构图:四个容器服务,分别是 n8n-main、n8n-executor、postgres、redis。其中 n8n-main 对外暴露 5678 端口,其他服务都在 Docker 内部网络中通信。
这个结构里最关键的一点,是 n8n-main 和 n8n-executor 使用同一个镜像,但启动命令和环境变量有所不同。前者是"统治者",负责任务生成和 API 服务;后者是"苦力",只管执行。两者必须连接到同一个 PostgreSQL 和同一个 Redis,否则就会出现主服务把任务塞进队列,执行器却连接着另一个 Redis 导致收不到任务的情况。
3.2 docker-compose.yml 完整内容
下面这份 compose 文件是我在 2.9.2 版本上实际验证过的写法,直接复制后把密码、域名这些变量改成你自己的就行。
yaml复制version: "3.8"
services:
n8n-main:
image: n8nio/n8n:2.9.2
restart: unless-stopped
ports:
- "5678:5678"
environment:
- N8N_HOST=n8n.example.com
- N8N_PORT=5678
- N8N_PROTOCOL=https
- N8N_ENCRYPTION_KEY=please-change-me-to-a-random-32-char-key
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_BULL_REDIS_PORT=6379
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=change-me
- N8N_EXTERNAL_EXECUTOR=true
- GENERIC_TIMEZONE=Asia/Shanghai
volumes:
- n8n_main_data:/home/node/.n8n
depends_on:
- redis
- postgres
n8n-executor:
image: n8nio/n8n:2.9.2
restart: unless-stopped
environment:
- N8N_ENCRYPTION_KEY=please-change-me-to-a-random-32-char-key
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_BULL_REDIS_PORT=6379
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=change-me
- N8N_EXTERNAL_EXECUTOR=true
- N8N_RUNNERS_ENABLED=true
- GENERIC_TIMEZONE=Asia/Shanghai
volumes:
- n8n_executor_data:/home/node/.n8n
depends_on:
- redis
- postgres
- n8n-main
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
postgres:
image: postgres:15-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=change-me
- POSTGRES_DB=n8n
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
n8n_main_data:
n8n_executor_data:
redis_data:
postgres_data:
保存为 docker-compose.yml 后,在文件所在目录执行 docker compose up -d 就能把整套环境拉起来。
3.3 环境变量逐项拆解:每个配置都在干什么
很多朋友喜欢复制网上现成的 compose 文件,一跑发现不对就懵了。所以我把这里的关键环境变量讲透,你以后不看文档也能自己排查。
先看主实例的核心变量:
EXECUTIONS_MODE=queue:这是开启队列模式的总开关。不设置默认是 regular,也就是在进程内直接执行。只有设为 queue,主实例才会把任务写入 Redis 队列,外部执行器才有活可干。N8N_EXTERNAL_EXECUTOR=true:这个变量让主实例不再自己执行工作流,而是把执行请求全部推给队列。配合 EXECUTIONS_MODE=queue 一起用,等于明确告诉 n8n:你只负责调度,不要再干执行的活了。N8N_ENCRYPTION_KEY:加密密钥,用于加密工作流里的敏感数据(数据库密码、API Key 等)。主实例和外部执行器必须设置成同一个值,否则执行器拿到任务后解密失败,任务会直接报错。DB_TYPE=postgresdb:数据库类型。从 SQLite 切换到 PostgreSQL 就是通过这个变量指定的。QUEUE_BULL_REDIS_HOST=redis:Redis 地址。这里直接用 compose 里的服务名 redis 作为主机名,Docker 内部网络会自动解析。GENERIC_TIMEZONE=Asia/Shanghai:全局时区。这个不设置的话,定时触发可能会差了 8 个小时。
再看外部执行器实例,原理基本一样,只是少了端口暴露和 Webhook 相关配置。因为它不需要对外提供服务,只需要连上 Redis 去取任务,连上数据库去读写结果。N8N_RUNNERS_ENABLED=true 是让任务运行器处于激活状态,让工作流里的代码节点能在独立的 runner 进程中执行,进一步隔离资源占用。
有一个细节很多第一次部署的人会踩坑:N8N_ENCRYPTION_KEY 必须在所有实例中保持一致。如果你用 openssl rand -hex 16 之类的方式生成一个随机字符串,那就把主实例和执行器都配上同一个。这个密钥一旦不一致,执行器解密 workflow 数据时会失败,表现就是任务一直失败,日志里出现类似 decrypt failed 的错误。
3.4 启动、验证和执行器扩容
启动前先检查一下端口。执行 docker compose up -d 之后,等大概 30 秒到 1 分钟,n8n 主实例会完成数据库迁移和初始化。这时访问 http://服务器IP:5678,能看到 n8n 的初始化页面就说明主实例起来了。
接着验证外部执行器是否正常。我这里的方法是:
bash复制docker compose logs -f n8n-executor
如果看到执行器成功连上 Redis 并进入等待任务状态的日志,说明链路通了。然后到 n8n 里新建一个最简单的工作流,比如用 Manual Trigger 接一个 Set 节点,手动执行一次。执行成功后,在主实例的 Execution 页面能看到执行记录;如果执行失败,第一时间去看 n8n-executor 的日志。
关于扩容,外部执行器最大的优势就是可以动态加实例。高峰期来了,需要两个执行器同时干活,最简单的办法是复制一份 n8n-executor 的 service 定义,改个名字比如 n8n-executor-2,然后 docker compose up -d 启动。两个执行器消费同一个 Redis 队列,任务会自动分配。不过我建议了解一点背后的机制:n8n 的任务分法是基于队列的竞争消费模式,多个执行器实例同时监听同一个队列,同一个任务只会被其中一个拿走执行,不会出现重复执行的情况。
3.5 生产环境必须做的三件小事
如果是正式环境,我建议在基础部署之上再做三件事。
第一,把 N8N_HOST 和 N8N_PROTOCOL 设置为真实的域名和协议。如果你用 IP 加端口访问,N8N_HOST 写 IP、N8N_PROTOCOL 写 http 就行;如果有域名的,就把 N8N_HOST 设为完整域名,并在前面用 Nginx 或 Caddy 做反向代理加 HTTPS。因为 n8n 的 OAuth 回调和一些 webhook 链接是根据这两个变量拼接出来的,配置不对会导致第三方授权失败。
第二,给 PostgreSQL 设置单独的初始化密码,不要把 change-me 留在生产环境。其实在 compose 文件里,你可以用 ${POSTGRES_PASSWORD} 这样的变量引用方式,把真实密码放进 .env 文件并加入 .gitignore,避免把密钥提交到仓库里。
第三,把数据库的自动备份捡起来。n8n 的工作流定义都存在 PostgreSQL 里,定时用 pg_dump 备份数据库是成本最低的灾难恢复方案。
4. 常见问题与排障实录
4.1 任务一直失败,日志提示解密错误
这是我被问到过最多的问题。症状很典型:外部执行器模式部署完了,主实例能打开,工作流能保存,但一执行就失败,n8n-executor 日志里出现类似 decrypt failed 或者 bad decrypt 的报错。
原因基本就是 N8N_ENCRYPTION_KEY 不一致。主实例用密钥 A 加密了工作流数据存到数据库,执行器却用密钥 B 去解密,自然解不开。解决办法也很简单:所有 n8n 实例统一使用同一个 N8N_ENCRYPTION_KEY。注意,如果你启动过一次实例已经把数据写进去了,中途更换密钥也会导致无法读取历史数据,所以这个密钥从一开始就要定好,并且好好保存。
4.2 外部执行器始终不消费任务
另一种情况是主实例日志里能看到任务已经生成了,但 n8n-executor 那边毫无反应,像是没在工作一样。
排查思路按顺序来:
- 确认两个服务连的是同一个 Redis。很多人复制配置时改漏了 QUEUE_BULL_REDIS_HOST,一个指向 redis,一个指向 localhost,那它们根本不在一个频道里。
- 确认执行器的启动命令是正确的。外部执行器模式要启动的是执行器进程,不是主服务进程,如果执行器容器里的启动命令不对,它可能根本没有开始监听队列。
- 看 Redis 里有没有堆积任务。可以执行
docker exec -it redis redis-cli llen bull:jobs之类的命令查看队列长度,如果一直增长说明主实例在写入但执行器没消费,问题就锁定在执行器侧。 - 确认数据库连接正常。执行器启动时要初始化数据库连接池,连不上 PostgreSQL 的话它会一直等着重试。
4.3 定时任务触发时间和预期不一致
这个问题十有八九是时区没设置。n8n 默认使用 UTC 时间,如果你设置了"每天早上 9 点执行",但容器时区是 UTC,那你实际看到的就是北京时间下午 5 点才执行。
解决方法就是上面提到的 GENERIC_TIMEZONE=Asia/Shanghai。主实例和执行器都要设置,保证它们理解"早上 9 点"的方式一致。改完时区后重启服务,再把工作流里的定时触发器重新保存一次,让 n8n 用新的时区重新计算下一个触发时间。
4.4 反向代理和 HTTPS 配置的几个坑
用 Nginx 反向代理 n8n 时,有两个点需要注意。第一是 WebSocket,n8n 的前端页面推送执行进度时要用到 WebSocket,如果你在 Nginx 里只代理了普通 HTTP,没有配置 Upgrade 头,页面会一直转圈,工作流执行状态更新不实时。配置里要加上:
nginx复制location / {
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
第二是 webhook 回调地址。n8n 在响应某些 webhook 时,会返回一个链接给你的调用方。如果 N8N_HOST 和 N8N_PROTOCOL 没配好,它返回的链接可能是 http 的 IP 地址,第三方的回调就会走错地方。所以配完反向代理,去 n8n 的设置页面里确认生成的 webhook URL 是你预期的 https 域名。
4.5 时间久了,Docker 日志占满磁盘
这套架构跑起来后,PostgreSQL、Redis、n8n 三个容器每天都会输出日志,尤其 n8n 在频繁执行任务时会记录大量运行日志。如果不限制日志大小,几个月后 Docker 的 JSON 日志文件占满磁盘是迟早的事。
我习惯是在 /etc/docker/daemon.json 里全局限制日志轮转:
json复制{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
改完重启 Docker 服务,新容器的日志就会被自动切割。这里提个醒,这个配置只对后续创建的容器生效,已经存在的容器要重新创建才会应用新策略。
另外一个容易忽略的是 n8n 自己的执行历史。默认情况下 n8n 会把每一次执行记录都存到数据库,量大了以后 PostgreSQL 会变得很臃肿。建议在 n8n 的环境变量里加上 N8N_EXECUTIONS_DATA_PRUNE=true,并设置保留天数,比如 N8N_EXECUTIONS_DATA_MAX_AGE=168(小时,也就是 7 天),让 n8n 自动清理历史执行记录。
4.6 执行器节点突然 OOM
Code 节点写得很糟糕的时候,执行器内存会飙升。尤其是循环里套循环、数组处理不当还有内存泄漏的情况下,节点直接 OOM 也不是没可能。Docker 本身不会限制容器内存上限,除非你在 compose 文件里显式声明。
所以生产环境给执行器加上内存限制是有必要的。在 docker-compose.yml 的 n8n-executor 服务里加一行:
yaml复制 deploy:
resources:
limits:
memory: 2048M
这样即使某个节点的代码内存失控,Docker 也会把它杀掉重启,而不是让整台服务器都跟着卡死。不过要注意,限制内存之后,如果你有大数据量的工作流,反而会更容易触发 OOM,所以这个值要根据实际情况调整。我给一个参考:常规工作流场景 2G 是底线,涉及大数据处理可以提到 4G。
5. 跨机部署的一些补充思路
如果你的执行器需要跑在不同的物理机器上,compose 文件的写法就要稍微调整一下。前提是执行器所在的那台机器能够访问到主服务的 PostgreSQL 和 Redis,而这两者不一定需要暴露到公网,可以通过 Docker 网络、内部网络或者安全组来打通。跨机部署时,Redis 的地址就不能写服务名了,要写具体的 IP,数据库连接同理。
网络层面我有一点强烈建议:不要把 PostgreSQL 的 5432 端口暴露到公网,也不要把 Redis 的 6379 端口暴露到公网。这两个服务一旦被扫到并且没有强密码保护,很短时间内就会被爆破入侵。正确的姿势是只让应用容器访问它们,对外只开放 n8n 的 5678 端口(或者通过 Nginx 暴露 443)。
跨机部署还有一个额外要求,就是主实例和外部执行器的镜像版本必须完全一致,包括小版本号都不能差。n8n 内部有一套任务协议和数据格式,两边的版本不一致很可能出现任务分发出错或者数据解析失败的问题。我建议所有实例都固定为同一个镜像 tag,比如 n8nio/n8n:2.9.2,不要用 latest。因为 latest 是会变动的,你今天部署的和下个月部署的可能是不同版本,到时候排查问题会非常头疼。
回到我这两年的使用体验,外部执行器模式确实让 n8n 的可靠性上了一个台阶。以前我最怕的就是某个定时任务在高峰期启动,把整个实例搞得不响应。现在主实例和执行器各自独立,资源占用被隔离开,数据库和队列也换成了更扛压的组件,日常运维省心了很多。如果你正在单机模式上挣扎,希望这篇文章能帮你把整套方案顺利落地。最后再提醒一句:部署完成后,别忘了立刻把 N8N_ENCRYPTION_KEY 和数据库密码备份到安全的地方。
