改动 DIFY 源代码再自己构建镜像,这件事我做过不止一次。最近又帮一个项目组把官方版 DIFY 改了内部流程再重新打包镜像,顺手把整个操作链路又捋了一遍。DIFY 本身是目前很热门的大模型应用平台,官方镜像用起来确实省心,但到了一定程度就必须动源代码:默认提示词不满足要求、界面文案要换成自己的品牌、内置工具要裁剪、模型接入逻辑要调整,这些事情只在容器里改两下文件是治标不治本。这篇文章我就完整讲讲:拿到 DIFY 源代码、找到要改的位置、修改后重新构建后端和前端镜像,再用自定义镜像把整套服务拉起来。整个过程适合同样在做 DIFY 本地化部署、想深度二次开发的团队,有一点 Docker 基础的人都能跟着做下来。
1. 为什么不能直接改容器,而要回到源代码构建镜像
1.1 容器改动的“一次性陷阱”
很多人第一次部署完 DIFY,遇到需要调整的地方,下意识会进容器直接改代码。做法大概是docker exec -it dify-api bash,进到容器里的/app/api目录,找到某个 Python 文件,用 vim 改两行,然后重启容器。当时看确实生效了,但只要这个容器被重新创建——比如执行docker compose down之后再来一次docker compose up -d,或者镜像被重新拉取更新,所有改动全部归零。
这不是操作失误,而是容器的天然属性:容器只是在镜像之上叠加了一个可写层,文件改动都落在这个临时可写层里。容器删除,这一层跟着销毁。虽然可以docker commit把容器固化成新镜像,但这条路非常容易踩坑,比如容器里残留了临时文件、日志、缓存,导致镜像体积膨胀,更致命的是你根本记不清自己到底改过哪些文件,后续没法做版本回溯,也没法对照官方升级。
正确做法从一开始就应该是:把改动回到源代码层面,用修改后的代码重新构建出属于你自己的镜像。这样每一次改动都可追踪、可复现,换一台服务器部署时直接用这个自定义镜像就行。
1.2 官方镜像和源码版本本身就是一一对应的
DIFY 社区版的所有镜像,本质上都是用某个 release 分支或某个 tag 上的源码构建出来的产物。官方镜像langgenius/dify-api:1.10.0和 GitHub 上dify仓库的1.10.0tag 是对应的。
所以一个很实用的思路是:在源码仓库里先锁定官方发行版的 tag,然后基于这个 tag 开自己的分支做修改。这样你随时可以知道“我的自定义版本相对于官方版本到底多改了什么”,后期官方发布新版本时,也能用git diff看到自己跟官方所有差异,方便决定是否合并升级。
1.3 哪些改动真正值得构建新镜像
不是所有问题都要走到“改源码、构建镜像”这一步。先判断改动类型:
- 值得改源码的场景:修改 Agent 默认提示词、修改前端品牌文案、增加自定义工具、修改模型供应商接入逻辑、裁剪内置依赖、固定某个依赖版本规避兼容性问题。
- 不需要改源码的场景:纯环境配置类参数,比如修改
docker-compose.yaml中的环境变量,调整数据库连接、日志级别、并发数、模型 API Key,这些直接在部署配置里改即可,不用重新构建镜像。
这里有一个常见的误区:改环境变量能解决的事非要去改代码。比如日志级别,代码里写的是从环境变量读取,你改代码里那个默认值往往不生效,因为环境变量的优先级高于代码默认值。改代码前先在源码里搜一下这个值是不是被环境变量覆盖了,能省很多事。
2. 准备环境:一次干净的二次开发从这里开始
2.1 环境清单
要在本地完成“改源码 + 构建镜像”这套操作,以我实际经验来看,最少需要下面这些条件:
- 操作系统:Linux 最省事;Windows 强烈建议用 WSL2,原生 Windows 跑 Docker 构建偶尔会遇到路径映射和权限问题。
- Docker:20.10 以上版本,构建镜像用 BuildKit 模式,默认也是这个模式。
- Docker Compose:v2 版本,直接用
docker compose命令而不是老旧的docker-compose。 - Git:用来拉源码、切分支、看改动。
- 磁盘空间:DIFY 后端镜像加前端镜像,加上构建缓存,建议预留 15GB 以上磁盘空间,第一次构建往往会把所有基础镜像和依赖包都拉下来。
检查环境是否就绪,可以依次执行:
docker --version docker compose version git --version2.2 获取 DIFY 源码并锁定版本
把一个开源项目的源码长期稳定地管理起来,最忌讳就是随便 clone 最新版,今天 main 分支上的代码和官方发版镜像可能根本不是一回事。所以我习惯固定到明确的 release tag 上。
git clone https://github.com/langgenius/dify.git cd dify # 查看有哪些版本 git tag -l # 切到你需要的版本,比如 1.10.0 git checkout 1.10.0 # 基于这个版本开一个自己的开发分支 git checkout -b custom-build锁定 tag 这一步很关键。DIFY 社区更新速度很快,隔几周就是一个新版本,依赖结构、配置文件位置、环境变量名都可能发生变化。如果 clone 了 main 分支,构建时用的 Dockerfile 和部署时用的 docker-compose 配置可能是“开发中版本”,跟你在 DIFY 官方文档里看到的部署方式对不上,就会出现一堆莫名其妙的兼容问题。
2.3 源码目录里我们需要关心的几个位置
DIFY 仓库结构大概分为几大块:
api/:后端服务,Python + Flask 写的,所有业务逻辑、Agent 实现、工作流引擎、模型接入、知识库处理都在这里。web/:前端服务,Next.js + TypeScript,用户看到的控制台界面。docker/:镜像构建和部署编排相关内容,Dockerfile 和 docker-compose 配置在这里。sdks/:各语言 SDK 示例,构建镜像时用不到。
后面所有改动,我们其实只关心api/、web/、docker/这三个目录就够了。
关于版本的细节,我建议构建前先看一眼docker/目录下的 Dockerfile 和 compose 文件。不同版本基础镜像、依赖安装方式都可能调整,先了解官方是怎么构建的,后面遇到问题才有排查方向。
3. 定位要修改的代码,用一次真实改动掌握方法
3.1 用“关键词搜索”代替“通读源码”
DIFY 后端代码量不小,第一次打开源码不要想着从头读到尾。我的经验是:先明确要改什么功能或文案,然后在整个源码目录里搜关键词,定位到目标文件,再顺着上下文一路看下去。
比如我现在要把 Agent 内置的默认提示词改成适合自己业务场景的版本。这种提示词一般会以比较明显的字符串形式存在源码里,那我就先搜典型的提示词片段。
cd api grep -r "You are" --include="*.py" -n .如果不确定提示词是中文还是英文,可以多搜几个关键词,比如“你是一个”、“assistant”、“You are a helpful assistant”等等。命中之后打开对应文件,找到那段字符串所在的位置,确认它确实是 Agent 默认提示词的定义处。
这里有一个实际操作中的心得:所谓“默认提示词”可能在多个地方出现。有的在 Agent 的 prompt 模板里,有的可能在 System Prompt 的默认配置里,还有的在前端界面初始化应用时写入。你要找的是应用真正使用的那个,建议修改之前先在源码里把同一关键词的所有命中位置都看一遍,并用grep确认没有遗漏。
3.2 一个完整的例子:修改后端默认提示词
假设我们已经通过grep定位到一个 Python 文件,里面有一行类似这样的内容:
DEFAULT_AGENT_PROMPT = "You are a helpful assistant."现在我要把它改成项目需要的提示词:
DEFAULT_AGENT_PROMPT = "You are an expert in enterprise knowledge management. Please answer strictly based on the provided context."改完文件之后,用git diff查看本次改动:
cd /path/to/dify git diff修改前后对比一目了然,这比在容器里改完就忘强太多了。确认无误后提交到分支:
git add api/core/agent/xxx.py git commit -m "feat: customize default agent prompt for enterprise scenario"3.3 注意代码读取顺序和配置覆盖问题
改完源码不代表一定生效。DIFY 后端大量配置项支持通过环境变量覆盖,而且有些内容最终会存入数据库,源码里的默认值只是“首次初始化”时使用的值。
举个例子,你拉起了 DIFY,在界面上创建了一个应用,这个应用的系统提示词很可能已经被写进数据库了。这种情况下,你修改源码里的默认提示词,只对新创建的应用生效,存量应用不会自动更新,除非你手动在应用设置里重新调整,或者去数据库里更新对应记录。
所以每次改代码之前,先判断一下这个值到底是“代码里硬编码的全局常量”,还是“首次写入数据库的初始值”,又或者是“每次请求时动态拼接的模板”。三种场景的处理方式完全不同。这也是我见过很多人“改了源码重新构建镜像之后发现没效果”的根本原因之一。
3.4 前端界面的修改路径
如果你要修改的是前端界面,比如把登录页的标题、Logo 文案替换成自己公司的品牌,操作路径和后端不同,但方法论一致:进入web/目录,搜索前端文案关键词。
cd web grep -r "Your App" --include="*.tsx" --include="*.ts" -n .前端项目文件多、依赖重,构建时间比后端更长,但修改逻辑并不复杂。需要注意一点:前端构建产物最终是静态文件,会被 Nginx 或者对应容器直接托管。改完前端文件后,重新构建前端镜像是必须步骤,不是在容器里改一行文件就能完事的。
4. 从源码到镜像:后端与前端镜像的完整构建流程
4.1 构建后端镜像
代码改好,接下来就是重头戏:构建镜像。DIFY 的构建目录设计得很规整,后端 Dockerfile 在api/Dockerfile,前端 Dockerfile 在web/Dockerfile。
构建后端镜像的命令是:
cd /path/to/dify docker build -f api/Dockerfile -t dify-api:custom-1.10.0 ./api我来拆解一下这个命令里每个参数的作用。
-f api/Dockerfile指定使用哪个 Dockerfile 文件。-t dify-api:custom-1.10.0给新镜像打标签,dify-api是镜像名,custom-1.10.0是 tag,建议 tag 里带上对应版本号,方便后续识别。“./api”是构建上下文(build context),也就是告诉 Docker 构建时能访问哪些文件。这里只把api/目录作为上下文,不要图省事直接写成“.”,否则整个 DIFY 仓库都会被发送给 Docker daemon,构建准备阶段会慢很多,而且可能把一些无关文件也带进上下文。
后端镜像体积比较大,因为要安装大量 Python 依赖。构建时间一般在几分钟到十几分钟不等,取决于你的网络环境和机器性能。
4.2 构建前端镜像
前端镜像同理:
docker build -f web/Dockerfile -t dify-web:custom-1.10.0 ./web前端构建过程需要下载大量 npm 包,第一次构建会比后端慢,这是正常的。前端镜像的构建产物会包含打包后的静态文件,以及一个轻量的 Web 服务器(通常是 Nginx)。
这里可以顺便解释一下为什么我们经常说“构建缓存很重要”。无论是后端还是前端,Docker 在构建过程中都会对每一层做缓存。如果你只改了api/core/agent/xxx.py这个文件,那么 Docker 会复用之前所有没变化的构建层,只重新执行从“COPY 代码”开始的后续层,大大缩短构建时间。但如果你的构建命令里加了--no-cache,就等于告诉 Docker 放弃全部缓存,哪怕只改了一行代码,也要把依赖重新装一遍。
4.3 把自定义镜像写进 docker-compose 并启动
镜像构建完成,现在要让整套环境跑起来。打开 DIFY 的docker-compose.yaml,找到 api 和 web 这两个服务定义。
原本的配置大概是:
services: api: image: langgenius/dify-api:1.10.0 web: image: langgenius/dify-web:1.10.0把镜像引用改为我们刚构建的版本:
services: api: image: dify-api:custom-1.10.0 web: image: dify-web:custom-1.10.0然后执行:
docker compose up -d注意,docker compose up会先检查本地是否有对应镜像,如果没有才会尝试拉取。我们自定义镜像已经在本地存在,所以它不会去拉官方镜像。启动完成后,查看日志确认服务状态:
docker compose logs -f api再访问/install完成初始化,或者直接登录控制台验证你的改动是否生效。
4.4 镜像推送到私有仓库
如果这套自定义版本要给团队其他人用,或者要部署到多台服务器,需要把镜像推到你们自己的私有镜像仓库,比如 Harbor、Registry 或者云厂商的容器镜像服务。
docker tag dify-api:custom-1.10.0 registry.example.com/dify/dify-api:custom-1.10.0 docker push registry.example.com/dify/dify-api:custom-1.10.0服务器上只需要把 docker-compose 里的 image 改成私有仓库地址,拉取启动即可。这样整个二次开发产物就真正变成了可分发、可复用的资产。
5. 构建过程中的常见坑与排查技巧
5.1 构建速度很慢,甚至反复失败
DIFY 后端依赖大量 Python 包,前端依赖大量 npm 包,第一次构建慢是正常的,但如果出现反复卡住或超时,就要考虑换依赖源。国内环境下可以直接把默认源切换成国内公共源,不涉及任何环境变量之外的改动。
后端构建临时改 pip 源,可以在api/Dockerfile里看到 pip install 部分,加一个-i参数,例如:
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple前端构建时 npm 源可以这样处理,在web/目录下临时配置:
npm config set registry https://registry.npmmirror.com不过直接在 Dockerfile 里改源会影响可维护性,我更推荐的做法是理解官方 Dockerfile 使用的构建参数和安装阶段,然后通过 build-arg 或临时环境变量的方式传递源地址。但很多项目图省事直接改了 Dockerfile,也能跑通,就看你自己权衡。
5.2 改了代码,构建后却没效果
这个我前面提到过,最常见的原因有几种:
- 改的值是从环境变量或数据库读取的,代码里的默认值根本不会被用到。
- 前端浏览器缓存了旧的静态资源,看起来像是没生效,强制刷新或者换个无痕窗口看下。
- 默认提示词等初始化数据已经写入数据库,存量数据不会自动读新默认值。
- 构建时 Docker 缓存了旧代码层。确认代码确实在构建上下文里,并且改动文件的时间戳正常。
排查建议:先在源码根目录用grep确认你要改的字符串是写死的,还是通过变量/环境变量引入的。确认之后,修改时在代码里加一行临时日志输出,构建启动后看日志,最快判断改动有没有真正进入运行环境。
5.3 前后端版本不匹配导致白屏或接口报错
DIFY 的 api 和 web 两个服务是分开构建的,如果后端代码改了接口结构、环境变量名,而前端没有同步适配,就可能出现页面打不开、接口返回 500 这类问题。
最典型的场景是:你只改了后端源码,但 docker-compose 里 web 服务用的还是官方旧镜像。新旧版本之间接口不兼容,前端自然跑不起来。解决方案就是确保每次自定义构建时,前后端版本保持同一个 tag 基线,改了后端就同步确认前端版本对着,改了前端也要确认后端能对得上。
5.4 基础镜像拉取失败和构建上下文过大
构建 DIFY 这样的大项目,基础镜像是否能在本地命中很关键。官方 Dockerfile 一般会固定基础镜像的版本号,比如python:3.10-slim、node:20-alpine这类。如果构建时卡在拉取基础镜像这一步,可以先手动把基础镜像拉下来,再执行构建。
上下文过大是另一个隐蔽问题。构建命令里末尾的上下文路径不要随手写当前目录。DIFY 仓库里有很多历史文件、示例资源、SDK 代码,你构建后端镜像只需要api/下的内容,构建前端镜像只需要web/下的内容。上下文路径写对了,不仅能减少构建准备时间,还能避免一些无谓的缓存失效。
5.5 个人习惯:每次改动尽量集中、离散
最后分享一个我自己执行这类二次开发时养成的习惯。每一次自定义改动,尽可能做成一个小而清晰的 commit,commit message 写清楚“改了什么、为什么改、对版本有什么影响”。比如:
git add api/core/agent/xxx.py git commit -m "feat: adjust agent default prompt - change default prompt to fit enterprise knowledge scenario - only affects newly created apps"这个习惯在后期升级官方版本时非常有用。官方发一个新版本,你可以回到主分支拉取新 tag,然后在自己的分支上执行git rebase或git merge,通过 diff 一眼看出哪些自定义改动需要重新适配,哪些已经合并进官方版本,冲突处理起来也有据可查。
如果从头到尾只改不记录,几个月后你自己都不知道这个镜像里跟官方版本差了多少东西,那样部署到生产环境是有很大风险的。代码管理这件事,在二次开发里从来都不是可选项,而是必修课。