n8n VPS 不断离线?先区分4种故障
n8n 显示连接丢失不一定是服务离线。本文教您区分 WebSocket 提示、容器重启循环、内存终止和定时任务未触发,并用 Docker 状态与日志定位原因。
n8n 为什么会不断离线:4 种故障,1 种现象
“n8n 不断离线”这句话可能对应 4 种不同的故障,每种故障都需要不同的修复方法。容器运行正常,但编辑器显示连接丢失提示。容器会自行重启。内核会因 Node.js 进程占用过多内存而将其终止。或者进程本身完全正常,只是已启用的工作流始终没有触发。修改了错误的设置后,您可能会花整个周末处理一个根本不存在的问题。
因此,在修改任何配置前,先确定您遇到的是哪一种故障。n8n 作为单个 Node.js 进程运行,通常位于一个 Docker 容器中,前面使用负责终止 TLS(传输层安全)的反向代理。这些层都可能以各自的方式发生故障,而浏览器会使用相同的消息报告所有这些故障。
按此顺序诊断
在 VPS(虚拟专用服务器)上运行这些命令,并读取本机输出的值。不要将这些值与论坛帖子中的数字比较。这里有用的值描述的是您的服务器,而不是其他人的服务器。
docker ps -a --filter name=n8n
docker logs --tail 200 --timestamps n8n
docker inspect n8n | grep -iE 'Status|Running|RestartCount|OOMKilled|ExitCode'
docker stats --no-streamdocker ps -a 的 STATUS 列表示容器处于当前状态的时长。将其与问题开始的时间进行比较。如果容器在横幅出现前很久就已经启动,那么 n8n 从未离线。发生故障的是浏览器与后端之间的连接,也就是下一节介绍的 websocket 通道。
RestartCount 表示 Docker 重启此容器的次数。记下这个数字,等待一分钟后再读取一次。如果您观察期间数字持续增加,说明容器处于重启循环中。每次重启前的日志行会说明原因。
OOMKilled 是一个 true 或 false 标志。True 表示 Linux 内核终止了进程,因为进程超出了内存限制。这个限制可能是容器自身的限制,也可能是整台机器的限制。该字段可以将内存终止与其他类型的退出区分开来,因此应先读取它,再进行推断。
ExitCode 表示容器上次退出时返回的值。您不需要记住每个退出码的含义。读取本次的值,然后读取同一时间戳下 docker logs 的末尾内容。日志末尾和 out of memory 标志结合起来可以说明发生了什么,但单独依赖其中任何一个都可能导致误判。
docker stats 显示实时内存使用量以及当前生效的限制。在第二个终端中保持该命令运行,触发导致故障的工作流,并观察故障发生时数值的变化。
连接断开提示通常来自反向代理
n8n 编辑器会与后端保持一个长期打开的推送连接,以便将执行进度实时显示在画布上。默认情况下,该连接使用 WebSocket,N8N_PUSH_BACKEND 用于选择此连接类型,其默认值为 websocket。WebSocket 首先以普通 HTTP 请求启动,并携带 Connection: Upgrade 和 Upgrade: websocket 标头。服务器返回 101 Switching Protocols,之后双方通过同一个 TCP 套接字进行双向通信。
有两种情况会导致连接中断,而且问题都出在代理,而不是 n8n。代理向上游使用 HTTP/1.0,或删除升级标头,因此升级过程不会完成,编辑器会不断重新连接。另一种情况是升级成功,但代理随后因连接处于静默状态而关闭套接字,因为没有消息的 WebSocket 看起来与空闲连接完全相同。在这两种情况下,容器都处于正常状态。该提示表示浏览器失去了与后端的通信通道。
在修改任何配置前,先在浏览器中确认这一点。打开开发者工具,进入 Network 选项卡,将筛选条件设为 WS,然后重新加载编辑器。推送请求应到达 101 Switching Protocols,并保持打开状态。如果推送请求返回普通状态码,或每隔几秒重新出现一次,问题通常出在代理。
保持编辑器连接的 nginx 设置
除非明确配置,否则 nginx 不会转发升级请求。proxy_pass 默认使用 HTTP/1.0 与后端通信,Connection 和 Upgrade 是逐跳标头,nginx 转发请求时会将其移除。必须将这两个标头重新添加。map 块应放在 http 上下文中,而不是放在 server 内部。
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}server {
listen 443 ssl;
http2 on;
server_name n8n.example.com;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
}
}proxy_read_timeout 是最容易遗漏的配置项。默认值为 60 秒,并且同样适用于已升级的 WebSocket。因此,处于空闲状态的编辑器实例如果保持标签页打开,会在最后一条消息经过约一分钟后断开连接。增大该值即可解决您返回长时间未操作的标签页时看到的提示。
sudo nginx -t && sudo systemctl reload nginx
sudo nginx -T | grep -iE 'proxy_http_version|upgrade|proxy_read_timeout'nginx -T 会输出完整的运行中配置,而不是某个文件的内容,因此可以确认您的修改确实已加载。如果配置位于任何 include 行都不会加载的文件中,正确的修复就会看起来没有效果。
然后告知 n8n 当前位于代理之后,因为它会根据这些值生成 URL。
environment:
- N8N_HOST=n8n.example.com
- N8N_PROTOCOL=https
- N8N_PORT=5678
- N8N_PROXY_HOPS=1
- N8N_WEBHOOK_URL=https://n8n.example.com/N8N_PROXY_HOPS 默认为 0,这表示 n8n 会将连接地址视为客户端地址,并忽略 X-Forwarded-For。请将其设置为容器前面的代理数量。截至 2026 年 8 月,N8N_WEBHOOK_URL 是当前名称,旧名称 WEBHOOK_URL 仍可使用,但 n8n 启动时会输出弃用警告。
Traefik 转发 WebSocket,但随后将其超时
Traefik 无需中间件或额外标签即可转发 WebSocket 升级请求。因此,Traefik 用户看到此提示时,通常遇到的是超时,而不是缺少请求头。相关参数位于 entryPoint 上。截至 2026 年 8 月,Traefik v3 中,idleTimeout 的默认值为 180 秒,readTimeout 的默认值为 60 秒。
entryPoints:
websecure:
address: ":443"
transport:
respondingTimeouts:
readTimeout: 0
idleTimeout: 3600sCaddy 会在 reverse_proxy 中自动处理升级请求,无需为此配置指令。如果您完全无法更改代理配置,因为代理由其他人管理,可以使用 N8N_PUSH_BACKEND=sse 切换推送通道。SSE(服务器推送事件)是一个保持打开状态的普通 HTTP 响应,因此即使代理拒绝升级请求,也能继续工作;但过于严格的空闲超时仍会中断连接。选择代理本身是另一个需要单独决定的问题,nginx、Caddy 和 Traefik 对比介绍了各代理带来的运维成本。
容器确实在重启时
如果 RestartCount 持续增加,说明容器启动失败,Docker 正在将其重新启动。将日志时间戳与每次重启对照,查看紧接在重启前发生了什么。几乎所有问题都属于以下四类:配置错误导致启动失败、n8n 无法访问数据库、服务运行后崩溃,或进程因内存不足被终止。
先检查卷,因为权限问题最容易被忽略。官方镜像以非特权用户 node 运行,并将数据存储在 /home/node/.n8n。由 root 创建的绑定挂载目录无法由该用户写入,因此进程每次都会在启动时退出,restart policy 会将这个问题隐藏在不断重启的循环中。
docker compose config
docker run --rm -it --entrypoint sh docker.n8n.io/n8nio/n8n -c 'id'
docker exec n8n ls -ld /home/node/.n8n使用命名卷可以完全避免此问题,因为 Docker 会为其创建正确的所有权。如果必须使用绑定挂载,请将主机目录的所有者 chown 设置为第一条命令输出的数字用户 ID。了解一次主机与容器之间的所有权映射很有帮助,PUID 和 PGID 说明介绍了这些镜像如何决定由哪个用户写入文件。
看起来像崩溃的内存不足终止
n8n 进程上方有两个相互独立的内存上限,它们的失效方式不同。容器控制组限制由内核强制执行:一旦超过该限制,进程会立即被终止,没有机会写入任何内容,并且 OOMKilled 的结果为 true。V8 堆限制由 Node.js 在进程内部执行:一旦超过该限制,Node 会抛出堆错误,显示堆栈跟踪,然后自行退出,因此 OOMKilled 的结果为 false。从浏览器看,这两种情况完全相同。从 docker inspect 看,它们只相差一个字段。
将 Node 堆上限设置为低于容器限制。如果堆上限高于容器限制,V8 会继续分配内存,超过内核介入的阈值。因此,其垃圾回收器不会先达到自身上限,最终总是发生更严重的故障,并且没有日志可供查看。
services:
n8n:
image: docker.n8n.io/n8nio/n8n
restart: unless-stopped
environment:
- NODE_OPTIONS=--max-old-space-size=<MiB, below the container limit>
deploy:
resources:
limits:
memory: <your container limit>根据 VPS 的实际内存配置选择这两个数值,并为数据库、代理和操作系统预留空间。docker stats --no-stream 会同时显示当前使用量和生效中的限制,因此您可以确认自己设置的限制就是 Docker 应用的限制。Compose 如何应用内存限制详细说明了设置多个限制时哪个键优先。
执行数据会在运行过程中持续累积
一次执行会在运行期间保留每个节点的输出,n8n 随后会存储这些数据。由此产生两个后果。一次运行的峰值内存取决于其中传递的最大数据批次,因此,一次处理 10000 行的工作流,与每次处理 200 行的同一工作流,实际上是两个不同的程序。存储的数据副本会持续增长,直到被删除。
修剪功能用于解决第二个问题。截至 2026 年 8 月,默认设置为启用修剪,EXECUTIONS_DATA_MAX_AGE 为 336 小时(14 天),EXECUTIONS_DATA_PRUNE_MAX_COUNT 为 10000。对于运行 SQLite 的小型 VPS,这些值相对宽松,因为所有数据都存储在同一个文件中,而且为编辑器提供服务的同一进程还必须读写该文件。
environment:
- EXECUTIONS_DATA_PRUNE=true
- EXECUTIONS_DATA_MAX_AGE=72
- EXECUTIONS_DATA_PRUNE_MAX_COUNT=1000
- EXECUTIONS_DATA_SAVE_ON_SUCCESS=none
- EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS=falseEXECUTIONS_DATA_SAVE_ON_SUCCESS=none 是积极修剪设置。它会保留失败的执行以供调试,并删除成功的执行。请有意选择此设置,因为如果工作流产生了错误输出但没有报告错误,之后将没有任何数据可供检查。修剪还会先将行标记为已删除,再在后续过程中移除这些行;SQLite 会重新利用已释放的页面,而不是将其返还给操作系统。因此,修改设置后,磁盘上的文件不会立即缩小。
如果要降低峰值,而不是减少存储总量,就应减少每次运行传递的数据量。将大型任务拆分为多个子工作流,让子工作流向父工作流返回较小的结果;使用 Loop Over Items 节点进行批处理;不要将完整数据集传入 Code 节点。
二进制文件不应在内存中传递
N8N_DEFAULT_BINARY_DATA_MODE 默认为 default,会将二进制数据保存在运行中执行实例的内存中。节点下载的每个文件,以及传递给下一个节点的每个副本,都会一直保留在那里,直到运行结束。一个工作流只要获取几个较大的附件,就可能使进程超过普通 JSON 处理根本不会接近的限制。因此,崩溃会在某个特定工作流运行时发生,而不是经过固定时间后发生。
environment:
- N8N_DEFAULT_BINARY_DATA_MODE=filesystem使用 filesystem 后,二进制数据会写入 N8N_BINARY_DATA_STORAGE_PATH。默认情况下,该路径位于 n8n 用户目录中,因此会使用与其他数据相同的卷。切换前,请确认该卷有足够空间。N8N_PAYLOAD_SIZE_MAX 用于设置传入 webhook 请求正文的最大大小,单位为 MiB(mebibytes),默认值为 16。提高该值可以允许更大的请求进入,但这也意味着您选择承担相应的内存开销。
同一主机上的其他服务也会竞争相同的 RAM。如果 OOM killer 开始终止进程的时间,正好是您添加数据库容器之后,那么 在 Docker 中还是主机上运行数据库 就是您现在需要作出的取舍。
重启策略,以及重启后恢复运行
未设置重启策略的容器退出后会保持停止状态,主机重启后也不会自动恢复。restart: unless-stopped 会在这两种情况下重新启动容器,同时不会重新启动您手动停止的容器。restart: always 还会重新启动您主动停止的容器,但要等到 Docker 下次启动后才会执行。
n8n 提供一个健康检查端点,由 N8N_ENDPOINT_HEALTH 指定,默认值为 healthz。首先从主机检查该端点,确认此路径在您的实例上正确。
curl -fsS http://127.0.0.1:5678/healthz
docker exec n8n which wget curl
sudo systemctl is-enabled docker健康检查本身不会重新启动任何容器。Compose 会将容器标记为不健康,然后停止处理。因此,健康检查必须配合重启策略,或由旁边的外部监控程序处理,才能产生实际效果。编写能够实际执行操作的健康检查 和 让堆栈在重启后再次启动 分别介绍这两个部分。
n8n 正常运行但工作流始终未触发
此工作流不会显示横幅,也不会重启。容器处于运行状态,编辑器可以正常使用,但您预期的运行记录没有出现在执行列表中。大多数情况下,原因有以下4种。
- 工作流未激活。Schedule Trigger 只会在生产路径上运行,因此在画布中测试不会创建任何计划。
- 时区不是您所在的时区。
GENERIC_TIMEZONE默认使用America/New_York,因此计划设为09:00时,会一直按照该时区的09:00触发,直到您将GENERIC_TIMEZONE和TZ设置为自己的时区。 - 停机期间错过的触发不会在之后补执行。n8n 启动时才会注册触发器,因此容器重启期间到期的计划不会延迟执行。启动后的下一次运行时间,是启动后下一个到期时间。
- 工作流被自动停用。
N8N_WORKFLOW_AUTODEACTIVATION_ENABLED默认关闭;启用后,持续崩溃的工作流会被取消发布。之后,它看起来就像一个从未激活过的工作流。
打开执行列表,并筛选此工作流。有失败记录,说明是工作流问题。完全没有记录,说明是触发器问题,应从上述4种原因开始排查。
首先修改什么
- 在编辑任何文件前,先自行阅读容器中的
STATUS、RestartCount和OOMKilled。 - 如果容器从未停止运行,请修复代理升级请求头和空闲超时设置。
- 如果
OOMKilled为 true,请有意设置一个容器限制,将 Node 堆上限设在该限制以下,并将二进制数据切换到filesystem。 - 如果没有任何任务触发,请检查工作流是否处于活动状态,以及实例时区是否设置为您的时区。
这些大多是只需设置一次的配置,前提是安装已正常运行。如果您仍在搭建该安装环境,请参阅 使用 Docker 配置带 HTTPS 的 n8n 教程;这些设置都基于该教程。
FAQ
为什么 n8n 编辑器在容器运行时显示连接丢失提示?
编辑器会保持一个 WebSocket 连接,用于传输执行进度。如果反向代理不转发 Connection: Upgrade 和 Upgrade: websocket 标头,或上游不使用 HTTP/1.1,升级就无法完成,浏览器会不断重新连接,而 n8n 仍处于正常状态。在 nginx 中需要 proxy_http_version 1.1 和两行 proxy_set_header,还需要将 proxy_read_timeout 设置为长于默认 60 秒的值,避免空闲标签页被断开。使用 sudo nginx -T 检查正在运行的配置,不要只检查您编辑过的文件。
如何区分内存不足导致的终止和普通崩溃?
运行 docker inspect n8n | grep -iE 'OOMKilled|ExitCode|RestartCount' 并读取 OOMKilled 标志。True 表示内核因进程超过内存限制而将其终止。此时容器日志中不会有有用信息,因为进程没有机会写入日志。False 表示进程末尾出现堆错误和堆栈跟踪,且该内容位于 docker logs 中。这说明 Node.js 达到了自身的 V8 堆上限,然后自行退出。将 NODE_OPTIONS=--max-old-space-size 设置为低于容器限制的值。这样会触发第二种故障,而这种故障会留下证据。
清理执行数据会立即释放磁盘空间吗?
不会。EXECUTIONS_DATA_PRUNE 会将旧执行记录标记为待删除,后续任务才会根据 EXECUTIONS_DATA_PRUNE_HARD_DELETE_INTERVAL 设置的计划删除这些记录。使用 SQLite 时,数据库文件还会重新利用已释放的页面,而不是将空间归还给文件系统。因此,即使记录已删除,磁盘上的文件大小也会在一段时间内保持不变。将 EXECUTIONS_DATA_MAX_AGE 和 EXECUTIONS_DATA_PRUNE_MAX_COUNT 设置为适合您服务器的值,然后第二天再检查,不要立即检查。
为什么 n8n 重启期间,计划工作流没有运行?
n8n 会在进程启动时注册触发器,不会补执行停机期间到期的计划。因此,重启循环只会导致没有执行,不会产生一批补执行任务。启动后的下一次执行时间,就是启动后下一个到期时间。如果任务不能遗漏,请让外部调用方通过 webhook 触发工作流。这样,重试逻辑就位于 n8n 之外。
健康检查会在 n8n 停止响应时重启它吗?
不会自动重启。Compose 健康检查只会将容器标记为 healthy 或 unhealthy。重启由重启策略负责,因此 restart: unless-stopped 会在容器退出后将其重新启动;只要 Docker 服务已启用,它也会在主机重启后重新启动容器。使用 sudo systemctl is-enabled docker 确认这一点。如果需要专门处理 unhealthy 状态,则必须在 Docker 外部运行监控程序,由它读取状态并重启服务。