最近有朋友跑过来问我,说他在 Dify 工作流里用“开始节点”接收用户上传的图片,后面接了一个代码节点,想把文件 URL 拿出来做二次处理。结果在代码节点里打印出来一看,文件地址要么是/api/v1/files/xxx/access?...这种相对路径,要么直接是http://localhost:80/api/v1/files/xxx/access?...,拿到浏览器里根本打不开,整个工作流直接被卡在这一步。
我跟他讲,这大概率不是 Dify 的 bug,而是部署的时候FILES_URL这个环境变量没有配好。这篇文章就围绕这个问题,把原因、定位思路、解决办法、以及几类容易踩的坑完整梳理一遍。内容主要针对 Docker Compose 方式本地部署 Dify、并且用工作流处理文件上传的同学,Dify 1.x 版本都适用,老版本思路也大同小异。
1. 先定位:开始节点的文件变量到底长什么样
1.1 用代码节点把文件变量完整打印出来
遇到文件 URL 不完整的问题,我建议第一步别急着改代码,先在 Dify 工作流里搭一个最简单的“调试链路”,把文件变量的真实结构打出来看。操作很简单:开始节点里拖一个文件类型变量,比如叫my_file,类型选“文件”或“图片”,接着接一个代码节点,输入变量绑定sys.files.my_file,代码直接原样输出 JSON。
import json def main(file_var: dict) -> dict: return { "result": json.dumps(file_var, ensure_ascii=False, indent=2) }运行一次工作流,手工上传一个测试图片,然后看代码节点的输出。不同 Dify 版本的字段结构会有一点差异,但核心通常长这样:
{ "dify_model_plugin": { "file_type": "image", "transfer_method": "local_file", "type": "image", "url": "/api/v1/files/9b185436-xxxx-xxxx-xxxx/access?timestamp=1700000000&nonce=abc&sign=xxx" }, "filename": "test.png", "transfer_method": "local_file", "type": "image", "url": "http://localhost:80/api/v1/files/9b185436-xxxx-xxxx-xxxx/access?timestamp=1700000000&nonce=abc&sign=xxx" }重点看两个字段:url和remote_url。url是 Dify 内部生成的文件访问地址,remote_url一般只在用户通过外链方式上传文件时才会有值,本地文件上传时基本上用不到。
如果你看到url是以/api/v1/...开头,或者写的是http://localhost:80/...,甚至出现http://nginx:80/...,那就说明 Dify 在生成这个地址的时候,没有拿到正确的外部访问域名,或者压根没配置,采用了默认值。后面的所有排查,都是围绕这个问题展开的。
1.2 为什么 URL 会拼成相对路径或 localhost
Dify 的文件,默认不是直接把服务器上的存储路径暴露给你的。它会把文件存在本地存储目录,然后通过一个 API 路由对外提供访问。工作流里文件变量里的url,本质上就是一个动态拼接出来的访问地址。拼接的时候,Dify 会依赖一个基础地址前缀,这个前缀的配置项就是FILES_URL。
关键点在于:Dify 工作流内部传递变量时,是没有经过外部网关或者 Nginx 反代的。如果你没有在环境变量里显式指定FILES_URL,Dify 只能按默认值去拼。默认值通常就是http://localhost,有些 Docker 部署场景下还可能是容器内部的服务名,比如http://nginx:80。于是你拿到的 URL 要么是相对路径,要么是 localhost,要么是容器内部地址。
这就是为什么同样一个工作流,你在本机部署时感觉一切正常,因为浏览器就在 localhost 上,拿到的http://localhost/...能直接打开;一旦换到服务器上,通过域名访问,别人上传的文件打出来的 URL 还是 localhost,那肯定 404。问题不在工作流逻辑,而在“文件访问的基础地址”没有跟着部署环境一起变。
2. 最快解决路径:配置 FILES_URL 并正确重启
2.1 修改 .env 文件的具体操作
Docker Compose 部署的 Dify,环境变量基本都集中在部署目录下的docker/.env里面。找到这个文件,搜索FILES_URL,没有就手动加一行。
FILES_URL=https://dify.example.com这里有几个细节要注意:
FILES_URL要填用户实际访问你 Dify 服务时使用的地址。如果你用域名反代,就填https://dify.example.com;如果你只是用服务器 IP 加端口访问,就填http://服务器IP:端口。- 结尾不要带斜杠,
https://dify.example.com/这种写法容易导致后面拼接时出现双斜杠,虽然 Dify 有的版本能容错,但没必要给自己埋坑。 - 如果你的服务是 HTTPS,这里一定也要写 HTTPS。否则 Dify 生成的图片地址还是 HTTP,浏览器会把 HTTP 资源直接拦截掉,表现就是“图片加载不出来”,这个问题容易和文件 URL 不完整混在一起。
如果你在.env里找不到FILES_URL,不要慌,手动新增一行即可。Dify 的 docker-compose.yaml 里通常会引用这个变量,并且有默认值兜底,你新增后就会覆盖默认值。
2.2 改完环境变量后,容器必须这样重建才生效
这是整个排查过程里最容易踩的坑,我见过太多人改完.env后只跑了一句docker compose up -d,然后发现文件 URL 还是老样子,于是怀疑 Dify 是不是缓存了什么。
实际上,docker compose up -d在容器已经存在、只有环境变量变化的时候,默认不会重新创建容器。你必须强制让它重建一次,新环境变量才会真正注入到容器进程里。
docker compose up -d --force-recreate api worker web如果你不确定服务名,就直接全部重建:
docker compose up -d --force-recreate重建完成后,建议进容器里确认一下环境变量是否真的生效:
docker ps | grep api docker exec -it <api容器名> env | grep FILES_URL看到输出的FILES_URL是你设置的值,再重新运行工作流,打印文件变量,url字段应该就变成完整的外部地址了。
提示:如果你是在 Dify 的源码目录用
docker/docker-compose.yaml启动的,记得确认你改的是docker/.env,不是项目根目录下的某个无关.env文件,别问我是怎么知道的。
3. 配好 FILES_URL 之后,工作流里怎么安全使用文件 URL
3.1 代码节点里做一次兜底拼接,最省心
全局配置FILES_URL是首选方案,但不少人在实际项目里会遇到一种情况:代码节点运行环境是沙箱,拿到的文件变量结构依然带有相对路径。这时候不要慌,可以在代码节点里做一个兜底拼接,避免下游节点拿到非法 URL。
核心思路很简单:判断url字段是否以http://或https://开头,如果不是,就手动拼上你的文件访问基础地址。
import json FILES_BASE_URL = "https://dify.example.com" def main(file_var: dict) -> dict: url = file_var.get("url", "") or "" if url.startswith("/"): url = FILES_BASE_URL.rstrip("/") + url return { "complete_url": url }这个方案的优点是稳定,不依赖代码节点里能不能读到环境变量。缺点也很明显,域名是写死的,以后换域名要改工作流。所以我一般是把它作为“临时补救措施”,长期方案还是把FILES_URL配置好,让 Dify 自动生成完整地址。
3.2 HTTP 请求节点里动态拼接文件地址
如果你需要在 HTTP 请求节点里把文件 URL 传给第三方服务,建议不要直接拖文件变量作为完整 URL,也不要手动拼一个可能重复的地址。正确做法是让代码节点先输出一个完整的complete_url,然后在 HTTP 节点的 URL 输入框里引用这个变量。
比如代码节点输出了complete_url,HTTP 节点 URL 栏写成:
{{#complete_url#}}这样最干净。如果确实需要手动拼接,只需要注意一点:先判断文件变量里的url是不是已经带http前缀。否则容易出现https://your-domain.comhttp://localhost/...这种奇怪地址。
还有一点特别容易忽略:Dify 文件 URL 后面通常会带签名参数,比如timestamp、nonce、sign。这些参数是访问校验的一部分,拼接和转发的时候一定要原样保留,不要觉得看着碍眼就删掉。没有签名,文件接口会直接拒绝访问。
3.3 LLM 读图和外部服务拉取文件时,为什么也受这个配置影响
有些同学会问:我把文件传给 LLM 节点,是不是就不需要管FILES_URL?
这个要看情况。Dify 内部处理文件时,如果是走本地文件流转,它会自己读取文件内容,转换成模型需要的格式,这时候FILES_URL配不配影响不大。但如果你集成的模型供应商要求图片必须通过公网 URL 传入,或者你在工作流里把文件 URL 交给了外部服务去拉取,那么FILES_URL没配好,直接影响就是模型读图失败或者外部服务 403。
我遇到过一个实际案例:工作流里把图片 URL 传给一个第三方图像处理服务,第三方服务收到地址后去拉取图片,拿到的 URL 是http://localhost:80/...,对方自然访问不到。排查到最后,就是FILES_URL没有配外部地址。改完之后,图片 URL 变成公网可访问地址,问题立刻解决。
所以结论是:哪怕你现在只做内部文件处理,我也建议把FILES_URL规范配置好,因为你不知道哪天会加一个外部服务调用,到时候再排查会非常被动。
4. 常见问题与排查技巧实录
4.1 本地正常、线上文件 URL 打不开
现象:本地部署时一切正常,上传图片后 URL 能打开;部署到服务器后,同样的工作流,代码节点打印出来的 URL 还是http://localhost:80/...,外部访问必然失败。
原因很直接:.env里的FILES_URL没有跟着部署环境改,仍然是默认值。Dify 本身不会自动感知你当前是通过什么域名访问的,它只知道你配置的基础地址。
排查步骤我一般是这样:
- 在代码节点打印文件变量的完整 JSON,确认
url到底是什么。 - 在服务器上执行
docker exec -it <api容器名> env | grep FILES_URL,看容器里的实际值。 - 如果容器里的
FILES_URL是http://localhost或空值,直接改.env并强制重建容器。
这组操作基本能把九成以上的问题定位出来。
4.2 改了 FILES_URL 仍然不生效怎么办
这种现象通常有三个原因。
第一个原因是容器没有重建。我在前面强调过,docker compose up -d不会因为环境变量变化就自动重建容器,必须加--force-recreate。很多人改完没重启,或者只重启了一半服务,导致 api 容器生效了,worker 容器还是旧环境变量,而处理文件任务的可能正好是 worker。
第二个原因是改错了.env文件。Docker Compose 部署的 Dify,.env文件在docker目录下。如果你同时有源码目录和部署目录,注意别混淆。
第三个原因是容器内环境变量被 docker-compose.yaml 中的引用方式限制住了。如果你手动维护过docker-compose.yaml,要确认 api 和 worker 服务里确实引用了${FILES_URL}。官方编排一般都会引用,但如果你从老版本升级过来,或者自己改过编排,可能会漏掉。
检查完这三个地方,基本都能解决。
4.3 文件 URL 带签名参数,过期后访问 403
这也是一个高频疑惑。代码节点里打印出来的 URL 很长,带有timestamp、nonce、sign参数。这个不是 URL 不完整,而是 Dify 的文件访问签名机制。
这些签名参数是用来校验请求合法性的,防止未授权的人通过拼接 URL 访问文件。带签名的链接通常是有时效的,超过一定时间后,Dify 会返回 403。
如果你的下游服务需要长期保存文件,不要把它返回的临时 URL 直接存到数据库里长期使用。正确做法是让服务端把文件下载下来,转存到自己的对象存储或者文件系统里,保存你自己生成的新地址。这个思路适用于任何使用 Dify 文件 URL 做二次分发的场景。
4.4 用了 Nginx 反代之后,文件 URL 变成内部地址
Dify 用 Docker Compose 部署后,大多数人会在前面套一层 Nginx 做反向代理。这时候如果FILES_URL没配好,文件 URL 可能变成http://nginx:80/...或者容器内部 IP。
原因是容器内生成 URL 时,Dify 只能根据它能拿到的信息来拼。虽然 Nginx 反代时会传递一些 header,但 Dify 内部生成文件 URL 的逻辑不一定依赖于这些 header,最可靠的还是直接配置外部访问地址。
配置建议和前面一样,FILES_URL写成用户实际访问的域名。另外,如果域名是 HTTPS,FILES_URL必须用 HTTPS,否则会出现页面是 HTTPS、文件资源是 HTTP 的混合内容问题,浏览器一样会拦截。
5. 别忽略:工作流发布为 API 时,返回文件 URL 同样依赖 FILES_URL
5.1 外部系统通过 API 拿到的文件地址长什么样
开始节点上传的文件问题,不只出现在工作流画布里。当你把工作流发布成 API,第三方系统调用接口、上传文件、拿到返回结果时,返回数据里的文件变量同样会走FILES_URL拼接逻辑。
之前有个做业务对接的同事踩过这个坑:外部系统调用 Dify 工作流 API,工作流跑通了,返回的文件字段看起来也有 URL,但对方点开就是打不开。我让他去查了一下部署环境,果然FILES_URL没配置,返回的地址是内网地址。配置好之后,外部系统才能正常下载文件。
所以,如果你把 Dify 工作流 API 暴露给第三方系统使用,检查清单里一定要有这一项:确认FILES_URL是外部可访问的域名。否则对方拿到文件也访问不了,你还得排查半天。
5.2 对外提供文件访问的配置建议
这里给一个比较稳妥的配置习惯:
FILES_URL统一在docker/.env里配置,不要写死在具体某个工作流里。- 如果 Dify 是给多个环境共用,建议每个环境单独维护一份
.env,避免测试环境配置被带到生产环境。 - 对外文件访问的域名最好和后台管理域名保持一致,或者是一个专门的文件子域名,方便后续做 CDN 或者迁移存储。
另外一个细节是,如果文件量大、访问频繁,建议考虑把 Dify 的本地文件存储切换到对象存储(比如 S3、OSS),这样文件访问地址可以走对象存储的域名,FILES_URL的配置指向也会更清晰。不过这是另一个话题了,本次先聚焦在 URL 不完整这个眼前的问题上。
最后再分享一个我个人常用的调试习惯:遇到 Dify 文件相关的问题,第一步永远是先把文件变量完整打印出来看结构,而不是直接改代码逻辑。因为你只有看到url到底是什么,才能判断是环境变量问题、签名过期问题,还是字段用错了。排查文件 URL 不完整这件事,只要抓住FILES_URL、容器是否重建、URL 是否带全签名这三个点,基本就能把所有相关坑都趟平。