最近在社群里被问得最多的一个报错,就是 Dify 里的PluginInvokeError。不同用户贴出来的日志五花八门:有人做知识库流水线时插件调用失败,有人在工作流里挂了一个 HTTP 请求节点直接红字,有人在把 Dify 接到本地大模型(比如 Ollama)上时反复报连接超时。表面看是插件执行异常,但顺着日志往下查,最后十有八九会碰到同一个问题:容器 DNS 配置。这篇我就把 Dify 插件调用链路里 DNS 的定位方法、常见故障和修复手段一次讲透。内容适合正用 Docker 部署 Dify、准备做插件二次开发、或者想把 Dify 接进本地大模型服务的同学,我会把自己真实排查时的操作命令和判断逻辑全部摊开,照着做就能少走弯路。
1. 认识 PluginInvokeError:报错背后的请求链路
1.1 错误信息可能比你想象的更简单
Dify 的插件调用机制并不是在主进程里直接执行代码,而是交给专门管理插件的容器或进程去跑。当插件内部发起 HTTP 请求时,它会先做域名解析,再去建立连接、发送请求。如果域名解析失败,Dify 会把异常包装成PluginInvokeError返回。所以你在日志里看到的错误往往只有一行:
plugin_invoke_error: PluginInvokeError Failed to invoke tool 'http_request'很多朋友看到这个错误先怀疑自己的插件代码、API Key 或服务地址,这也没错,但我建议先冷静一下,看一下错误信息里有没有更细的线索。Dify 的日志通常会包含内部异常堆栈,里面会出现类似getaddrinfo failed、Name or service not known、connection refused或timeout之类的关键词。不同关键词对应的处理方式完全不同。如果看到的是getaddrinfo failed或Name or service not known,那基本可以确定是 DNS 解析问题,别急着改业务代码。
1.2 为什么 DNS 会成为头号嫌疑对象
原因在于 Dify 容器网络是典型的微服务组网。默认的docker-compose.yaml里会拉起 nginx、api、worker、web、plugin_daemon、ssrf_proxy 等多个容器,插件运行时如果需要调用 Dify 内部的接口,通常写的就是容器名,比如http://api:5001。容器名本身不是一个真实域名,它需要由 Docker 内置的 DNS 来解析成具体 IP。
一旦容器 DNS 配置出问题,内部服务名解析失败,插件自然报PluginInvokeError。再叠加一个高频场景:很多人在宿主机上跑了 Ollama 或其他本地大模型服务,Dify 插件里去调http://localhost:11434,这同样会失败,因为容器里的localhost不是宿主机。这一类问题虽然不是严格意义的 DNS 解析失败,但本质上和“容器网络寻址”相关,排查思路是同一个。所以遇到PluginInvokeError,我的第一个动作永远是查容器网络和 DNS,而不是去翻插件源码。
2. 容器 DNS 是怎么工作的:先摸清 Dify 的网络底牌
2.1 Docker 内置 DNS 与 127.0.0.11
Docker 为每个用户自定义网络(bridge 类型)提供了一套内置 DNS 服务,地址固定是127.0.0.11。只要容器接入的是自定义 bridge 网络,容器内的/etc/resolv.conf就会被 Docker 改写成类似这样:
nameserver 127.0.0.11 options ndots:0这里可以把它想象成公司前台。容器之间的服务名(比如api、worker)由这个“前台”直接转接;而访问公网域名时,“前台”自己不认识,就转给外部的总机——也就是宿主机配置的上游 DNS。这个设计本身很高效,问题在于上游 DNS 经常配置得不靠谱。特别是宿主机用的是 systemd-resolved 时,总机线路会受到干扰,导致容器解析外网域名时断时续。
2.2 /etc/resolv.conf 的生成逻辑
Docker 守护进程(dockerd)启动时会读取宿主机的/etc/resolv.conf,如果没有在启动参数或daemon.json里额外指定 DNS,那么宿主机上的 nameserver 就会被当作容器默认的上游 DNS。听起来很合理,但坑也在这里。
例如 Ubuntu 22.04 默认开启了 systemd-resolved,宿主机的/etc/resolv.conf实际指向127.0.0.53。这个地址是一个本地缓存服务,本身还需要再去访问真实 DNS。Docker 容器如果直接拿127.0.0.53作为上游 DNS,就会遇到“容器访问外部域名失败,但容器之间按容器名解析正常”的怪异现象。Dify 插件在这种环境下访问外部工具 API、OpenAI 接口、本地模型服务时,就会间歇性出现PluginInvokeError。很多人以为服务不稳定,其实只是 DNS 链路太长,超时了。
2.3 Dify 容器组网里哪些域名最容易被卡
以社区版 1.10 左右的默认 compose 为例,Dify 相关容器通常有这些角色:
nginx:对外提供 Web 入口。api:后端 API 服务,内部端口一般是 5001。worker:任务执行服务,消费队列任务。web:前端页面。plugin_daemon:插件守护进程,负责加载和调用插件。ssrf_proxy:防止 SSRF 的代理组件。sandbox:执行 Python 代码的沙箱容器。
插件调用 Dify 内部接口时,很常见的地址就是http://api:5001。如果多个自定义插件都需要访问这个地址,只要 Docker DNS 一抖动,所有插件都会跟着报错。你还会发现,用浏览器访问 Dify 控制台完全正常,因为浏览器是走宿主机的 DNS,不经过容器网络。这就是为什么很多人第一反应是“Dify 界面好好的,插件却一直失败”。
3. 一次完整的 PluginInvokeError 排查实录
3.1 第一步:从日志里把真正有用的错误摘出来
排查开始时,不要凭感觉改配置,先去翻日志。先用下面的命令把 Dify 核心服务最近一段时间的日志捞出来:
docker compose logs --tail=200 dify-api | grep -i "plugin" docker compose logs --tail=200 plugin_daemon如果你用的是较新版本的 Dify,服务名可能不叫dify-api,而是api-1或docker-api-1,建议先用docker compose ps查看当前实际容器名。日志里重点找getaddrinfo、Name or service not known、Temporary failure in name resolution、Connection refused这些关键字。
假设你看到类似{"error": "PluginInvokeError: Failed to invoke tool 'fetch_url', cause: getaddrinfo failed"}这样的内容,那排查方向就可以直接锁定 DNS。如果看到的是connection refused,说明解析可能没问题,而是目标服务端口未监听或者网络策略阻断。先做好这一步,后面很多力气才不会白费。
3.2 第二步:进容器手动验证 DNS 解析
日志只能说明“插件执行过程中出了问题”,究竟是不是 DNS 还要进容器里实测。Dify 的插件容器通常是精简镜像,不一定有nslookup或dig,但getent大概率是有的,因为它来自 glibc,几乎每个容器都会带。
docker exec -it plugin_daemon sh cat /etc/resolv.conf getent hosts api getent hosts www.baidu.com如果getent hosts api能返回一个 172.x 之类的内网 IP,说明容器名解析正常。如果返回“Name or service not known”,说明容器内部服务名解析失败。接着测外网域名,比如getent hosts www.baidu.com,如果这个也失败,说明上游 DNS 配置有问题;如果内部服务名解析失败但外网域名正常,问题可能出在 Docker 网络的服务发现,而不是上游 DNS。
另外,还可以直接用wget或curl做一次实际请求,判断是否是端口问题:
docker exec plugin_daemon sh -c "wget -q -O - http://api:5001/health || echo health_check_fail"这一步的目标是把“DNS 解析失败”和“网络不通”分开。不同容器里有没有wget不一定,也可以用/dev/tcp这种方式,但为了简洁我还是建议优先用wget或curl。
3.3 第三步:检查 Docker 守护进程与 compose 的 DNS 配置链路
手动验证只是看到了现象,接下来要找到引发 DNS 异常的配置源头。依次执行下面几条命令:
cat /etc/docker/daemon.json docker info | grep -A 3 "DNS" docker inspect plugin_daemon --format '{{json .HostConfig.Dns}}' grep -n "dns" docker-compose.yaml如果daemon.json里根本没有 DNS 配置,说明 Docker 正在用宿主机resolv.conf中的上游 DNS。继续执行:
cat /etc/resolv.conf在 Ubuntu 桌面版或启用了 systemd-resolved 的系统上,你会看到nameserver 127.0.0.53,这个时候要高度怀疑它就是罪魁祸首。
再补充一个判断方法:对比不同容器的 DNS 配置。执行docker inspect看看 api、worker、plugin_daemon 的HostConfig.Dns字段是否一致。如果你在 compose 里只给某个容器指定了 DNS,而其他容器没有,那“只有部分插件报错”就解释得通了。
3.4 第四步:用一个干净容器做交叉验证
为了确认“是整个网络的 DNS 配置不对,还是只有 Dify 插件容器不对”,最好的办法是起一个临时容器接到 Dify 的默认网络里测试。Dify 的 compose 项目名如果保持默认,网络名通常是dify_default。
docker run --rm --network dify_default curlimages/curl curl -I http://api:5001如果这个临时容器里访问api:5001也失败,说明是整个网络的 DNS 配置有问题,光改 Dify 的容器配置不彻底。如果临时容器正常,只有 plugin_daemon 不行,那就需要单独给 plugin_daemon 设置 DNS 或检查它的镜像是否缺少相关组件。
这个“干净容器对照法”看着简单,但真的能帮你节省很多时间。我处理过好几次问题都发现,Dify 本身网络是通的,只是某些插件镜像基于更小的运行时,解析逻辑受容器内配置影响更大,最后给对应容器单独加 DNS 就解决了。
4. 容器 DNS 配置的几种修复方式
4.1 改全局 daemon.json:一劳永逸但影响面最大
如果你的宿主机 DNS 本身就乱,最直接的办法是在 Docker 守护进程层面统一指定上游 DNS。编辑/etc/docker/daemon.json,加入dns字段:
{ "dns": [ "223.5.5.5", "119.29.29.29" ] }选 DNS 服务器时可以优先考虑国内访问稳定的公共 DNS,例如阿里云的223.5.5.5和腾讯云的119.29.29.29。如果你所在企业内网有必须解析的内部域名,记得把内网 DNS 放在列表最前面,否则内部服务名可能解析不了。
改完以后需要重启 Docker 守护进程,这一步要注意:重启会将所有运行中的容器一并重启,会影响线上服务。如果你的 Dify 已经跑了好几天,容器里有内存态任务,最好先docker compose stop停掉 Dify,再重启 Docker,最后docker compose up -d拉起环境。命令如下:
sudo systemctl restart docker docker compose up -d之后再用docker inspect验证容器的HostConfig.Dns是否已经变成刚配置的地址。全局配置的好处是所有容器都继承,坏处也是影响面大。对于只想快速解决某一类插件问题的情况,我更推荐下面这种定向配置。
4.2 在 compose 文件里给插件容器单独指定 DNS
对于自部署 Dify,我更习惯直接修改docker-compose.yaml,给plugin_daemon或者你能确认出问题的插件服务加上dns配置:
services: plugin_daemon: dns: - 223.5.5.5 - 119.29.29.29注意,不同版本的 Dify compose 文件里插件守护进程的服务名可能不一样,有的老版本叫plugin_daemon,新版本可能是plugin-daemon。所以这里建议先执行docker compose ps查看服务列表,再决定改哪一项。
修改完成后,让配置生效不是简单 restart 就行的,需要重新创建容器:
docker compose up -d --force-recreate plugin_daemon这样只会重建 plugin_daemon,不会影响 api、worker 和 nginx。如果担心影响其他服务,也可以只对plugin_daemon执行。这个方案比改全局 daemon.json 安全得多,适合生产环境紧急止血。
4.3 用 extra_hosts 把关键域名写死:直白且高效
有时候 DNS 短期修不好,但插件又急着要用,可以把关键服务域名直接写到容器的 hosts 文件里。在 compose 的对应服务下加extra_hosts:
services: plugin_daemon: extra_hosts: - "api:192.168.1.20" - "ollama:192.168.1.30"这里的api:192.168.1.20表示把api这个名字固定解析到192.168.1.20。这种方式适合目标服务 IP 比较固定的场景,比如局域网内部的 Ollama 服务、固定的向量数据库地址。它的优点是不依赖 DNS,缺点是没有灵活性,一旦服务换了 IP,就需要手动同步。
如果目标服务在宿主机上,还可以用 Docker 提供的host-gateway语法:
services: plugin_daemon: extra_hosts: - "host.docker.internal:host-gateway"加了这条之后,容器访问host.docker.internal时会被解析到宿主机的真实 IP。这也是让 Dify 插件调用宿主机 Ollama 最稳妥的写法。
4.4 Docker Desktop 用户与 host 网络模式的特殊处理
如果你是在 Windows 或 macOS 上用 Docker Desktop 部署 Dify,DNS 配置入口略有不同。Docker Desktop 的 Settings 里可以设置 Docker daemon 的 JSON 配置,也支持全局 DNS,但实测下来,Docker Desktop 在某些网络环境下自身 DNS 解析不够稳定,插件报PluginInvokeError的频率明显比 Linux 服务器高。
另一个常见做法是把某个容器直接改成network_mode: host,让容器共享宿主机网络栈。这在 Linux 上很顺手,但在 Docker Desktop(尤其 macOS)上支持并不完整,容器虽然能联网,但端口映射和容器名解析行为跟预期差别很大。所以我的建议是:部署 Dify 尽量用 Linux 服务器,插件调用本地服务优先用extra_hosts,不要为了省事切换到 host 模式,否则后续很多容器间服务名的解析都会出问题。
5. 排查过程中常踩的坑:一组避雷对照表
5.1 容器里用 127.0.0.1 调宿主机,注定失败
这个操作我在社群里见得太多了。很多人在宿主机上启动了 Ollama,然后在 Dify 插件里填的 Base URL 是http://127.0.0.1:11434,结果必报PluginInvokeError。原因很简单:插件运行在容器里,容器内的127.0.0.1指向容器自己,而不是宿主机。解决办法上面已经提到,给容器加extra_hosts映射host.docker.internal:host-gateway,然后把地址改成http://host.docker.internal:11434。
建议的 compose 配置:
services: plugin_daemon: extra_hosts: - "host.docker.internal:host-gateway" dns: - 223.5.5.5改完以后重建容器再测试。如果 Ollama 也是用 Docker 启动的,并且和 Dify 在同一个自定义 network 里,那更简单,插件地址直接写http://ollama:11434即可。
5.2 systemd-resolved 造成的“时好时坏”
这个坑会表现得非常隐蔽。早上插件还能用,下午突然不行了,过几分钟又自行恢复。你翻日志找不到固定规律,容器重建之后依然时好时坏。这时候大概率是宿主机启用了 systemd-resolved,容器继承的上游 DNS 指针不稳定。
判断方法仍然是进容器看/etc/resolv.conf,如果 nameserver 是127.0.0.53,就可以确认了。修复方案优先在daemon.json里固定公共 DNS,或者直接关闭桌面系统的 systemd-resolved 服务(但这会影响系统自身的域名解析,建议慎重)。最省事的就是我前面写的daemon.json指定 DNS。
这里有个细节值得注意:即使你改完daemon.json并重启 Docker,旧容器也需要重建才会用到新的 DNS,所以docker compose up -d --force-recreate是必须的。
5.3 看到 SSL 错误别急着改 DNS
很多用户把 Dify 接入公司内部系统时,看到SSL: CERTIFICATE_VERIFY_FAILED就以为是域名解析问题,其实是证书链路没配好,或者容器时间不同步。DNS 解析是“找到服务器在哪”,SSL 是“确认这个服务器确实可信”,两个环节不要混在一起。
排查时可以先用getent hosts确认域名能解析出 IP,然后看报错关键字。如果域名能解析但报证书错误,下一步检查容器系统时间:
docker exec plugin_daemon date如果时间明显不对,会导致 HTTPS 证书有效性校验失败。关于 Dify 接入内网 HTTPS 服务时的证书处理,通常要把自签 CA 证书挂载到插件容器里并设置SSL_CERT_FILE环境变量,而不是盲目关掉校验。热搜里经常出现“dify ssl错误”这类词,其实很多都是时间同步问题。这一块和 DNS 是两个方向,不能混为一谈。
5.4 PluginInvokeError 并不全等于 DNS 问题
为了行文严谨,这里把常见的几类报错放在一起对照。通过错误关键字快速判断方向,是我认为排查效率最高的做法。
| 日志关键字 | 可能原因 | 优先排查方向 |
|---|---|---|
getaddrinfo/Name or service not known | DNS 解析失败 | 容器 DNS 配置、上游 DNS、extra_hosts |
connection refused | 端口没监听,或防火阻断 | 被调服务状态、端口映射、服务协议 |
timed out | 网络不通、防火墙丢包 | 网络策略、路由、防火墙 |
CERTIFICATE_VERIFY_FAILED | 证书不受信任或时间偏差 | 容器时间、CA 证书、SSL 配置 |
JSONDecodeError | 返回内容不是预期 JSON | 服务方响应格式、插件解析逻辑 |
日志里经常还会看到 Dify 工作流整体报错,比如提示“上下文超长”,这种问题就不是出在插件调用链路,而是模型的 token 限制。因为 Dify 会把工作流上下文传给模型,当文本量超过模型限制时会报长度相关错误,和容器 DNS 完全没有关系。一旦把问题归错了类,就会在错误的维度上做无效排查。所以我每次都会强调:先定位异常关键字,再下结论。
5.5 给常用排查场景准备一个检查脚本
排查 Dify 容器 DNS 问题时,与其一遍遍手敲命令,不如准备一个小脚本放在宿主机上,出问题时一键跑,快速定位方向。
# dify-dns-check.sh echo "==== 1. resolv.conf ====" docker exec plugin_daemon cat /etc/resolv.conf echo "==== 2. resolve api container ====" docker exec plugin_daemon getent hosts api || echo "FAIL: api" echo "==== 3. resolve external domain ====" docker exec plugin_daemon getent hosts www.baidu.com || echo "FAIL: external" echo "==== 4. test api port ====" docker exec plugin_daemon sh -c "wget -q -O - http://api:5001/health || echo FAIL" || echo "FAIL: http api" echo "==== 5. docker daemon dns ====" cat /etc/docker/daemon.json | grep -A 4 '"dns"' echo "==== 6. host config dns ====" docker inspect plugin_daemon --format '{{json .HostConfig.Dns}}'脚本不复杂,但很实用。每次插件报PluginInvokeError,我会先跑一遍这个脚本,根据输出直接判断是上游 DNS、容器名解析、还是服务端口的问题,省掉很多纠结。
6. 最后想分享的一点体会
踩过几次坑之后,我现在处理PluginInvokeError的习惯已经固定下来:先看异常关键字,再进容器验证 DNS,确认后再决定改全局配置还是局部配置。说实话,Dify 本身的设计让插件调用链路比普通单体应用复杂不少,而 DNS 又是这条链路上最不起眼、最容易出错的一环。我见过有人折腾了几天,最后发现只是/etc/docker/daemon.json里少了一行 DNS 配置。这种问题一旦理解原理,解决起来就是几分钟的事。
最后再送一个小技巧:改完 DNS 配置后,不要只执行docker compose restart,因为很多配置(尤其 DNS 和 extra_hosts)在重新 create 容器时才会生效,用docker compose up -d --force-recreate才是稳妥做法。容器重建后 IP 可能会变,但 Dify 内部服务名解析会自动适配;如果插件里写了硬编码 IP,那就要特别注意了。希望这篇排查攻略能帮你省下接下来几天的排查时间,下次再看到PluginInvokeError,第一反应不再是翻代码,而是直接想到容器里的那个resolv.conf。